Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 3ee22fd

Browse files
committed
fix(firmware): update quota response format to match API (used/reset)
The Firmware.ai /api/v1/quota endpoint returns: - used: Amount used in current window (0 to 1 scale, 1 = limit reached) - reset: ISO timestamp when quota resets Updated: - getFirmwareQuota fetcher to return {used, reset} - Message handler to send used/reset fields - useFirmwareQuota hook interface - FirmwareQuotaDisplay to show percentage and time until reset - Tests for new response format Display now shows: - Normal: '52% used · resets in 2h 30m' - Warning (>80%): yellow styling - At limit (100%): 'Limit reached · resets in Xm' with red styling
1 parent 8a4c0d5 commit 3ee22fd

5 files changed

Lines changed: 56 additions & 25 deletions

File tree

src/api/providers/__tests__/firmware.spec.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -483,8 +483,8 @@ describe("Firmware fetchers", () => {
483483
mockFetch.mockResolvedValueOnce({
484484
ok: true,
485485
json: async () => ({
486-
remaining: 42.5,
487-
window_hours: 5,
486+
used: 0.5217,
487+
reset: "2026-01-25T11:02:14.242Z",
488488
}),
489489
})
490490

@@ -499,23 +499,20 @@ describe("Firmware fetchers", () => {
499499
}),
500500
)
501501

502-
expect(quota.remaining).toBe(42.5)
503-
expect(quota.windowHours).toBe(5)
502+
expect(quota.used).toBe(0.5217)
503+
expect(quota.reset).toBe("2026-01-25T11:02:14.242Z")
504504
})
505505

506-
it("should handle alternative response fields", async () => {
506+
it("should handle missing fields with defaults", async () => {
507507
mockFetch.mockResolvedValueOnce({
508508
ok: true,
509-
json: async () => ({
510-
balance: 100,
511-
windowHours: 3,
512-
}),
509+
json: async () => ({}),
513510
})
514511

515512
const quota = await getFirmwareQuota("test-key")
516513

517-
expect(quota.remaining).toBe(100)
518-
expect(quota.windowHours).toBe(3)
514+
expect(quota.used).toBe(0)
515+
expect(quota.reset).toBeDefined()
519516
})
520517

521518
it("should throw error on HTTP failure", async () => {

src/api/providers/fetchers/firmware.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,23 @@ export async function getFirmwareModels(apiKey?: string): Promise<ModelRecord> {
117117
}
118118
}
119119

120+
export interface FirmwareQuotaResponse {
121+
used: number // 0 to 1 scale (1 = limit reached)
122+
reset: string // ISO timestamp when quota resets
123+
}
124+
120125
/**
121126
* Fetches quota information from the Firmware.ai API
122127
*
128+
* Response format: { "used": 0.5217, "reset": "2026-01-25T11:02:14.242Z" }
129+
* - used: Amount used in the current window (0 to 1, where 1 = limit reached)
130+
* - reset: ISO timestamp when the quota resets
131+
*
123132
* @param apiKey The API key for the Firmware.ai provider
124133
* @returns A promise that resolves to quota information
125134
* @throws Will throw an error if the request fails
126135
*/
127-
export async function getFirmwareQuota(apiKey: string): Promise<{ remaining: number; windowHours: number }> {
136+
export async function getFirmwareQuota(apiKey: string): Promise<FirmwareQuotaResponse> {
128137
const url = `${FIRMWARE_BASE_URL}/quota`
129138

130139
try {
@@ -150,8 +159,8 @@ export async function getFirmwareQuota(apiKey: string): Promise<{ remaining: num
150159
const data = await response.json()
151160

152161
return {
153-
remaining: data.remaining ?? data.balance ?? 0,
154-
windowHours: data.window_hours ?? data.windowHours ?? 5,
162+
used: data.used ?? 0,
163+
reset: data.reset ?? new Date().toISOString(),
155164
}
156165
} finally {
157166
clearTimeout(timeoutId)

src/core/webview/webviewMessageHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1125,7 +1125,7 @@ export const webviewMessageHandler = async (
11251125
provider.postMessageToWebview({
11261126
type: "firmwareQuota",
11271127
requestId,
1128-
values: { remaining: quota.remaining, windowHours: quota.windowHours },
1128+
values: { used: quota.used, reset: quota.reset },
11291129
})
11301130
} catch (error) {
11311131
const errorMessage = error instanceof Error ? error.message : String(error)

webview-ui/src/components/settings/providers/FirmwareQuotaDisplay.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,44 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
22

33
import { useFirmwareQuota } from "@/components/ui/hooks/useFirmwareQuota"
44

5+
const formatTimeUntilReset = (ms: number): string => {
6+
if (ms <= 0) return "resetting..."
7+
const hours = Math.floor(ms / (1000 * 60 * 60))
8+
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60))
9+
if (hours > 0) return `${hours}h ${minutes}m`
10+
return `${minutes}m`
11+
}
12+
513
export const FirmwareQuotaDisplay = () => {
614
const { data: quota } = useFirmwareQuota()
715

8-
if (quota === null || quota === undefined) {
16+
if (!quota) {
917
return null
1018
}
1119

12-
const formattedRemaining = quota.remaining.toFixed(2)
13-
const billingUrl = "https://app.firmware.ai/billing"
20+
const percentUsed = Math.round(quota.used * 100)
21+
const resetDate = new Date(quota.reset)
22+
const now = new Date()
23+
const msUntilReset = resetDate.getTime() - now.getTime()
24+
25+
const isAtLimit = quota.used >= 1
26+
const isWarning = quota.used >= 0.8
27+
28+
const statusText = isAtLimit
29+
? `Limit reached · resets in ${formatTimeUntilReset(msUntilReset)}`
30+
: `${percentUsed}% used · resets in ${formatTimeUntilReset(msUntilReset)}`
31+
32+
const colorClass = isAtLimit
33+
? "text-vscode-errorForeground"
34+
: isWarning
35+
? "text-vscode-editorWarning-foreground"
36+
: "text-vscode-foreground"
1437

1538
return (
16-
<VSCodeLink href={billingUrl} className="text-vscode-foreground hover:underline whitespace-nowrap">
17-
${formattedRemaining} remaining
39+
<VSCodeLink
40+
href="https://app.firmware.ai/billing"
41+
className={`${colorClass} hover:underline whitespace-nowrap`}>
42+
{statusText}
1843
</VSCodeLink>
1944
)
2045
}

webview-ui/src/components/ui/hooks/useFirmwareQuota.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import type { ExtensionMessage } from "@roo-code/types"
55
import { vscode } from "@src/utils/vscode"
66

77
export interface FirmwareQuotaInfo {
8-
remaining: number
9-
windowHours: number
8+
used: number // 0 to 1 scale (1 = limit reached)
9+
reset: string // ISO timestamp when quota resets
1010
}
1111

1212
export const useFirmwareQuota = () => {
@@ -25,10 +25,10 @@ export const useFirmwareQuota = () => {
2525
window.removeEventListener("message", handleMessage)
2626
clearTimeout(timeout)
2727

28-
if (message.values?.remaining !== undefined) {
28+
if (message.values?.used !== undefined) {
2929
setQuota({
30-
remaining: message.values.remaining,
31-
windowHours: message.values.windowHours ?? 5,
30+
used: message.values.used,
31+
reset: message.values.reset,
3232
})
3333
setError(null)
3434
} else if (message.values?.error) {

0 commit comments

Comments
 (0)