refactor(provider-usage): simplify usage loading

This commit is contained in:
Josh Lambert
2026-08-12 14:12:36 -04:00
parent 763967180b
commit 611bfbc4b6
47 changed files with 702 additions and 380 deletions
+6 -24
View File
@@ -69,30 +69,9 @@ const minimax: Adapter = {
cachePrefixes: ["minimax-direct-"],
async run(ctx) {
const items = await direct(ctx.candidates, ctx.fetch, ctx.source)
const failed = ctx.failedCandidates.flatMap((providerID) =>
ctx.preserve(`minimax-direct-${bindings[providerID].region}`),
)
const skipped = new Set<string>()
if (ctx.failedCandidates.length) {
const shared = ctx.preserve("minimax-direct-shared")
const match = ctx.candidates.find(
(candidate) => ctx.preserve("minimax-direct-shared", fingerprint(candidate.key)).length,
)
const group = match ? ctx.candidates.filter((candidate) => candidate.key === match.key) : []
const id = match
? group.length > 1
? "minimax-direct-shared"
: `minimax-direct-${bindings[match.providerID].region}`
: undefined
const item = items.find((candidate) => candidate.id === id)
if (!item || item.fetchState === "unavailable") {
failed.push(...shared)
if (id) skipped.add(id)
}
}
const merged = [
...new Map([...failed, ...items.filter((item) => !skipped.has(item.id))].map((item) => [item.id, item])).values(),
]
// A candidate is either live or failed, never both, so the id sets are disjoint.
const stale = ctx.failedCandidates.flatMap((id) => ctx.preserve(`minimax-direct-${bindings[id].region}`))
const merged = [...items, ...stale]
ctx.prune(
"minimax-direct-",
merged.map((item) => item.id),
@@ -371,6 +350,9 @@ function makeService(
const results = yield* Effect.promise(() =>
Promise.all(
registry.map((adapter) =>
// Adapters are expected to be total (they absorb their own failures into
// unavailable/stale snapshots). This catch is the containment boundary so a
// faulty future adapter degrades to stale output instead of failing the endpoint.
adapter.run(ctx).catch(
(): AdapterResult => ({
items: adapter.cachePrefixes.flatMap((prefix) =>
@@ -70,26 +70,18 @@ function durationMs(period: CodingPlanQuotaWindow["period"]) {
return period.value * multipliers[period.unit]
}
function periodLabel(period: CodingPlanQuotaWindow["period"]) {
const singular = period.value === 1
if (period.unit === "hour") return `${period.value}-hour quota`
if (period.unit === "day") return singular ? "Daily quota" : `${period.value}-day quota`
if (period.unit === "week") return singular ? "Weekly quota" : `${period.value}-week quota`
return singular ? "Monthly quota" : `${period.value}-month quota`
}
function window(subscriptionId: string, value: CodingPlanQuotaWindow): ProviderUsage.UsageWindow {
const remaining = value.remainingPercent
const duration = durationMs(value.period)
return {
id: `${subscriptionId}:${value.id}`,
label: periodLabel(value.period),
resource: "subscription",
unit: "percent",
orientation: "remaining_percent",
used: Math.max(0, 100 - remaining),
remaining,
limit: 100,
period: value.period,
...(duration !== undefined ? { durationMs: duration } : {}),
resetAt: value.resetsAt,
state: remaining <= 0 ? "exhausted" : "active",
@@ -105,11 +105,17 @@ function duration(start: number | undefined, end: number | undefined) {
return end - start
}
function label(resource: string, kind: "interval" | "weekly", value: number | undefined) {
const prefix = resource === "general" ? "Shared quota" : resource === "video" ? "Video" : resource
if (value === 300 * 60 * 1000) return `${prefix} 5-hour`
if (value === 10_080 * 60 * 1000) return `${prefix} weekly`
return kind === "weekly" ? `${prefix} weekly` : `${prefix} interval`
const hourMs = 60 * 60 * 1000
const dayMs = 24 * hourMs
const weekMs = 7 * dayMs
function cadence(kind: "interval" | "weekly", span: number | undefined): ProviderUsage.UsagePeriod | undefined {
if (kind === "weekly") return { unit: "week", value: 1 }
if (span === undefined) return undefined
if (span % weekMs === 0) return { unit: "week", value: span / weekMs }
if (span % dayMs === 0) return { unit: "day", value: span / dayMs }
if (span % hourMs === 0) return { unit: "hour", value: span / hourMs }
return undefined
}
function window(
@@ -131,8 +137,8 @@ function window(
const span = duration(start, end)
const base = {
id: `${row.model_name}-${kind}`,
label: label(row.model_name, kind, span),
resource: row.model_name,
period: cadence(kind, span),
durationMs: span,
resetAt: reset(end, remains, fetchedAt),
}
@@ -245,53 +251,33 @@ export async function direct(
identity?: string,
) => Promise<ProviderUsage.UsageSnapshot> = (_id, load) => load(),
) {
const groups = new Map<string, Candidate[]>()
for (const candidate of candidates) {
if (!candidate.key.startsWith("sk-cp")) continue
const group = groups.get(candidate.key) ?? []
group.push(candidate)
groups.set(candidate.key, group)
}
// Each configured provider is an independent plan with a stable per-region
// cache cell, even when providers share a credential.
return Promise.all(
[...groups.values()].map(async (group) => {
const shared = group.length > 1
const first = group[0]
const id = shared ? "minimax-direct-shared" : `minimax-direct-${bindings[first.providerID].region}`
// The key fingerprint scopes the cache cell to the configured credential, so
// swapping keys never reuses the previous account's quota via TTL or stale fallback.
const identity = createHash("sha256").update(first.key).digest("hex")
return cached(
id,
async () => {
const responses = await Promise.allSettled(
group.map((candidate) => query(candidate.providerID, candidate.key, fetcher)),
)
const index = responses.findIndex((response) => response.status === "fulfilled")
if (index === -1) {
return unavailable(
id,
first.providerID,
shared ? "Direct MiniMax" : first.label,
bindings[first.providerID].manage,
)
}
const candidate = group[index]
const response = responses[index]
if (response.status !== "fulfilled") {
return unavailable(id, candidate.providerID, "Direct MiniMax", bindings[candidate.providerID].manage)
}
return normalize(response.value, {
id,
providerID: candidate.providerID,
sourceLabel: candidate.label,
managementUrl: bindings[candidate.providerID].manage,
fetchedAt: new Date().toISOString(),
})
},
identity,
)
}),
candidates
.filter((candidate) => candidate.key.startsWith("sk-cp"))
.map((candidate) => {
const binding = bindings[candidate.providerID]
const id = `minimax-direct-${binding.region}`
// The key fingerprint scopes the cache cell to the configured credential, so
// swapping keys never reuses the previous account's quota via TTL or stale fallback.
const identity = createHash("sha256").update(candidate.key).digest("hex")
return cached(
id,
() =>
query(candidate.providerID, candidate.key, fetcher)
.then((native) =>
normalize(native, {
id,
providerID: candidate.providerID,
sourceLabel: candidate.label,
managementUrl: binding.manage,
fetchedAt: new Date().toISOString(),
}),
)
.catch(() => unavailable(id, candidate.providerID, candidate.label, binding.manage)),
identity,
)
}),
)
}
@@ -80,7 +80,8 @@ describe("managed provider usage", () => {
windows: [
{
id: "byteplus-plan:monthly",
label: "Monthly quota",
resource: "subscription",
period: { unit: "month", value: 1 },
remaining: 75,
used: 25,
limit: 100,
@@ -44,6 +44,7 @@ describe("MiniMax usage normalization", () => {
remaining: 80,
used: 20,
limit: 100,
period: { unit: "hour", value: 5 },
resetAt: "2026-06-19T05:00:00.000Z",
})
expect(direct.windows[0]?.remaining).not.toBe(1)
@@ -112,6 +113,7 @@ describe("MiniMax usage normalization", () => {
orientation: "amount",
remaining: 150,
limit: 150,
period: { unit: "week", value: 1 },
})
})
@@ -151,7 +153,7 @@ describe("MiniMax usage transport and detection", () => {
expect(fn).not.toHaveBeenCalled()
})
test("deduplicates a shared credential while probing fixed regions", async () => {
test("keeps same-key providers as independent regional plans", async () => {
const fn = mock((url: string | URL | Request) =>
Promise.resolve(
String(url).includes("api.minimax.io")
@@ -168,8 +170,17 @@ describe("MiniMax usage transport and detection", () => {
)
expect(fn).toHaveBeenCalledTimes(2)
expect(items).toHaveLength(1)
expect(items[0]).toMatchObject({ id: "minimax-direct-shared", providerID: "minimax-cn-coding-plan" })
expect(items).toHaveLength(2)
expect(items.find((item) => item.id === "minimax-direct-global")).toMatchObject({
providerID: "minimax-coding-plan",
sourceLabel: "MiniMax Global",
fetchState: "unavailable",
})
expect(items.find((item) => item.id === "minimax-direct-china")).toMatchObject({
providerID: "minimax-cn-coding-plan",
sourceLabel: "MiniMax China",
fetchState: "ready",
})
expect(JSON.stringify(items)).not.toContain("sk-cp-shared")
})
@@ -190,15 +201,15 @@ describe("MiniMax usage transport and detection", () => {
expect(JSON.stringify(seen)).not.toContain("sk-cp")
})
test("returns one unavailable item when an ambiguous key fails everywhere", async () => {
test("returns per-provider unavailable items when every query fails", async () => {
const fn = mock(() => Promise.resolve(response({ message: "raw failure" }, 500)))
const items = await direct(
[candidate("minimax-coding-plan", "sk-cp-shared"), candidate("minimax-cn-coding-plan", "sk-cp-shared")],
fn as unknown as typeof fetch,
)
expect(items).toHaveLength(1)
expect(items[0]).toMatchObject({ id: "minimax-direct-shared", fetchState: "unavailable" })
expect(items).toHaveLength(2)
expect(items.every((item) => item.fetchState === "unavailable")).toBe(true)
expect(JSON.stringify(items)).not.toContain("raw failure")
})
})
@@ -1,4 +1,4 @@
import { describe, expect, mock } from "bun:test"
import { describe, expect, mock, setSystemTime } from "bun:test"
import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect"
import { Catalog } from "../src/catalog"
import { Credential } from "../src/credential"
@@ -406,6 +406,54 @@ describe("ProviderUsage location service", () => {
}),
)
it.live("expires the success cache and retries failures on the shorter error TTL", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
const responses = [native(80), new Response("upstream failure", { status: 503 }), native(70)]
const base = Date.parse("2026-08-12T00:00:00.000Z")
setSystemTime(new Date(base))
const scope = yield* Scope.make()
const usage = Context.get(
yield* Layer.buildWithScope(
configuredLayer({
calls,
direct: "sk-cp-direct",
accountID: "org",
transport: {
fetch: mock(() => {
calls.direct++
return Promise.resolve(responses.shift()!)
}) as unknown as typeof fetch,
plans: async () => [],
byok: async () => [],
usage: async () => {
throw new Error("unused")
},
},
}),
scope,
),
ProviderUsage.Service,
)
expect((yield* usage.get()).items[0]).toMatchObject({ fetchState: "ready", windows: [{ remaining: 80 }] })
expect((yield* usage.get()).items[0]).toMatchObject({ fetchState: "ready" })
expect(calls.direct).toBe(1)
// Success TTL (60s) elapsed: the next get refetches and degrades to stale on failure.
setSystemTime(new Date(base + 61_000))
expect((yield* usage.get()).items[0]).toMatchObject({ fetchState: "stale", windows: [{ remaining: 80 }] })
expect((yield* usage.get()).items[0]).toMatchObject({ fetchState: "stale" })
expect(calls.direct).toBe(2)
// Error TTL (10s) elapsed: the failure is retried and recovers.
setSystemTime(new Date(base + 72_000))
expect((yield* usage.get()).items[0]).toMatchObject({ fetchState: "ready", windows: [{ remaining: 70 }] })
expect(calls.direct).toBe(3)
yield* Scope.close(scope, Exit.void)
}).pipe(Effect.ensuring(Effect.sync(() => setSystemTime()))),
)
it.live("prunes authoritative removal but preserves a transient credential failure", () =>
Effect.gen(function* () {
const removedCalls = { direct: 0, cloud: 0 }
@@ -470,7 +518,7 @@ describe("ProviderUsage location service", () => {
}),
)
it.live("does not duplicate shared usage when one provider credential fails", () =>
it.live("keeps same-key providers independent when one credential fails", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
let failure: "global" | undefined
@@ -489,17 +537,19 @@ describe("ProviderUsage location service", () => {
ProviderUsage.Service,
)
expect((yield* usage.get()).items).toHaveLength(1)
expect((yield* usage.get()).items).toHaveLength(2)
failure = "global"
const result = yield* usage.get()
expect(result.items).toHaveLength(1)
expect(result.items[0]).toMatchObject({ providerID: "minimax-cn-coding-plan", fetchState: "ready" })
expect(calls.direct).toBe(3)
expect(result.items).toHaveLength(2)
expect(result.items.find((item) => item.id === "minimax-direct-global")).toMatchObject({ fetchState: "stale" })
expect(result.items.find((item) => item.id === "minimax-direct-china")).toMatchObject({ fetchState: "ready" })
// The surviving sibling serves from cache; the failed one is not refetched.
expect(calls.direct).toBe(2)
}),
)
it.live("preserves failed shared usage when the surviving provider rotates credentials", () =>
it.live("refreshes a rotated credential while preserving the failed sibling's cached usage", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
let failure: "global" | undefined
@@ -519,23 +569,21 @@ describe("ProviderUsage location service", () => {
ProviderUsage.Service,
)
expect((yield* usage.get()).items).toHaveLength(1)
expect((yield* usage.get()).items).toHaveLength(2)
failure = "global"
key = "sk-cp-rotated"
const result = yield* usage.get()
expect(result.items).toHaveLength(2)
expect(result.items.find((item) => item.id === "minimax-direct-shared")).toMatchObject({ fetchState: "stale" })
expect(result.items.find((item) => item.id === "minimax-direct-global")).toMatchObject({ fetchState: "stale" })
expect(result.items.find((item) => item.id === "minimax-direct-china")).toMatchObject({ fetchState: "ready" })
expect(calls.direct).toBe(3)
}),
)
it.live("preserves shared usage when the surviving same-key query fails", () =>
it.live("omits a failed provider with no cached usage", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
let failure: "global" | undefined
let queryFailure = false
const scope = yield* Scope.make()
const usage = Context.get(
yield* Layer.buildWithScope(
@@ -544,32 +592,18 @@ describe("ProviderUsage location service", () => {
accountID: "org",
config: { china: true },
direct: "sk-cp-shared",
failure: () => failure,
transport: {
fetch: mock(() => {
calls.direct++
return queryFailure ? Promise.reject(new Error("private query failure")) : Promise.resolve(native(80))
}) as unknown as typeof fetch,
plans: async () => [],
byok: async () => [],
usage: async () => {
throw new Error("unused")
},
},
failure: () => "global" as const,
}),
scope,
),
ProviderUsage.Service,
)
expect((yield* usage.get()).items).toHaveLength(1)
failure = "global"
queryFailure = true
const result = yield* usage.get()
expect(result.items).toHaveLength(1)
expect(result.items[0]).toMatchObject({ id: "minimax-direct-shared", fetchState: "stale" })
expect(JSON.stringify(result)).not.toContain("private query failure")
expect(result.items[0]).toMatchObject({ id: "minimax-direct-china", fetchState: "ready" })
expect(calls.direct).toBe(1)
}),
)
@@ -6,10 +6,17 @@
* so the two surfaces can never drift; labels are injectable for i18n.
*/
export interface UsagePeriodLike {
unit: "hour" | "day" | "week" | "month"
value: number
}
export interface UsageWindowLike {
state: "active" | "exhausted" | "unlimited" | "not_in_plan" | "unknown"
orientation: "used_percent" | "remaining_percent" | "amount" | "count"
unit: string
resource: string
period?: UsagePeriodLike
used?: number
remaining?: number
limit?: number
@@ -24,6 +31,17 @@ export interface UsageLabels {
remaining(value: string): string
remainingOf(value: string, limit: string): string
usedOf(value: string, limit: string): string
quota: string
daily: string
weekly: string
monthly: string
hours(count: number): string
days(count: number): string
weeks(count: number): string
months(count: number): string
/** Display name for MiniMax's pooled "general" resource. */
shared: string
scoped(resource: string, period: string): string
}
export const english: UsageLabels = {
@@ -35,6 +53,30 @@ export const english: UsageLabels = {
remaining: (value) => `${value} remaining`,
remainingOf: (value, limit) => `${value} of ${limit} remaining`,
usedOf: (value, limit) => `${value} of ${limit} used`,
quota: "Quota",
daily: "Daily quota",
weekly: "Weekly quota",
monthly: "Monthly quota",
hours: (count) => `${count}-hour quota`,
days: (count) => `${count}-day quota`,
weeks: (count) => `${count}-week quota`,
months: (count) => `${count}-month quota`,
shared: "Shared",
scoped: (resource, period) => `${resource} · ${period}`,
}
const period = (value: UsagePeriodLike, labels: UsageLabels) => {
if (value.unit === "hour") return labels.hours(value.value)
if (value.unit === "day") return value.value === 1 ? labels.daily : labels.days(value.value)
if (value.unit === "week") return value.value === 1 ? labels.weekly : labels.weeks(value.value)
return value.value === 1 ? labels.monthly : labels.months(value.value)
}
export const windowLabel = (window: UsageWindowLike, labels: UsageLabels = english) => {
const phrase = window.period ? period(window.period, labels) : labels.quota
// Plan-level windows ("subscription") are the whole card; named resources prefix theirs.
if (window.resource === "subscription") return phrase
return labels.scoped(window.resource === "general" ? labels.shared : window.resource, phrase)
}
const number = (value: number) => value.toLocaleString(undefined, { maximumFractionDigits: 2 })
+20 -31
View File
@@ -3,6 +3,7 @@ import { existsSync } from "fs"
import * as vscode from "vscode"
import type {
KiloClient,
ProviderUsage,
Session,
SessionStatus,
Event,
@@ -367,8 +368,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
/** Cached notificationsLoaded payload */
private cachedNotificationsMessage: NotificationsMessage | null = null
/** Cached provider usage payload for profile view remounts and temporary disconnects. */
private cachedProviderUsageMessage: { type: "providerUsageLoaded"; data: unknown } | null = null
private providerUsageRequested = false
private cachedProviderUsageMessage: { type: "providerUsageLoaded"; data: ProviderUsage } | null = null
private providerUsageGeneration = 0
private pendingKiloModel: { modelID?: string; agent?: string } | null = null
private pendingReviewComments: { comments: unknown[]; autoSend: boolean }[] = []
@@ -575,7 +575,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.postMessage({ type: "workspaceDirectoryChanged", directory: directory ?? "" })
this.postMessage({ type: "configBindingExpired", reason: "project-changed" })
this.requirements.clear()
if (this.providerUsageRequested) void this.fetchAndSendProviderUsage()
}
public setDiffVirtualProvider(provider: import("./DiffVirtualProvider").DiffVirtualProvider): void {
@@ -1560,18 +1559,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
await handleRefreshProfile(this.authCtx)
return true
}
if (message.type === "requestProviderUsage") {
await this.fetchAndSendProviderUsage()
return true
}
if (message.type === "refreshProviderUsage") {
await this.fetchAndSendProviderUsage(true)
return true
}
if (message.type === "releaseProviderUsage") {
// Profile view unmounted: stop background refreshes on directory/auth
// changes until the view requests usage again.
this.providerUsageRequested = false
await this.fetchAndSendProviderUsage()
return true
}
return false
@@ -2370,32 +2359,28 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}
private async fetchAndSendProviderUsage(force = false): Promise<void> {
this.providerUsageRequested = true
private async fetchAndSendProviderUsage(): Promise<void> {
const generation = ++this.providerUsageGeneration
const client = this.client
if (!client) {
if (this.cachedProviderUsageMessage) this.postMessage(this.cachedProviderUsageMessage)
this.postMessage(
this.cachedProviderUsageMessage ?? {
type: "providerUsageLoaded",
error: "Provider usage could not be loaded.",
},
)
return
}
const directory = this.getProjectDirectory(this.currentSession?.id)
const result = await (
force ? client.kilocode.providerUsage.refresh({ directory }) : client.kilocode.providerUsage.get({ directory })
).catch((error) => {
const result = await client.kilocode.providerUsage.refresh({ directory }).catch((error) => {
console.error("[Kilo New] KiloProvider: Failed to fetch provider usage:", error)
return undefined
})
if (generation !== this.providerUsageGeneration) return
if (!result?.data) {
if (this.cachedProviderUsageMessage) {
// Re-serve the cached data, but tell the user when their explicit
// refresh failed instead of silently presenting stale data as fresh.
this.postMessage(
force
? { ...this.cachedProviderUsageMessage, error: "Provider usage could not be refreshed." }
: this.cachedProviderUsageMessage,
)
this.postMessage({ ...this.cachedProviderUsageMessage, error: "Provider usage could not be refreshed." })
return
}
this.postMessage({ type: "providerUsageLoaded", error: "Provider usage could not be loaded." })
@@ -4038,12 +4023,19 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
postMessage: (msg) => this.postMessage(msg),
getWorkspaceDirectory: () => this.getWorkspaceDirectory(),
disposeGlobal: () => this.disposeGlobal(),
invalidateProviderUsage: () => this.invalidateProviderUsage(),
fetchAndSendProviders: () => this.fetchAndSendProviders(),
fetchAndSendAgents: () => this.fetchAndSendAgents(),
fetchAndSendSpeechToTextModels: () => this.fetchAndSendSpeechToTextModels(),
}
}
private invalidateProviderUsage(): void {
this.providerUsageGeneration++
this.cachedProviderUsageMessage = null
this.postMessage({ type: "providerUsageLoaded", reset: true })
}
private async disposeGlobal(): Promise<void> {
if (!this.client) return
@@ -4188,9 +4180,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
/** Re-fetch all server-side state after an auth change. */
private async reloadAfterAuthChange(): Promise<void> {
this.requirements.clear()
this.providerUsageGeneration++
this.cachedProviderUsageMessage = null
if (this.providerUsageRequested) this.postMessage({ type: "providerUsageLoaded", reset: true })
this.invalidateProviderUsage()
await this.fetchAndSendConfig()
await Promise.all([
this.fetchAndSendProviders(),
@@ -4199,7 +4189,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.fetchAndSendCommands(),
this.fetchAndSendIndexingStatus(),
this.fetchAndSendNotifications(),
this.providerUsageRequested ? this.fetchAndSendProviderUsage() : Promise.resolve(),
])
}
@@ -13,6 +13,7 @@ export interface AuthContext {
postMessage(msg: unknown): void
getWorkspaceDirectory(): string
disposeGlobal(): Promise<void>
invalidateProviderUsage(): void
fetchAndSendProviders(): Promise<void>
fetchAndSendAgents(): Promise<void>
fetchAndSendSpeechToTextModels(): Promise<void>
@@ -60,6 +61,7 @@ export async function handleLogin(ctx: AuthContext, attempt: number, getAttempt:
console.log("[Kilo New] KiloProvider: 🔐 Login successful")
ctx.invalidateProviderUsage()
await ctx.disposeGlobal()
// Step 3: Fetch profile and push to webview
@@ -85,6 +87,7 @@ export async function handleLogout(ctx: AuthContext): Promise<void> {
console.log("[Kilo New] KiloProvider: 🚪 Logged out successfully")
ctx.postMessage({ type: "profileData", data: null })
ctx.invalidateProviderUsage()
await ctx.disposeGlobal()
await ctx.fetchAndSendProviders()
@@ -119,6 +122,7 @@ export async function handleSetOrganization(ctx: AuthContext, organizationId: st
return
}
ctx.invalidateProviderUsage()
await ctx.disposeGlobal()
// Org switch succeeded — refresh profile and providers independently (best-effort)
@@ -1,6 +1,6 @@
import { describe, expect, it } from "bun:test"
import type { ProviderUsage, ProviderUsageWindow } from "@kilocode/sdk/v2/client"
import { formatWindow, windowProgress } from "@kilocode/kilo-gateway/provider-usage"
import { formatWindow, windowLabel, windowProgress } from "@kilocode/kilo-gateway/provider-usage"
const { KiloProvider } = await import("../../src/KiloProvider")
@@ -10,43 +10,46 @@ const data: ProviderUsage = {
}
type Internals = {
providerUsageRequested: boolean
cachedProviderUsageMessage: unknown
fetchAndSendProviderUsage: (force?: boolean) => Promise<void>
handleProfileDataMessage: (message: { type: string }) => Promise<boolean>
fetchAndSendProviderUsage: () => Promise<void>
reloadAfterAuthChange: () => Promise<void>
postMessage: (message: unknown) => void
fetchAndSendConfig: () => Promise<void>
fetchAndSendProviders: () => Promise<void>
fetchAndSendAgents: () => Promise<void>
fetchAndSendSkills: () => Promise<void>
fetchAndSendCommands: () => Promise<void>
fetchAndSendIndexingStatus: () => Promise<void>
fetchAndSendNotifications: () => Promise<void>
}
type UsageClient = {
get: (input: { directory?: string }) => Promise<unknown>
refresh?: (input: { directory?: string }) => Promise<unknown>
refresh: (input: { directory?: string }) => Promise<unknown>
}
// Answers any SDK endpoint outside the fake usage client with a benign empty
// response, so tests never have to mirror KiloProvider's internal fetcher list.
const benign = (value: unknown): unknown =>
typeof value === "function"
? value
: new Proxy(() => {}, {
get: (_, prop) =>
prop === "then" ? undefined : benign((value as Record<PropertyKey, unknown> | undefined)?.[prop]),
apply: () => Promise.resolve({ data: [] }),
})
function bridge(usage: UsageClient) {
const messages: unknown[] = []
const provider = new KiloProvider(
{} as never,
{ getClient: () => ({ kilocode: { providerUsage: usage } }) } as never,
{ getClient: () => benign({ kilocode: { providerUsage: usage } }) } as never,
undefined,
{ projectDirectory: "/repo" },
)
const internal = provider as unknown as Internals
internal.postMessage = (message) => messages.push(message)
return { internal, messages }
return { provider, internal, messages }
}
const usageMessages = (messages: unknown[]) =>
messages.filter((message) => (message as { type?: string }).type === "providerUsageLoaded")
describe("provider usage presentation", () => {
const window = (value: Partial<ProviderUsageWindow>): ProviderUsageWindow => ({
id: "quota",
label: "Quota",
resource: "general",
unit: "percent",
orientation: "remaining_percent",
@@ -66,17 +69,20 @@ describe("provider usage presentation", () => {
expect(formatWindow(window({ state: "unlimited" }))).toBe("Unlimited")
expect(formatWindow(window({ state: "not_in_plan" }))).toBe("Not in plan")
})
it("composes window labels from structured periods instead of wire strings", () => {
expect(windowLabel(window({ resource: "subscription", period: { unit: "month", value: 1 } }))).toBe("Monthly quota")
expect(windowLabel(window({ resource: "subscription", period: { unit: "day", value: 3 } }))).toBe("3-day quota")
expect(windowLabel(window({ period: { unit: "hour", value: 5 } }))).toBe("Shared · 5-hour quota")
expect(windowLabel(window({ period: { unit: "week", value: 1 } }))).toBe("Shared · Weekly quota")
expect(windowLabel(window({ resource: "image" }))).toBe("image · Quota")
})
})
describe("KiloProvider provider usage bridge", () => {
it("uses cache-aware GET and explicit refresh POST", async () => {
const get: Array<{ directory?: string }> = []
it("loads usage only through an explicit refresh", async () => {
const refresh: Array<{ directory?: string }> = []
const { internal, messages } = bridge({
get: async (input) => {
get.push(input)
return { data }
},
refresh: async (input) => {
refresh.push(input)
return { data }
@@ -84,36 +90,26 @@ describe("KiloProvider provider usage bridge", () => {
})
await internal.fetchAndSendProviderUsage()
await internal.fetchAndSendProviderUsage(true)
expect(get).toEqual([{ directory: "/repo" }])
expect(refresh).toEqual([{ directory: "/repo" }])
expect(messages).toEqual([
{ type: "providerUsageLoaded", data },
{ type: "providerUsageLoaded", data },
])
expect(internal.providerUsageRequested).toBe(true)
expect(messages).toEqual([{ type: "providerUsageLoaded", data }])
expect(internal.cachedProviderUsageMessage).toEqual({ type: "providerUsageLoaded", data })
})
it("surfaces a failed forced refresh alongside the cached data", async () => {
const { internal, messages } = bridge({
get: async () => ({ data }),
refresh: async () => ({ error: { _tag: "ServiceUnavailable" } }),
})
internal.cachedProviderUsageMessage = { type: "providerUsageLoaded", data }
await internal.fetchAndSendProviderUsage()
await internal.fetchAndSendProviderUsage(true)
expect(messages).toEqual([
{ type: "providerUsageLoaded", data },
{ type: "providerUsageLoaded", data, error: "Provider usage could not be refreshed." },
])
expect(messages).toEqual([{ type: "providerUsageLoaded", data, error: "Provider usage could not be refreshed." }])
})
it("posts a terminal loading error when the backend has no cached response", async () => {
const { internal, messages } = bridge({
get: async () => ({ error: { _tag: "ServiceUnavailable" } }),
refresh: async () => ({ error: { _tag: "ServiceUnavailable" } }),
})
await internal.fetchAndSendProviderUsage()
@@ -121,35 +117,76 @@ describe("KiloProvider provider usage bridge", () => {
expect(messages).toEqual([{ type: "providerUsageLoaded", error: "Provider usage could not be loaded." }])
})
it("refreshes usage after auth invalidation only after the profile requested it", async () => {
const provider = new KiloProvider({} as never, {} as never)
const internal = provider as unknown as Internals
let usage = 0
internal.fetchAndSendConfig = async () => {}
internal.fetchAndSendProviders = async () => {}
internal.fetchAndSendAgents = async () => {}
internal.fetchAndSendSkills = async () => {}
internal.fetchAndSendCommands = async () => {}
internal.fetchAndSendIndexingStatus = async () => {}
internal.fetchAndSendNotifications = async () => {}
internal.fetchAndSendProviderUsage = async () => {
usage++
}
await internal.reloadAfterAuthChange()
expect(usage).toBe(0)
internal.providerUsageRequested = true
await internal.reloadAfterAuthChange()
expect(usage).toBe(1)
})
it("releases the background refresh latch when the profile view unmounts", async () => {
const { internal } = bridge({ get: async () => ({ data }) })
it("invalidates cached usage without reloading on auth change", async () => {
const requests: unknown[] = []
const { internal, messages } = bridge({
refresh: async (input) => {
requests.push(input)
return { data: { generatedAt: "a", items: [] } }
},
})
await internal.fetchAndSendProviderUsage()
expect(internal.providerUsageRequested).toBe(true)
await internal.reloadAfterAuthChange()
expect(await internal.handleProfileDataMessage({ type: "releaseProviderUsage" })).toBe(true)
expect(internal.providerUsageRequested).toBe(false)
expect(usageMessages(messages)).toEqual([
{ type: "providerUsageLoaded", data: { generatedAt: "a", items: [] } },
{ type: "providerUsageLoaded", reset: true },
])
expect(requests).toHaveLength(1)
expect(internal.cachedProviderUsageMessage).toBeNull()
})
it("resets usage without fetching when auth changes before the profile is opened", async () => {
const requests: unknown[] = []
const { internal, messages } = bridge({
refresh: async (input) => {
requests.push(input)
return { data }
},
})
await internal.reloadAfterAuthChange()
expect(requests).toEqual([])
expect(usageMessages(messages)).toEqual([{ type: "providerUsageLoaded", reset: true }])
})
it("drops an in-flight usage response from the previous account", async () => {
let release!: (value: { data: ProviderUsage }) => void
const first = new Promise<{ data: ProviderUsage }>((resolve) => (release = resolve))
const calls: unknown[] = []
const { internal, messages } = bridge({
refresh: (input) => {
calls.push(input)
return first
},
})
const hung = internal.fetchAndSendProviderUsage()
await internal.reloadAfterAuthChange()
release({ data: { generatedAt: "a", items: [] } })
await hung
expect(usageMessages(messages)).toEqual([{ type: "providerUsageLoaded", reset: true }])
expect(calls).toHaveLength(1)
expect(internal.cachedProviderUsageMessage).toBeNull()
})
it("invalidates cached usage without fetching when the workspace directory changes", async () => {
const requests: unknown[] = []
const { provider, internal, messages } = bridge({
refresh: async (input) => {
requests.push(input)
return { data }
},
})
await internal.fetchAndSendProviderUsage()
provider.setProjectDirectory("/other")
expect(requests).toHaveLength(1)
expect(internal.cachedProviderUsageMessage).toBeNull()
expect(messages).toContainEqual({ type: "workspaceDirectoryChanged", directory: "/other" })
})
})
@@ -364,9 +364,7 @@ const AppContent: Component = () => {
providerUsageError={server.providerUsageError()}
deviceAuth={server.deviceAuth()}
onLogin={server.startLogin}
onRequestProviderUsage={server.requestProviderUsage}
onRefreshProviderUsage={server.refreshProviderUsage}
onReleaseProviderUsage={server.releaseProviderUsage}
/>
</Match>
<Match when={currentView() === "settings"}>
@@ -1,4 +1,4 @@
import { Component, Show, createSignal, createMemo, createEffect, onCleanup, onMount } from "solid-js"
import { Component, Show, createSignal, createMemo, createEffect, onMount } from "solid-js"
import { Button } from "@kilocode/kilo-ui/button"
import { Card } from "@kilocode/kilo-ui/card"
import { Icon } from "@kilocode/kilo-ui/icon"
@@ -19,9 +19,7 @@ export interface ProfileViewProps {
providerUsageLoading?: boolean
providerUsageError?: string
onLogin: () => void
onRequestProviderUsage?: () => void
onRefreshProviderUsage?: () => void
onReleaseProviderUsage?: () => void
}
const formatBalance = (amount: number): string => {
@@ -43,15 +41,12 @@ const ProfileView: Component<ProfileViewProps> = (props) => {
const personal = createMemo(() => props.profileData?.profile.hasPersonalAccount !== false)
// Always fetch fresh profile+balance when navigating to this view
// Load current profile and usage when navigating to this view.
onMount(() => {
vscode.postMessage({ type: "refreshProfile" })
props.onRequestProviderUsage?.()
props.onRefreshProviderUsage?.()
})
// Stop background usage refreshes while this view is not visible
onCleanup(() => props.onReleaseProviderUsage?.())
// Reset pending target whenever profileData changes (success or failure both send a fresh profile)
createEffect(() => {
props.profileData // track
@@ -9,7 +9,7 @@ import { Spinner } from "@kilocode/kilo-ui/spinner"
import { Tag } from "@kilocode/kilo-ui/tag"
import { useLanguage } from "../../context/language"
import { localeToBcp47 } from "../../context/language-utils"
import { formatWindow, windowProgress } from "@kilocode/kilo-gateway/provider-usage"
import { formatWindow, windowLabel, windowProgress } from "@kilocode/kilo-gateway/provider-usage"
export interface ProviderUsageCardsProps {
data: ProviderUsageData | undefined
@@ -38,6 +38,16 @@ const labels = (language: Language) => ({
remaining: (value: string) => language.t("profile.usage.window.remaining", { value }),
remainingOf: (value: string, limit: string) => language.t("profile.usage.window.remainingOf", { value, limit }),
usedOf: (value: string, limit: string) => language.t("profile.usage.window.usedOf", { value, limit }),
quota: language.t("profile.usage.window.quota"),
daily: language.t("profile.usage.window.daily"),
weekly: language.t("profile.usage.window.weekly"),
monthly: language.t("profile.usage.window.monthly"),
hours: (count: number) => language.t("profile.usage.window.hours", { count: String(count) }),
days: (count: number) => language.t("profile.usage.window.days", { count: String(count) }),
weeks: (count: number) => language.t("profile.usage.window.weeks", { count: String(count) }),
months: (count: number) => language.t("profile.usage.window.months", { count: String(count) }),
shared: language.t("profile.usage.window.shared"),
scoped: (resource: string, period: string) => language.t("profile.usage.window.scoped", { resource, period }),
})
const variant = (item: ProviderUsageSnapshot) => {
@@ -104,6 +114,7 @@ const UsageCard: Component<{
{(window) => {
const progress = () => windowProgress(window)
const value = () => formatWindow(window, labels(props.language))
const title = () => windowLabel(window, labels(props.language))
return (
<div class="provider-usage-row">
<Show when={progress() !== undefined}>
@@ -113,22 +124,24 @@ const UsageCard: Component<{
maxValue={100}
showValueLabel
getValueLabel={value}
aria-label={`${window.label}: ${value()}`}
aria-label={`${title()}: ${value()}`}
aria-valuetext={value()}
>
{window.label}
{title()}
</Progress>
</Show>
<Show when={progress() === undefined}>
<div class="provider-usage-summary">
<span>{window.label}</span>
<span>{title()}</span>
<strong>{value()}</strong>
</div>
</Show>
<Show when={window.resetAt}>
{(reset) => (
<CardDescription>
{props.language.t("profile.usage.reset", { date: new Date(reset()).toLocaleString() })}
{props.language.t("profile.usage.reset", {
date: new Date(reset()).toLocaleString(localeToBcp47(props.language.locale())),
})}
</CardDescription>
)}
</Show>
@@ -153,7 +166,7 @@ const UsageCard: Component<{
variant="secondary"
size="small"
onClick={() => props.onOpen(url())}
aria-label={`Manage ${props.item.planLabel}`}
aria-label={props.language.t("profile.usage.action.managePlan", { plan: props.item.planLabel })}
>
{props.language.t("profile.usage.action.manage")}
</Button>
@@ -208,11 +221,11 @@ const KiloPassCard: Component<{
used={pass().currentPeriodUsageUsd}
paid={pass().currentPeriodBaseCreditsUsd}
bonus={pass().currentPeriodBonusCreditsUsd}
label="This month's usage"
paidLabel="Paid"
label={props.language.t("profile.pass.usage")}
paidLabel={props.language.t("profile.pass.paid")}
bonusLabel={props.language.t("profile.pass.bonus")}
format={money}
aria-label="Kilo Pass monthly usage"
aria-label={props.language.t("profile.pass.meter")}
/>
<Show when={renewal()}>
{(date) => (
@@ -261,20 +274,20 @@ export const ProviderUsageCards: Component<ProviderUsageCardsProps> = (props) =>
<Show
when={props.data}
fallback={
<Show
when={!props.loading && props.error}
fallback={
<>
<Show when={props.loading}>
<div class="provider-usage-loading" role="status" aria-label={language.t("profile.usage.title")}>
<Spinner />
</div>
}
>
{(error) => (
<Card variant="warning" role="alert">
<CardDescription>{error()}</CardDescription>
</Card>
)}
</Show>
</Show>
<Show when={!props.loading && props.error}>
{(error) => (
<Card variant="warning" role="alert">
<CardDescription>{error()}</CardDescription>
</Card>
)}
</Show>
</>
}
>
{(data) => (
@@ -26,9 +26,7 @@ interface ServerContextValue {
providerUsage: Accessor<ProviderUsageData | undefined>
providerUsageLoading: Accessor<boolean>
providerUsageError: Accessor<string | undefined>
requestProviderUsage: () => void
refreshProviderUsage: () => void
releaseProviderUsage: () => void
deviceAuth: Accessor<DeviceAuthState>
startLogin: () => void
goToLogin: () => void
@@ -54,7 +52,6 @@ export const ServerProvider: ParentComponent = (props) => {
const [providerUsage, setProviderUsage] = createSignal<ProviderUsageData>()
const [providerUsageLoading, setProviderUsageLoading] = createSignal(false)
const [providerUsageError, setProviderUsageError] = createSignal<string>()
let providerUsageRetry: ReturnType<typeof setTimeout> | undefined
const [deviceAuth, setDeviceAuth] = createSignal<DeviceAuthState>(initialDeviceAuth)
const [vscodeLanguage, setVscodeLanguage] = createSignal<string | undefined>()
const [languageOverride, setLanguageOverride] = createSignal<string | undefined>()
@@ -72,12 +69,10 @@ export const ServerProvider: ParentComponent = (props) => {
const usageSub = vscode.onMessage((m: ExtensionMessage) => {
if (m.type !== "providerUsageLoaded") return
if (providerUsageRetry) clearTimeout(providerUsageRetry)
providerUsageRetry = undefined
if (m.reset) {
setProviderUsage(undefined)
setProviderUsageError(undefined)
setProviderUsageLoading(true)
setProviderUsageLoading(false)
return
}
if (m.data) setProviderUsage(m.data)
@@ -85,18 +80,11 @@ export const ServerProvider: ParentComponent = (props) => {
setProviderUsageLoading(false)
})
const usageReadySub = vscode.onMessage((m: ExtensionMessage) => {
if (m.type !== "extensionDataReady" || !providerUsageLoading() || providerUsage()) return
if (providerUsageRetry) clearTimeout(providerUsageRetry)
providerUsageRetry = undefined
vscode.postMessage({ type: "requestProviderUsage" })
})
const resetProviderUsageForDirectory = () => {
if (providerUsage() === undefined && !providerUsageLoading()) return
setProviderUsage(undefined)
setProviderUsageError(undefined)
setProviderUsageLoading(true)
setProviderUsageLoading(false)
}
onMount(() => {
@@ -185,8 +173,6 @@ export const ServerProvider: ParentComponent = (props) => {
gitSub()
fontSub()
usageSub()
usageReadySub()
if (providerUsageRetry) clearTimeout(providerUsageRetry)
unsubscribe()
})
@@ -218,32 +204,10 @@ export const ServerProvider: ParentComponent = (props) => {
startLogin()
}
const retryProviderUsage = () => {
if (providerUsageRetry) clearTimeout(providerUsageRetry)
providerUsageRetry = setTimeout(() => {
providerUsageRetry = undefined
if (providerUsageLoading() && !providerUsage()) vscode.postMessage({ type: "requestProviderUsage" })
}, 3000)
}
const requestProviderUsage = () => {
setProviderUsageLoading(true)
setProviderUsageError(undefined)
vscode.postMessage({ type: "requestProviderUsage" })
retryProviderUsage()
}
const refreshProviderUsage = () => {
setProviderUsageLoading(true)
setProviderUsageError(undefined)
vscode.postMessage({ type: "refreshProviderUsage" })
retryProviderUsage()
}
const releaseProviderUsage = () => {
if (providerUsageRetry) clearTimeout(providerUsageRetry)
providerUsageRetry = undefined
vscode.postMessage({ type: "releaseProviderUsage" })
}
const value: ServerContextValue = {
@@ -257,9 +221,7 @@ export const ServerProvider: ParentComponent = (props) => {
providerUsage,
providerUsageLoading,
providerUsageError,
requestProviderUsage,
refreshProviderUsage,
releaseProviderUsage,
deviceAuth,
startLogin,
goToLogin,
+14
View File
@@ -663,6 +663,7 @@ export const dict = {
"profile.usage.plan.canceling": "الخطة: تُلغى في نهاية الفترة",
"profile.usage.plan.unknown": "الخطة: الحالة غير معروفة",
"profile.usage.action.manage": "إدارة",
"profile.usage.action.managePlan": "إدارة {{plan}}",
"profile.usage.routing": "فوترة الخطة مفعّلة. توجيه Kilo Gateway {{state}}.",
"profile.usage.routingState.disabled": "معطّل",
"profile.usage.routingState.missing": "مفقود",
@@ -672,6 +673,16 @@ export const dict = {
"profile.usage.window.remaining": "متبقي {{value}}",
"profile.usage.window.remainingOf": "متبقي {{value}} من {{limit}}",
"profile.usage.window.usedOf": "تم استخدام {{value}} من {{limit}}",
"profile.usage.window.quota": "الحصة",
"profile.usage.window.daily": "الحصة اليومية",
"profile.usage.window.weekly": "الحصة الأسبوعية",
"profile.usage.window.monthly": "الحصة الشهرية",
"profile.usage.window.hours": "حصة كل {{count}} ساعة",
"profile.usage.window.days": "حصة كل {{count}} يوم",
"profile.usage.window.weeks": "حصة كل {{count}} أسبوع",
"profile.usage.window.months": "حصة كل {{count}} شهر",
"profile.usage.window.shared": "مشتركة",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "تتم إعادة الضبط في {{date}}",
"profile.usage.status.unknown": "غير معروف",
"profile.usage.status.unlimited": "غير محدود",
@@ -681,6 +692,9 @@ export const dict = {
"profile.action.topUp": "إضافة رصيد",
"profile.pass.subscribe": "احصل على Kilo Pass لإضافة رصيد وكسب مكافآت",
"profile.pass.bonus": "مكافأة",
"profile.pass.usage": "استخدام هذا الشهر",
"profile.pass.paid": "مدفوع",
"profile.pass.meter": "استخدام Kilo Pass الشهري",
"profile.pass.renews": "يتجدد",
"profile.action.logout": "تسجيل الخروج",
+14
View File
@@ -679,6 +679,7 @@ export const dict = {
"profile.usage.plan.canceling": "Plano: Cancela no fim do período",
"profile.usage.plan.unknown": "Plano: Status desconhecido",
"profile.usage.action.manage": "Gerenciar",
"profile.usage.action.managePlan": "Gerenciar {{plan}}",
"profile.usage.routing": "A cobrança do plano está ativa. O roteamento do Kilo Gateway está {{state}}.",
"profile.usage.routingState.disabled": "desativado",
"profile.usage.routingState.missing": "ausente",
@@ -688,6 +689,16 @@ export const dict = {
"profile.usage.window.remaining": "{{value}} restante",
"profile.usage.window.remainingOf": "{{value}} de {{limit}} restantes",
"profile.usage.window.usedOf": "{{value}} de {{limit}} usados",
"profile.usage.window.quota": "Cota",
"profile.usage.window.daily": "Cota diária",
"profile.usage.window.weekly": "Cota semanal",
"profile.usage.window.monthly": "Cota mensal",
"profile.usage.window.hours": "Cota de {{count}} horas",
"profile.usage.window.days": "Cota de {{count}} dias",
"profile.usage.window.weeks": "Cota de {{count}} semanas",
"profile.usage.window.months": "Cota de {{count}} meses",
"profile.usage.window.shared": "Compartilhada",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "Redefine em {{date}}",
"profile.usage.status.unknown": "Desconhecido",
"profile.usage.status.unlimited": "Ilimitado",
@@ -697,6 +708,9 @@ export const dict = {
"profile.action.topUp": "Adicionar créditos",
"profile.pass.subscribe": "Obtenha o Kilo Pass para adicionar créditos e ganhar bônus",
"profile.pass.bonus": "Bônus",
"profile.pass.usage": "Uso deste mês",
"profile.pass.paid": "Pago",
"profile.pass.meter": "Uso mensal do Kilo Pass",
"profile.pass.renews": "Renova",
"profile.action.logout": "Sair",
+14
View File
@@ -719,6 +719,7 @@ export const dict = {
"profile.usage.plan.canceling": "Plan: Otkazuje se na kraju perioda",
"profile.usage.plan.unknown": "Plan: Status nepoznat",
"profile.usage.action.manage": "Upravljaj",
"profile.usage.action.managePlan": "Upravljaj planom {{plan}}",
"profile.usage.routing": "Naplata plana je aktivna. Kilo Gateway usmjeravanje je {{state}}.",
"profile.usage.routingState.disabled": "onemogućeno",
"profile.usage.routingState.missing": "odsutno",
@@ -728,6 +729,16 @@ export const dict = {
"profile.usage.window.remaining": "Preostalo {{value}}",
"profile.usage.window.remainingOf": "Preostalo {{value}} od {{limit}}",
"profile.usage.window.usedOf": "Iskorišteno {{value}} od {{limit}}",
"profile.usage.window.quota": "Kvota",
"profile.usage.window.daily": "Dnevna kvota",
"profile.usage.window.weekly": "Sedmična kvota",
"profile.usage.window.monthly": "Mjesečna kvota",
"profile.usage.window.hours": "Kvota od {{count}} sati",
"profile.usage.window.days": "Kvota od {{count}} dana",
"profile.usage.window.weeks": "Kvota od {{count}} sedmica",
"profile.usage.window.months": "Kvota od {{count}} mjeseci",
"profile.usage.window.shared": "Dijeljena",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "Obnavlja se {{date}}",
"profile.usage.status.unknown": "Nepoznato",
"profile.usage.status.unlimited": "Neograničeno",
@@ -737,6 +748,9 @@ export const dict = {
"profile.action.topUp": "Dopuni",
"profile.pass.subscribe": "Nabavite Kilo Pass da dodate kredite i zaradite bonuse",
"profile.pass.bonus": "Bonus",
"profile.pass.usage": "Potrošnja ovog mjeseca",
"profile.pass.paid": "Plaćeno",
"profile.pass.meter": "Mjesečna potrošnja Kilo Passa",
"profile.pass.renews": "Obnavlja se",
"profile.action.logout": "Odjava",
+14
View File
@@ -717,6 +717,7 @@ export const dict = {
"profile.usage.plan.canceling": "Abonnement: Opsiges ved periodens udgang",
"profile.usage.plan.unknown": "Abonnement: Status ukendt",
"profile.usage.action.manage": "Administrer",
"profile.usage.action.managePlan": "Administrer {{plan}}",
"profile.usage.routing": "Abonnementsfakturering er aktiv. Kilo Gateway-routing er {{state}}.",
"profile.usage.routingState.disabled": "deaktiveret",
"profile.usage.routingState.missing": "fraværende",
@@ -726,6 +727,16 @@ export const dict = {
"profile.usage.window.remaining": "{{value}} tilbage",
"profile.usage.window.remainingOf": "{{value}} af {{limit}} tilbage",
"profile.usage.window.usedOf": "{{value}} af {{limit}} brugt",
"profile.usage.window.quota": "Kvote",
"profile.usage.window.daily": "Daglig kvote",
"profile.usage.window.weekly": "Ugentlig kvote",
"profile.usage.window.monthly": "Månedlig kvote",
"profile.usage.window.hours": "{{count}}-timers kvote",
"profile.usage.window.days": "{{count}}-dages kvote",
"profile.usage.window.weeks": "{{count}}-ugers kvote",
"profile.usage.window.months": "{{count}}-måneders kvote",
"profile.usage.window.shared": "Delt",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "Nulstilles den {{date}}",
"profile.usage.status.unknown": "Ukendt",
"profile.usage.status.unlimited": "Ubegrænset",
@@ -735,6 +746,9 @@ export const dict = {
"profile.action.topUp": "Fyld op",
"profile.pass.subscribe": "Få Kilo Pass for at tilføje kredit og optjene bonusser",
"profile.pass.bonus": "Bonus",
"profile.pass.usage": "Denne måneds forbrug",
"profile.pass.paid": "Betalt",
"profile.pass.meter": "Månedligt Kilo Pass-forbrug",
"profile.pass.renews": "Fornyes",
"profile.action.logout": "Log ud",
@@ -730,6 +730,7 @@ export const dict = {
"profile.usage.plan.canceling": "Tarif: Kündigung zum Ende des Abrechnungszeitraums",
"profile.usage.plan.unknown": "Tarif: Status unbekannt",
"profile.usage.action.manage": "Verwalten",
"profile.usage.action.managePlan": "{{plan}} verwalten",
"profile.usage.routing": "Tarifabrechnung ist aktiv. Kilo-Gateway-Routing ist {{state}}.",
"profile.usage.routingState.disabled": "deaktiviert",
"profile.usage.routingState.missing": "nicht vorhanden",
@@ -739,6 +740,16 @@ export const dict = {
"profile.usage.window.remaining": "{{value}} verbleibend",
"profile.usage.window.remainingOf": "{{value}} von {{limit}} verbleibend",
"profile.usage.window.usedOf": "{{value}} von {{limit}} verwendet",
"profile.usage.window.quota": "Kontingent",
"profile.usage.window.daily": "Tägliches Kontingent",
"profile.usage.window.weekly": "Wöchentliches Kontingent",
"profile.usage.window.monthly": "Monatliches Kontingent",
"profile.usage.window.hours": "{{count}}-Stunden-Kontingent",
"profile.usage.window.days": "{{count}}-Tage-Kontingent",
"profile.usage.window.weeks": "{{count}}-Wochen-Kontingent",
"profile.usage.window.months": "{{count}}-Monats-Kontingent",
"profile.usage.window.shared": "Geteilt",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "Wird am {{date}} zurückgesetzt",
"profile.usage.status.unknown": "Unbekannt",
"profile.usage.status.unlimited": "Unbegrenzt",
@@ -748,6 +759,9 @@ export const dict = {
"profile.action.topUp": "Aufladen",
"profile.pass.subscribe": "Hol dir Kilo Pass, um Guthaben hinzuzufügen und Boni zu verdienen",
"profile.pass.bonus": "Bonus",
"profile.pass.usage": "Verbrauch in diesem Monat",
"profile.pass.paid": "Bezahlt",
"profile.pass.meter": "Monatlicher Verbrauch des Kilo Pass",
"profile.pass.renews": "Verlängert sich",
"profile.action.logout": "Abmelden",
@@ -628,6 +628,7 @@ export const dict = {
"profile.usage.plan.canceling": "Plan: Cancels at period end",
"profile.usage.plan.unknown": "Plan: Status unknown",
"profile.usage.action.manage": "Manage",
"profile.usage.action.managePlan": "Manage {{plan}}",
"profile.usage.routing": "Plan billing is active. Kilo Gateway routing is {{state}}.",
"profile.usage.routingState.disabled": "disabled",
"profile.usage.routingState.missing": "missing",
@@ -637,6 +638,16 @@ export const dict = {
"profile.usage.window.remaining": "{{value}} remaining",
"profile.usage.window.remainingOf": "{{value}} of {{limit}} remaining",
"profile.usage.window.usedOf": "{{value}} of {{limit}} used",
"profile.usage.window.quota": "Quota",
"profile.usage.window.daily": "Daily quota",
"profile.usage.window.weekly": "Weekly quota",
"profile.usage.window.monthly": "Monthly quota",
"profile.usage.window.hours": "{{count}}-hour quota",
"profile.usage.window.days": "{{count}}-day quota",
"profile.usage.window.weeks": "{{count}}-week quota",
"profile.usage.window.months": "{{count}}-month quota",
"profile.usage.window.shared": "Shared",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "Resets {{date}}",
"profile.usage.status.unknown": "Unknown",
"profile.usage.status.unlimited": "Unlimited",
@@ -646,6 +657,9 @@ export const dict = {
"profile.action.topUp": "Top up",
"profile.pass.subscribe": "Get Kilo Pass to add credits and earn bonuses",
"profile.pass.bonus": "Bonus",
"profile.pass.usage": "This month's usage",
"profile.pass.paid": "Paid",
"profile.pass.meter": "Kilo Pass monthly usage",
"profile.pass.renews": "Renews",
"profile.action.logout": "Log Out",
+14
View File
@@ -724,6 +724,7 @@ export const dict = {
"profile.usage.plan.canceling": "Plan: Se cancela al final del período",
"profile.usage.plan.unknown": "Plan: Estado desconocido",
"profile.usage.action.manage": "Gestionar",
"profile.usage.action.managePlan": "Gestionar {{plan}}",
"profile.usage.routing": "La facturación del plan está activa. El enrutamiento de Kilo Gateway está {{state}}.",
"profile.usage.routingState.disabled": "deshabilitado",
"profile.usage.routingState.missing": "ausente",
@@ -733,6 +734,16 @@ export const dict = {
"profile.usage.window.remaining": "{{value}} restante",
"profile.usage.window.remainingOf": "{{value}} de {{limit}} restantes",
"profile.usage.window.usedOf": "{{value}} de {{limit}} usados",
"profile.usage.window.quota": "Cuota",
"profile.usage.window.daily": "Cuota diaria",
"profile.usage.window.weekly": "Cuota semanal",
"profile.usage.window.monthly": "Cuota mensual",
"profile.usage.window.hours": "Cuota de {{count}} horas",
"profile.usage.window.days": "Cuota de {{count}} días",
"profile.usage.window.weeks": "Cuota de {{count}} semanas",
"profile.usage.window.months": "Cuota de {{count}} meses",
"profile.usage.window.shared": "Compartida",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "Se restablece el {{date}}",
"profile.usage.status.unknown": "Desconocido",
"profile.usage.status.unlimited": "Ilimitado",
@@ -742,6 +753,9 @@ export const dict = {
"profile.action.topUp": "Recargar",
"profile.pass.subscribe": "Obtén Kilo Pass para añadir créditos y ganar bonificaciones",
"profile.pass.bonus": "Bonificación",
"profile.pass.usage": "Uso de este mes",
"profile.pass.paid": "Pagado",
"profile.pass.meter": "Uso mensual de Kilo Pass",
"profile.pass.renews": "Se renueva",
"profile.action.logout": "Cerrar sesión",
+14
View File
@@ -631,6 +631,7 @@ export const dict = {
"profile.usage.plan.canceling": "طرح: در پایان دوره لغو می‌شود",
"profile.usage.plan.unknown": "طرح: وضعیت نامشخص",
"profile.usage.action.manage": "مدیریت",
"profile.usage.action.managePlan": "مدیریت {{plan}}",
"profile.usage.routing": "صورتحساب طرح فعال است. مسیریابی Kilo Gateway {{state}} است.",
"profile.usage.routingState.disabled": "غیرفعال",
"profile.usage.routingState.missing": "ناموجود",
@@ -640,6 +641,16 @@ export const dict = {
"profile.usage.window.remaining": "{{value}} باقی‌مانده",
"profile.usage.window.remainingOf": "{{value}} از {{limit}} باقی‌مانده",
"profile.usage.window.usedOf": "{{value}} از {{limit}} استفاده‌شده",
"profile.usage.window.quota": "سهمیه",
"profile.usage.window.daily": "سهمیه روزانه",
"profile.usage.window.weekly": "سهمیه هفتگی",
"profile.usage.window.monthly": "سهمیه ماهانه",
"profile.usage.window.hours": "سهمیه {{count}} ساعته",
"profile.usage.window.days": "سهمیه {{count}} روزه",
"profile.usage.window.weeks": "سهمیه {{count}} هفته‌ای",
"profile.usage.window.months": "سهمیه {{count}} ماهه",
"profile.usage.window.shared": "مشترک",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "بازنشانی در {{date}}",
"profile.usage.status.unknown": "نامشخص",
"profile.usage.status.unlimited": "نامحدود",
@@ -649,6 +660,9 @@ export const dict = {
"profile.action.topUp": "شارژ کردن",
"profile.pass.subscribe": "Kilo Pass را دریافت کنید تا اعتبار اضافه کنید و پاداش کسب کنید",
"profile.pass.bonus": "پاداش",
"profile.pass.usage": "مصرف این ماه",
"profile.pass.paid": "پرداخت‌شده",
"profile.pass.meter": "مصرف ماهانه Kilo Pass",
"profile.pass.renews": "تمدید می‌شود",
"profile.action.logout": "خروج",
+14
View File
@@ -730,6 +730,7 @@ export const dict = {
"profile.usage.plan.canceling": "Forfait : résiliation à la fin de la période",
"profile.usage.plan.unknown": "Forfait : statut inconnu",
"profile.usage.action.manage": "Gérer",
"profile.usage.action.managePlan": "Gérer {{plan}}",
"profile.usage.routing": "La facturation du forfait est active. Le routage via Kilo Gateway est {{state}}.",
"profile.usage.routingState.disabled": "désactivé",
"profile.usage.routingState.missing": "manquant",
@@ -739,6 +740,16 @@ export const dict = {
"profile.usage.window.remaining": "{{value}} restant",
"profile.usage.window.remainingOf": "{{value}} sur {{limit}} restants",
"profile.usage.window.usedOf": "{{value}} sur {{limit}} utilisés",
"profile.usage.window.quota": "Quota",
"profile.usage.window.daily": "Quota quotidien",
"profile.usage.window.weekly": "Quota hebdomadaire",
"profile.usage.window.monthly": "Quota mensuel",
"profile.usage.window.hours": "Quota de {{count}} heures",
"profile.usage.window.days": "Quota de {{count}} jours",
"profile.usage.window.weeks": "Quota de {{count}} semaines",
"profile.usage.window.months": "Quota de {{count}} mois",
"profile.usage.window.shared": "Partagé",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "Réinitialisation le {{date}}",
"profile.usage.status.unknown": "Inconnu",
"profile.usage.status.unlimited": "Illimité",
@@ -748,6 +759,9 @@ export const dict = {
"profile.action.topUp": "Recharger",
"profile.pass.subscribe": "Obtenez Kilo Pass pour ajouter des crédits et gagner des bonus",
"profile.pass.bonus": "Bonus",
"profile.pass.usage": "Consommation de ce mois-ci",
"profile.pass.paid": "Payé",
"profile.pass.meter": "Consommation mensuelle du Kilo Pass",
"profile.pass.renews": "Renouvelle",
"profile.action.logout": "Déconnexion",
+14
View File
@@ -534,6 +534,7 @@ export const dict = {
"profile.usage.plan.canceling": "Piano: Si annulla al termine del periodo",
"profile.usage.plan.unknown": "Piano: Stato sconosciuto",
"profile.usage.action.manage": "Gestisci",
"profile.usage.action.managePlan": "Gestisci {{plan}}",
"profile.usage.routing": "La fatturazione del piano è attiva. L'instradamento tramite Kilo Gateway è {{state}}.",
"profile.usage.routingState.disabled": "disabilitato",
"profile.usage.routingState.missing": "mancante",
@@ -543,6 +544,16 @@ export const dict = {
"profile.usage.window.remaining": "{{value}} rimanente",
"profile.usage.window.remainingOf": "{{value}} di {{limit}} rimanenti",
"profile.usage.window.usedOf": "{{value}} di {{limit}} utilizzati",
"profile.usage.window.quota": "Quota",
"profile.usage.window.daily": "Quota giornaliera",
"profile.usage.window.weekly": "Quota settimanale",
"profile.usage.window.monthly": "Quota mensile",
"profile.usage.window.hours": "Quota di {{count}} ore",
"profile.usage.window.days": "Quota di {{count}} giorni",
"profile.usage.window.weeks": "Quota di {{count}} settimane",
"profile.usage.window.months": "Quota di {{count}} mesi",
"profile.usage.window.shared": "Condivisa",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "Si azzera il {{date}}",
"profile.usage.status.unknown": "Sconosciuto",
"profile.usage.status.unlimited": "Illimitato",
@@ -552,6 +563,9 @@ export const dict = {
"profile.action.topUp": "Ricarica",
"profile.pass.subscribe": "Ottieni Kilo Pass per aggiungere crediti e guadagnare bonus",
"profile.pass.bonus": "Bonus",
"profile.pass.usage": "Utilizzo di questo mese",
"profile.pass.paid": "Pagato",
"profile.pass.meter": "Utilizzo mensile di Kilo Pass",
"profile.pass.renews": "Si rinnova",
"profile.action.logout": "Esci",
"settings.agentBehaviour.title": "Comportamento agente",
+14
View File
@@ -711,6 +711,7 @@ export const dict = {
"profile.usage.plan.canceling": "プラン:期間終了時に解約",
"profile.usage.plan.unknown": "プラン:ステータス不明",
"profile.usage.action.manage": "管理",
"profile.usage.action.managePlan": "{{plan}} を管理",
"profile.usage.routing": "プランの請求は有効です。Kilo Gatewayのルーティングは{{state}}です。",
"profile.usage.routingState.disabled": "無効",
"profile.usage.routingState.missing": "欠落",
@@ -720,6 +721,16 @@ export const dict = {
"profile.usage.window.remaining": "残り {{value}}",
"profile.usage.window.remainingOf": "{{limit}} のうち残り {{value}}",
"profile.usage.window.usedOf": "{{limit}} のうち {{value}} 使用済み",
"profile.usage.window.quota": "クォータ",
"profile.usage.window.daily": "日次クォータ",
"profile.usage.window.weekly": "週次クォータ",
"profile.usage.window.monthly": "月次クォータ",
"profile.usage.window.hours": "{{count}}時間クォータ",
"profile.usage.window.days": "{{count}}日クォータ",
"profile.usage.window.weeks": "{{count}}週間クォータ",
"profile.usage.window.months": "{{count}}か月クォータ",
"profile.usage.window.shared": "共有",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "{{date}}にリセット",
"profile.usage.status.unknown": "不明",
"profile.usage.status.unlimited": "無制限",
@@ -729,6 +740,9 @@ export const dict = {
"profile.action.topUp": "チャージ",
"profile.pass.subscribe": "Kilo Passに登録してクレジットを追加し、ボーナスを獲得",
"profile.pass.bonus": "ボーナス",
"profile.pass.usage": "今月の使用量",
"profile.pass.paid": "有料分",
"profile.pass.meter": "Kilo Pass の月間使用量",
"profile.pass.renews": "更新",
"profile.action.logout": "ログアウト",
+14
View File
@@ -671,6 +671,7 @@ export const dict = {
"profile.usage.plan.canceling": "요금제: 기간 종료 시 취소",
"profile.usage.plan.unknown": "요금제: 상태 알 수 없음",
"profile.usage.action.manage": "관리",
"profile.usage.action.managePlan": "{{plan}} 관리",
"profile.usage.routing": "요금제 결제가 활성화되어 있습니다. Kilo Gateway 라우팅은 {{state}}입니다.",
"profile.usage.routingState.disabled": "비활성화 상태",
"profile.usage.routingState.missing": "누락된 상태",
@@ -680,6 +681,16 @@ export const dict = {
"profile.usage.window.remaining": "{{value}} 남음",
"profile.usage.window.remainingOf": "{{limit}} 중 {{value}} 남음",
"profile.usage.window.usedOf": "{{limit}} 중 {{value}} 사용됨",
"profile.usage.window.quota": "할당량",
"profile.usage.window.daily": "일일 할당량",
"profile.usage.window.weekly": "주간 할당량",
"profile.usage.window.monthly": "월간 할당량",
"profile.usage.window.hours": "{{count}}시간 할당량",
"profile.usage.window.days": "{{count}}일 할당량",
"profile.usage.window.weeks": "{{count}}주 할당량",
"profile.usage.window.months": "{{count}}개월 할당량",
"profile.usage.window.shared": "공유",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "{{date}}에 초기화",
"profile.usage.status.unknown": "알 수 없음",
"profile.usage.status.unlimited": "무제한",
@@ -689,6 +700,9 @@ export const dict = {
"profile.action.topUp": "충전",
"profile.pass.subscribe": "Kilo Pass를 구독하여 크레딧을 추가하고 보너스를 받으세요",
"profile.pass.bonus": "보너스",
"profile.pass.usage": "이번 달 사용량",
"profile.pass.paid": "유료",
"profile.pass.meter": "Kilo Pass 월간 사용량",
"profile.pass.renews": "갱신",
"profile.action.logout": "로그아웃",
+14
View File
@@ -672,6 +672,7 @@ export const dict = {
"profile.usage.plan.canceling": "Abonnement: Wordt aan het einde van de periode opgezegd",
"profile.usage.plan.unknown": "Abonnement: Status onbekend",
"profile.usage.action.manage": "Beheren",
"profile.usage.action.managePlan": "{{plan}} beheren",
"profile.usage.routing": "De abonnementsfacturering is actief. Kilo Gateway-routering is {{state}}.",
"profile.usage.routingState.disabled": "uitgeschakeld",
"profile.usage.routingState.missing": "afwezig",
@@ -681,6 +682,16 @@ export const dict = {
"profile.usage.window.remaining": "{{value}} resterend",
"profile.usage.window.remainingOf": "{{value}} van {{limit}} resterend",
"profile.usage.window.usedOf": "{{value}} van {{limit}} gebruikt",
"profile.usage.window.quota": "Quotum",
"profile.usage.window.daily": "Dagelijks quotum",
"profile.usage.window.weekly": "Wekelijks quotum",
"profile.usage.window.monthly": "Maandelijks quotum",
"profile.usage.window.hours": "Quotum per {{count}} uur",
"profile.usage.window.days": "Quotum per {{count}} dagen",
"profile.usage.window.weeks": "Quotum per {{count}} weken",
"profile.usage.window.months": "Quotum per {{count}} maanden",
"profile.usage.window.shared": "Gedeeld",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "Wordt op {{date}} gereset",
"profile.usage.status.unknown": "Onbekend",
"profile.usage.status.unlimited": "Onbeperkt",
@@ -690,6 +701,9 @@ export const dict = {
"profile.action.topUp": "Opwaarderen",
"profile.pass.subscribe": "Schaf Kilo Pass aan om tegoed toe te voegen en bonussen te verdienen",
"profile.pass.bonus": "Bonus",
"profile.pass.usage": "Verbruik deze maand",
"profile.pass.paid": "Betaald",
"profile.pass.meter": "Maandelijks Kilo Pass-verbruik",
"profile.pass.renews": "Vernieuwt",
"profile.action.logout": "Uitloggen",
+14
View File
@@ -679,6 +679,7 @@ export const dict = {
"profile.usage.plan.canceling": "Abonnement: Avsluttes ved periodens slutt",
"profile.usage.plan.unknown": "Abonnement: Status ukjent",
"profile.usage.action.manage": "Administrer",
"profile.usage.action.managePlan": "Administrer {{plan}}",
"profile.usage.routing": "Abonnementsfakturering er aktiv. Kilo Gateway-ruting er {{state}}.",
"profile.usage.routingState.disabled": "deaktivert",
"profile.usage.routingState.missing": "fraværende",
@@ -688,6 +689,16 @@ export const dict = {
"profile.usage.window.remaining": "{{value}} gjenstår",
"profile.usage.window.remainingOf": "{{value}} av {{limit}} gjenstår",
"profile.usage.window.usedOf": "{{value}} av {{limit}} brukt",
"profile.usage.window.quota": "Kvote",
"profile.usage.window.daily": "Daglig kvote",
"profile.usage.window.weekly": "Ukentlig kvote",
"profile.usage.window.monthly": "Månedlig kvote",
"profile.usage.window.hours": "{{count}}-timers kvote",
"profile.usage.window.days": "{{count}}-dagers kvote",
"profile.usage.window.weeks": "{{count}}-ukers kvote",
"profile.usage.window.months": "{{count}}-måneders kvote",
"profile.usage.window.shared": "Delt",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "Tilbakestilles {{date}}",
"profile.usage.status.unknown": "Ukjent",
"profile.usage.status.unlimited": "Ubegrenset",
@@ -697,6 +708,9 @@ export const dict = {
"profile.action.topUp": "Fyll på",
"profile.pass.subscribe": "Få Kilo Pass for å legge til kreditt og tjene bonuser",
"profile.pass.bonus": "Bonus",
"profile.pass.usage": "Denne månedens forbruk",
"profile.pass.paid": "Betalt",
"profile.pass.meter": "Månedlig Kilo Pass-forbruk",
"profile.pass.renews": "Fornyes",
"profile.action.logout": "Logg ut",
+14
View File
@@ -675,6 +675,7 @@ export const dict = {
"profile.usage.plan.canceling": "Plan: Zostanie anulowany z końcem okresu",
"profile.usage.plan.unknown": "Plan: Status nieznany",
"profile.usage.action.manage": "Zarządzaj",
"profile.usage.action.managePlan": "Zarządzaj {{plan}}",
"profile.usage.routing": "Rozliczanie planu jest aktywne. Routing przez Kilo Gateway jest {{state}}.",
"profile.usage.routingState.disabled": "wyłączony",
"profile.usage.routingState.missing": "brakujący",
@@ -684,6 +685,16 @@ export const dict = {
"profile.usage.window.remaining": "Pozostało {{value}}",
"profile.usage.window.remainingOf": "Pozostało {{value}} z {{limit}}",
"profile.usage.window.usedOf": "Wykorzystano {{value}} z {{limit}}",
"profile.usage.window.quota": "Limit",
"profile.usage.window.daily": "Limit dzienny",
"profile.usage.window.weekly": "Limit tygodniowy",
"profile.usage.window.monthly": "Limit miesięczny",
"profile.usage.window.hours": "Limit {{count}}-godzinny",
"profile.usage.window.days": "Limit {{count}}-dniowy",
"profile.usage.window.weeks": "Limit {{count}}-tygodniowy",
"profile.usage.window.months": "Limit {{count}}-miesięczny",
"profile.usage.window.shared": "Wspólny",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "Resetuje się {{date}}",
"profile.usage.status.unknown": "Nieznany",
"profile.usage.status.unlimited": "Bez limitu",
@@ -693,6 +704,9 @@ export const dict = {
"profile.action.topUp": "Doładuj",
"profile.pass.subscribe": "Zdobądź Kilo Pass, aby dodać środki i zdobywać bonusy",
"profile.pass.bonus": "Bonus",
"profile.pass.usage": "Zużycie w tym miesiącu",
"profile.pass.paid": "Opłacone",
"profile.pass.meter": "Miesięczne zużycie Kilo Pass",
"profile.pass.renews": "Odnawia się",
"profile.action.logout": "Wyloguj się",
+14
View File
@@ -716,6 +716,7 @@ export const dict = {
"profile.usage.plan.canceling": "Тариф: Отмена в конце периода",
"profile.usage.plan.unknown": "Тариф: Статус неизвестен",
"profile.usage.action.manage": "Управлять",
"profile.usage.action.managePlan": "Управление {{plan}}",
"profile.usage.routing": "Оплата тарифа активна. Маршрутизация через Kilo Gateway {{state}}.",
"profile.usage.routingState.disabled": "отключена",
"profile.usage.routingState.missing": "отсутствует",
@@ -725,6 +726,16 @@ export const dict = {
"profile.usage.window.remaining": "Осталось {{value}}",
"profile.usage.window.remainingOf": "Осталось {{value}} из {{limit}}",
"profile.usage.window.usedOf": "Использовано {{value}} из {{limit}}",
"profile.usage.window.quota": "Квота",
"profile.usage.window.daily": "Дневная квота",
"profile.usage.window.weekly": "Недельная квота",
"profile.usage.window.monthly": "Месячная квота",
"profile.usage.window.hours": "{{count}}-часовая квота",
"profile.usage.window.days": "{{count}}-дневная квота",
"profile.usage.window.weeks": "{{count}}-недельная квота",
"profile.usage.window.months": "{{count}}-месячная квота",
"profile.usage.window.shared": "Общая",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "Сбрасывается {{date}}",
"profile.usage.status.unknown": "Неизвестно",
"profile.usage.status.unlimited": "Без ограничений",
@@ -734,6 +745,9 @@ export const dict = {
"profile.action.topUp": "Пополнить",
"profile.pass.subscribe": "Оформите Kilo Pass, чтобы добавить кредиты и получать бонусы",
"profile.pass.bonus": "Бонус",
"profile.pass.usage": "Использование за этот месяц",
"profile.pass.paid": "Оплачено",
"profile.pass.meter": "Ежемесячное использование Kilo Pass",
"profile.pass.renews": "Продлевается",
"profile.action.logout": "Выйти",
+14
View File
@@ -709,6 +709,7 @@ export const dict = {
"profile.usage.plan.canceling": "แผน: ยกเลิกเมื่อสิ้นสุดรอบ",
"profile.usage.plan.unknown": "แผน: ไม่ทราบสถานะ",
"profile.usage.action.manage": "จัดการ",
"profile.usage.action.managePlan": "จัดการ {{plan}}",
"profile.usage.routing": "การเรียกเก็บเงินตามแผนเปิดใช้งานอยู่ การกำหนดเส้นทาง Kilo Gateway {{state}}",
"profile.usage.routingState.disabled": "ปิดใช้งาน",
"profile.usage.routingState.missing": "ขาดหาย",
@@ -718,6 +719,16 @@ export const dict = {
"profile.usage.window.remaining": "เหลือ {{value}}",
"profile.usage.window.remainingOf": "เหลือ {{value}} จาก {{limit}}",
"profile.usage.window.usedOf": "ใช้ไป {{value}} จาก {{limit}}",
"profile.usage.window.quota": "โควตา",
"profile.usage.window.daily": "โควตารายวัน",
"profile.usage.window.weekly": "โควตารายสัปดาห์",
"profile.usage.window.monthly": "โควตารายเดือน",
"profile.usage.window.hours": "โควตา {{count}} ชั่วโมง",
"profile.usage.window.days": "โควตา {{count}} วัน",
"profile.usage.window.weeks": "โควตา {{count}} สัปดาห์",
"profile.usage.window.months": "โควตา {{count}} เดือน",
"profile.usage.window.shared": "ใช้ร่วมกัน",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "รีเซ็ตในวันที่ {{date}}",
"profile.usage.status.unknown": "ไม่ทราบ",
"profile.usage.status.unlimited": "ไม่จำกัด",
@@ -727,6 +738,9 @@ export const dict = {
"profile.action.topUp": "เติมเงิน",
"profile.pass.subscribe": "รับ Kilo Pass เพื่อเพิ่มเครดิตและรับโบนัส",
"profile.pass.bonus": "โบนัส",
"profile.pass.usage": "การใช้งานเดือนนี้",
"profile.pass.paid": "ชำระแล้ว",
"profile.pass.meter": "การใช้งาน Kilo Pass รายเดือน",
"profile.pass.renews": "ต่ออายุ",
"profile.action.logout": "ออกจากระบบ",
+14
View File
@@ -667,6 +667,7 @@ export const dict = {
"profile.usage.plan.canceling": "Plan: Dönem sonunda iptal edilecek",
"profile.usage.plan.unknown": "Plan: Durum bilinmiyor",
"profile.usage.action.manage": "Yönet",
"profile.usage.action.managePlan": "{{plan}} planını yönet",
"profile.usage.routing": "Plan faturalandırması etkin. Kilo Gateway yönlendirmesi {{state}}.",
"profile.usage.routingState.disabled": "devre dışı",
"profile.usage.routingState.missing": "eksik",
@@ -676,6 +677,16 @@ export const dict = {
"profile.usage.window.remaining": "{{value}} kaldı",
"profile.usage.window.remainingOf": "{{limit}} içinden {{value}} kaldı",
"profile.usage.window.usedOf": "{{limit}} içinden {{value}} kullanıldı",
"profile.usage.window.quota": "Kota",
"profile.usage.window.daily": "Günlük kota",
"profile.usage.window.weekly": "Haftalık kota",
"profile.usage.window.monthly": "Aylık kota",
"profile.usage.window.hours": "{{count}} saatlik kota",
"profile.usage.window.days": "{{count}} günlük kota",
"profile.usage.window.weeks": "{{count}} haftalık kota",
"profile.usage.window.months": "{{count}} aylık kota",
"profile.usage.window.shared": "Paylaşımlı",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "{{date}} tarihinde sıfırlanır",
"profile.usage.status.unknown": "Bilinmiyor",
"profile.usage.status.unlimited": "Sınırsız",
@@ -685,6 +696,9 @@ export const dict = {
"profile.action.topUp": "Bakiye yükle",
"profile.pass.subscribe": "Kredi eklemek ve bonus kazanmak için Kilo Pass edinin",
"profile.pass.bonus": "Bonus",
"profile.pass.usage": "Bu ayki kullanım",
"profile.pass.paid": "Ücretli",
"profile.pass.meter": "Aylık Kilo Pass kullanımı",
"profile.pass.renews": "Yenilenir",
"profile.action.logout": "Çıkış Yap",
+14
View File
@@ -669,6 +669,7 @@ export const dict = {
"profile.usage.plan.canceling": "План: Скасування наприкінці періоду",
"profile.usage.plan.unknown": "План: Статус невідомий",
"profile.usage.action.manage": "Керувати",
"profile.usage.action.managePlan": "Керування {{plan}}",
"profile.usage.routing": "Оплата плану активна. Маршрутизація через Kilo Gateway {{state}}.",
"profile.usage.routingState.disabled": "вимкнена",
"profile.usage.routingState.missing": "відсутня",
@@ -678,6 +679,16 @@ export const dict = {
"profile.usage.window.remaining": "Залишилося {{value}}",
"profile.usage.window.remainingOf": "Залишилося {{value}} з {{limit}}",
"profile.usage.window.usedOf": "Використано {{value}} з {{limit}}",
"profile.usage.window.quota": "Квота",
"profile.usage.window.daily": "Денна квота",
"profile.usage.window.weekly": "Тижнева квота",
"profile.usage.window.monthly": "Місячна квота",
"profile.usage.window.hours": "{{count}}-годинна квота",
"profile.usage.window.days": "{{count}}-денна квота",
"profile.usage.window.weeks": "{{count}}-тижнева квота",
"profile.usage.window.months": "{{count}}-місячна квота",
"profile.usage.window.shared": "Спільна",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "Скидається {{date}}",
"profile.usage.status.unknown": "Невідомо",
"profile.usage.status.unlimited": "Без обмежень",
@@ -687,6 +698,9 @@ export const dict = {
"profile.action.topUp": "Поповнити",
"profile.pass.subscribe": "Отримайте Kilo Pass, щоб додати кредити та заробляти бонуси",
"profile.pass.bonus": "Бонус",
"profile.pass.usage": "Використання за цей місяць",
"profile.pass.paid": "Оплачено",
"profile.pass.meter": "Щомісячне використання Kilo Pass",
"profile.pass.renews": "Поновлюється",
"profile.action.logout": "Вийти",
+14
View File
@@ -692,6 +692,7 @@ export const dict = {
"profile.usage.plan.canceling": "套餐:将在周期结束时取消",
"profile.usage.plan.unknown": "套餐:状态未知",
"profile.usage.action.manage": "管理",
"profile.usage.action.managePlan": "管理 {{plan}}",
"profile.usage.routing": "套餐账单处于有效状态。Kilo Gateway 路由状态为 {{state}}。",
"profile.usage.routingState.disabled": "已禁用",
"profile.usage.routingState.missing": "缺失",
@@ -701,6 +702,16 @@ export const dict = {
"profile.usage.window.remaining": "剩余 {{value}}",
"profile.usage.window.remainingOf": "共 {{limit}},剩余 {{value}}",
"profile.usage.window.usedOf": "共 {{limit}},已使用 {{value}}",
"profile.usage.window.quota": "配额",
"profile.usage.window.daily": "每日配额",
"profile.usage.window.weekly": "每周配额",
"profile.usage.window.monthly": "每月配额",
"profile.usage.window.hours": "{{count}}小时配额",
"profile.usage.window.days": "{{count}}天配额",
"profile.usage.window.weeks": "{{count}}周配额",
"profile.usage.window.months": "{{count}}个月配额",
"profile.usage.window.shared": "共享",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "{{date}} 重置",
"profile.usage.status.unknown": "未知",
"profile.usage.status.unlimited": "无限制",
@@ -710,6 +721,9 @@ export const dict = {
"profile.action.topUp": "充值",
"profile.pass.subscribe": "订阅 Kilo Pass 以添加额度并赚取奖励",
"profile.pass.bonus": "奖励",
"profile.pass.usage": "本月用量",
"profile.pass.paid": "付费",
"profile.pass.meter": "Kilo Pass 每月用量",
"profile.pass.renews": "续订",
"profile.action.logout": "退出登录",
+14
View File
@@ -652,6 +652,7 @@ export const dict = {
"profile.usage.plan.canceling": "方案:將於週期結束時取消",
"profile.usage.plan.unknown": "方案:狀態未知",
"profile.usage.action.manage": "管理",
"profile.usage.action.managePlan": "管理 {{plan}}",
"profile.usage.routing": "方案帳單目前有效。Kilo Gateway 路由狀態為 {{state}}。",
"profile.usage.routingState.disabled": "已停用",
"profile.usage.routingState.missing": "缺失",
@@ -661,6 +662,16 @@ export const dict = {
"profile.usage.window.remaining": "剩餘 {{value}}",
"profile.usage.window.remainingOf": "共 {{limit}},剩餘 {{value}}",
"profile.usage.window.usedOf": "共 {{limit}},已使用 {{value}}",
"profile.usage.window.quota": "配額",
"profile.usage.window.daily": "每日配額",
"profile.usage.window.weekly": "每週配額",
"profile.usage.window.monthly": "每月配額",
"profile.usage.window.hours": "{{count}}小時配額",
"profile.usage.window.days": "{{count}}天配額",
"profile.usage.window.weeks": "{{count}}週配額",
"profile.usage.window.months": "{{count}}個月配額",
"profile.usage.window.shared": "共享",
"profile.usage.window.scoped": "{{resource}} · {{period}}",
"profile.usage.reset": "{{date}} 重設",
"profile.usage.status.unknown": "未知",
"profile.usage.status.unlimited": "無限制",
@@ -670,6 +681,9 @@ export const dict = {
"profile.action.topUp": "儲值",
"profile.pass.subscribe": "訂閱 Kilo Pass 以新增額度並賺取獎勵",
"profile.pass.bonus": "獎勵",
"profile.pass.usage": "本月用量",
"profile.pass.paid": "付費",
"profile.pass.meter": "Kilo Pass 每月用量",
"profile.pass.renews": "續訂",
"profile.action.logout": "登出",
@@ -230,9 +230,7 @@ const chatServer = {
providerUsage: () => undefined,
providerUsageLoading: () => false,
providerUsageError: () => undefined,
requestProviderUsage: () => undefined,
refreshProviderUsage: () => undefined,
releaseProviderUsage: () => undefined,
deviceAuth: () => ({ status: "idle" as const }),
startLogin: () => undefined,
goToLogin: () => undefined,
@@ -64,8 +64,8 @@ const usage: ProviderUsageData = {
windows: [
{
id: "general-interval",
label: "Shared quota 5-hour",
resource: "general",
period: { unit: "hour", value: 5 },
unit: "percent",
orientation: "remaining_percent",
used: 24,
@@ -9,15 +9,6 @@ export interface ProviderUsageLoadedMessage {
reset?: boolean
}
export interface RequestProviderUsageMessage {
type: "requestProviderUsage"
}
export interface RefreshProviderUsageMessage {
type: "refreshProviderUsage"
}
/** Sent when the Profile view unmounts so the extension stops background usage refreshes. */
export interface ReleaseProviderUsageMessage {
type: "releaseProviderUsage"
}
@@ -7,11 +7,7 @@ import type { Config } from "./config"
import type { ModelAllocation, ReviewComment, TerminalDestination, TerminalPlacement } from "./agent-manager"
import type { ReviewMessageData } from "../../../../src/shared/review-comments"
import type { WorkStyle, WorkStyleState } from "../../../../src/shared/work-style-presets"
import type {
RefreshProviderUsageMessage,
ReleaseProviderUsageMessage,
RequestProviderUsageMessage,
} from "./provider-usage"
import type { RefreshProviderUsageMessage } from "./provider-usage"
import type { AnacondaDesktopWebviewMessage } from "../../../../src/shared/anaconda-desktop-messages"
import type {
ClearLegacyDataMessage,
@@ -1401,9 +1397,7 @@ export type WebviewMessage =
| LoginRequest
| LogoutRequest
| RefreshProfileRequest
| RequestProviderUsageMessage
| RefreshProviderUsageMessage
| ReleaseProviderUsageMessage
| OpenExternalRequest
| OpenSettingsPanelRequest
| OpenProfilePanelRequest
@@ -1,7 +1,7 @@
import { TextAttributes } from "@opentui/core"
import { useKeyboard } from "@opentui/solid"
import type { ProviderUsage, ProviderUsageSnapshot } from "@kilocode/sdk/v2"
import { formatWindow } from "@kilocode/kilo-gateway/provider-usage"
import { formatWindow, windowLabel } from "@kilocode/kilo-gateway/provider-usage"
import { useTheme } from "@tui/context/theme"
import { useSDK } from "@tui/context/sdk"
import { useDialog } from "@tui/ui/dialog"
@@ -26,7 +26,7 @@ function Item(props: { item: ProviderUsageSnapshot }) {
{(window) => (
<box>
<text fg={theme.text}>
{window.label}: {formatWindow(window)}
{windowLabel(window)}: {formatWindow(window)}
</text>
<Show when={window.resetAt}>
{(reset) => <text fg={theme.textMuted}>Resets {new Date(reset()).toLocaleString()}</text>}
@@ -10,7 +10,6 @@ import {
import { described } from "@/server/routes/instance/httpapi/groups/metadata"
import { ProviderUsage } from "@opencode-ai/schema/kilocode/provider-usage"
import { AnacondaDesktopApi } from "./anaconda-desktop"
import { ProviderUsageLocationMiddleware } from "../middleware/provider-usage-location"
import { Result as AgentRequirementResult } from "@/kilocode/agent-requirements"
import {
Failure as AgentManagerFailure,
@@ -145,28 +144,24 @@ export const KilocodeApi = HttpApi.make("kilocode")
query: WorkspaceRoutingQuery,
success: described(ProviderUsage.Info, "Current provider usage"),
error: HttpApiError.ServiceUnavailable,
})
.annotateMerge(
OpenApi.annotations({
identifier: "kilocode.providerUsage.get",
summary: "Get provider usage",
description: "Get cache-aware, secret-free provider plan usage and personal billing status.",
}),
)
.middleware(ProviderUsageLocationMiddleware),
}).annotateMerge(
OpenApi.annotations({
identifier: "kilocode.providerUsage.get",
summary: "Get provider usage",
description: "Get cache-aware, secret-free provider plan usage and personal billing status.",
}),
),
HttpApiEndpoint.post("providerUsageRefresh", KilocodePaths.providerUsageRefresh, {
query: WorkspaceRoutingQuery,
success: described(ProviderUsage.Info, "Refreshed provider usage"),
error: HttpApiError.ServiceUnavailable,
})
.annotateMerge(
OpenApi.annotations({
identifier: "kilocode.providerUsage.refresh",
summary: "Refresh provider usage",
description: "Refresh provider plan usage while coalescing concurrent source requests.",
}),
)
.middleware(ProviderUsageLocationMiddleware),
}).annotateMerge(
OpenApi.annotations({
identifier: "kilocode.providerUsage.refresh",
summary: "Refresh provider usage",
description: "Refresh provider plan usage while coalescing concurrent source requests.",
}),
),
HttpApiEndpoint.get("notebookList", KilocodePaths.notebookList, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(NotebookRequest), "Pending notebook host requests"),
@@ -6,6 +6,7 @@ import * as KiloSkill from "@/kilocode/skill-remove"
import { Agent } from "@/agent/agent"
import { Command } from "@/command"
import { Config } from "@/config/config"
import { WorkspaceRef } from "@/effect/instance-ref"
import { InstanceState } from "@/effect/instance-state"
import { HeapSnapshot } from "@/kilocode/cli/heap-snapshot"
import type { RequestID as AgentManagerRequestID } from "@/kilocode/agent-manager/protocol"
@@ -14,6 +15,9 @@ import type { RequestID as NotebookRequestID } from "@/kilocode/notebook/protoco
import { Notebook } from "@/kilocode/notebook/service"
import { ModelUsage } from "@/kilocode/session/model-usage"
import { ProviderUsage } from "@opencode-ai/core/kilocode/provider-usage"
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap } from "@opencode-ai/core/location-services"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { InstanceStore } from "@/project/instance-store"
import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api"
import { Skill } from "@/skill"
@@ -37,6 +41,22 @@ export const kilocodeHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilocode"
const store = yield* InstanceStore.Service
const manager = yield* AgentManager.Service
const notebook = yield* Notebook.Service
const locations = yield* LocationServiceMap.Service
// Location-scoped services, keyed by the request's directory and workspace.
const located = Effect.fnUntraced(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
return yield* effect.pipe(
Effect.provide(
locations.get(
Location.Ref.make({
directory: AbsolutePath.make((yield* InstanceState.context).directory),
workspaceID: yield* WorkspaceRef,
}),
),
),
)
})
const heapSnapshot = Effect.fn("KilocodeHttpApi.heapSnapshot")(function* () {
return yield* Effect.sync(() => HeapSnapshot.write())
})
@@ -108,15 +128,15 @@ export const kilocodeHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilocode"
})
const providerUsage = Effect.fn("KilocodeHttpApi.providerUsage")(function* () {
return yield* (yield* ProviderUsage.Service)
.get()
.pipe(Effect.mapError(() => new HttpApiError.ServiceUnavailable({})))
return yield* located(ProviderUsage.Service.use((usage) => usage.get())).pipe(
Effect.mapError(() => new HttpApiError.ServiceUnavailable({})),
)
})
const providerUsageRefresh = Effect.fn("KilocodeHttpApi.providerUsageRefresh")(function* () {
return yield* (yield* ProviderUsage.Service)
.refresh()
.pipe(Effect.mapError(() => new HttpApiError.ServiceUnavailable({})))
return yield* located(ProviderUsage.Service.use((usage) => usage.refresh())).pipe(
Effect.mapError(() => new HttpApiError.ServiceUnavailable({})),
)
})
const notebookList = Effect.fn("KilocodeHttpApi.notebookList")(function* () {
@@ -1,40 +0,0 @@
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap } from "@opencode-ai/core/location-services"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Effect, Layer } from "effect"
import { HttpApiMiddleware } from "effect/unstable/httpapi"
import { WorkspaceRouteContext } from "@/server/routes/instance/httpapi/middleware/workspace-routing"
export type ProviderUsageLocationServices = Layer.Success<ReturnType<(typeof LocationServiceMap.Service)["get"]>>
export class ProviderUsageLocationMiddleware extends HttpApiMiddleware.Service<
ProviderUsageLocationMiddleware,
{
provides: ProviderUsageLocationServices
requires: WorkspaceRouteContext
}
>()("@kilocode/HttpApiProviderUsageLocation") {}
export const layer = Layer.effect(
ProviderUsageLocationMiddleware,
Effect.gen(function* () {
const locations = yield* LocationServiceMap.Service
return ProviderUsageLocationMiddleware.of((effect) =>
Effect.gen(function* () {
const route = yield* WorkspaceRouteContext
const directory = (() => {
try {
return decodeURIComponent(route.directory)
} catch {
return route.directory
}
})()
const ref = Location.Ref.make({
directory: AbsolutePath.make(directory),
workspaceID: route.workspaceID,
})
return yield* effect.pipe(Effect.provide(locations.get(ref)))
}),
)
}),
)
@@ -29,7 +29,6 @@ import { sandboxHandlers } from "./handlers/sandbox"
import { sessionImportHandlers } from "./handlers/session-import"
import { suggestionHandlers } from "./handlers/suggestion"
import { telemetryHandlers } from "./handlers/telemetry"
import { layer as providerUsageLocationLayer } from "./middleware/provider-usage-location"
export const provide = Layer.provide([
agentBuilderHandlers,
@@ -43,7 +42,7 @@ export const provide = Layer.provide([
instanceReloadHandlers,
interactiveTerminalHandlers,
kiloGatewayHandlers,
kilocodeHandlers.pipe(Layer.provide(providerUsageLocationLayer)),
kilocodeHandlers,
memoryHandlers,
networkHandlers,
remoteHandlers,
@@ -10,16 +10,22 @@ export const UsageError = Schema.Struct({
retryable: Schema.Boolean,
}).annotate({ identifier: "ProviderUsageError" })
export interface UsagePeriod extends Schema.Schema.Type<typeof UsagePeriod> {}
export const UsagePeriod = Schema.Struct({
unit: Schema.Literals(["hour", "day", "week", "month"]),
value: Schema.Int,
}).annotate({ identifier: "ProviderUsagePeriod" })
export interface UsageWindow extends Schema.Schema.Type<typeof UsageWindow> {}
export const UsageWindow = Schema.Struct({
id: Schema.String,
label: Schema.String,
resource: Schema.String,
unit: Schema.String,
orientation: Schema.Literals(["used_percent", "remaining_percent", "amount", "count"]),
used: optional(Schema.Finite),
remaining: optional(Schema.Finite),
limit: optional(Schema.Finite),
period: optional(UsagePeriod),
durationMs: optional(Schema.Finite),
resetAt: optional(Schema.String),
state: Schema.Literals(["active", "exhausted", "unlimited", "not_in_plan", "unknown"]),
+6 -1
View File
@@ -4186,15 +4186,20 @@ export type CommandFile = {
hints: Array<string>
}
export type ProviderUsagePeriod = {
unit: "hour" | "day" | "week" | "month"
value: number
}
export type ProviderUsageWindow = {
id: string
label: string
resource: string
unit: string
orientation: "used_percent" | "remaining_percent" | "amount" | "count"
used?: number
remaining?: number
limit?: number
period?: ProviderUsagePeriod
durationMs?: number
resetAt?: string
state: "active" | "exhausted" | "unlimited" | "not_in_plan" | "unknown"
+18 -4
View File
@@ -38758,15 +38758,26 @@
"required": ["name", "builtin", "location", "editable", "hints"],
"additionalProperties": false
},
"ProviderUsagePeriod": {
"type": "object",
"properties": {
"unit": {
"type": "string",
"enum": ["hour", "day", "week", "month"]
},
"value": {
"type": "integer"
}
},
"required": ["unit", "value"],
"additionalProperties": false
},
"ProviderUsageWindow": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"label": {
"type": "string"
},
"resource": {
"type": "string"
},
@@ -38786,6 +38797,9 @@
"limit": {
"type": "number"
},
"period": {
"$ref": "#/components/schemas/ProviderUsagePeriod"
},
"durationMs": {
"type": "number"
},
@@ -38797,7 +38811,7 @@
"enum": ["active", "exhausted", "unlimited", "not_in_plan", "unknown"]
}
},
"required": ["id", "label", "resource", "unit", "orientation", "state"],
"required": ["id", "resource", "unit", "orientation", "state"],
"additionalProperties": false
},
"ProviderUsageError": {