Merge pull request #11611 from Kilo-Org/feat/provider-usage-center

feat: add provider usage center
This commit is contained in:
Kirill Kalishev
2026-08-21 14:29:41 -04:00
committed by GitHub
80 changed files with 5448 additions and 160 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"@kilocode/cli": minor
"@kilocode/kilo-ui": minor
"kilo-code": minor
---
View current provider plan usage and quota windows in the CLI and VS Code profile.
+1 -1
View File
@@ -357,7 +357,7 @@
},
"packages/kilo-jetbrains": {
"name": "@kilocode/kilo-jetbrains",
"version": "7.4.22",
"version": "7.4.23",
},
"packages/kilo-memory": {
"name": "@kilocode/kilo-memory",
@@ -0,0 +1,395 @@
export * as ProviderUsage from "./provider-usage"
import { Context, Effect, Layer, Schema } from "effect"
import { createHash } from "node:crypto"
import { ProviderUsage as Contract } from "@opencode-ai/schema/kilocode/provider-usage"
import { Catalog } from "../catalog"
import { makeGlobalNode, makeLocationNode } from "../effect/app-node"
import { Integration } from "../integration"
import { PluginV2 } from "../plugin"
import { ProviderV2 } from "../provider"
import * as Cloud from "./provider-usage/cloud"
import { bindings, direct, type Candidate } from "./provider-usage/minimax/usage"
const successTtl = 60_000
const errorTtl = 10_000
const readyPlugin = PluginV2.ID.make("config-provider")
interface AdapterContext {
candidates: readonly Candidate[]
failedCandidates: readonly Candidate["providerID"][]
cloud: (() => Promise<Cloud.CloudState>) | undefined
token: string | undefined
cloudIdentity: string | undefined
cloudReliable: boolean
fetch: typeof fetch
usage: typeof Cloud.fetchCodingPlanUsage
identityCurrent(identity: string): boolean
source(id: string, load: () => Promise<Contract.UsageSnapshot>, identity?: string): Promise<Contract.UsageSnapshot>
preserve(prefix: string, identity?: string): Contract.UsageSnapshot[]
prune(prefix: string, keep: string[]): void
}
interface AdapterResult {
items: ReadonlyArray<Contract.UsageSnapshot>
}
interface Adapter {
cachePrefixes: readonly string[]
cloudScoped?: boolean
run(ctx: AdapterContext): Promise<AdapterResult>
}
const managed: Adapter = {
cachePrefixes: ["kilo-managed:"],
cloudScoped: true,
async run(ctx) {
if (!ctx.cloud || !ctx.token || !ctx.cloudIdentity) {
return { items: ctx.cloudReliable ? [] : ctx.preserve("kilo-managed:") }
}
const state = await ctx.cloud()
if (!ctx.identityCurrent(ctx.cloudIdentity)) return { items: [] }
if (!state.plans.ok || !state.byok.ok) return { items: ctx.preserve("kilo-managed:", ctx.cloudIdentity) }
const token = ctx.token
const identity = ctx.cloudIdentity
const detected = Cloud.plans(state)
const ids = detected.map((subscription) => `kilo-managed:${subscription.id}`)
ctx.prune("kilo-managed:", ids)
return {
items: await Promise.all(
detected.map((subscription) =>
ctx.source(`kilo-managed:${subscription.id}`, () => Cloud.managed(token, subscription, ctx.usage), identity),
),
),
}
},
}
const minimax: Adapter = {
cachePrefixes: ["minimax-direct-"],
async run(ctx) {
const items = await direct(ctx.candidates, ctx.fetch, ctx.source)
// 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),
)
return { items: merged }
},
}
const registry: readonly Adapter[] = [managed, minimax]
export class ServiceError extends Schema.TaggedErrorClass<ServiceError>()("ProviderUsageServiceError", {
message: Schema.String,
}) {}
interface SourceCell {
identity?: string
value?: Contract.UsageSnapshot
expires: number
updatedAt?: string
inflight?: Promise<Contract.UsageSnapshot>
}
interface CloudCell {
value?: Cloud.CloudState
expires: number
updatedAt?: string
inflight?: Promise<Cloud.CloudState>
}
interface State {
sources: Map<string, SourceCell>
cloud: CloudCell
cloudIdentity?: string
}
function fingerprint(value: string) {
return createHash("sha256").update(value).digest("hex")
}
function scopeCloudCache(state: State, token: string | undefined) {
const identity = token ? fingerprint(token) : undefined
if (state.cloudIdentity === identity) return identity
state.cloudIdentity = identity
state.cloud = { expires: 0 }
prune(state, "kilo-managed:", [])
return identity
}
function stale(next: Contract.UsageSnapshot, previous: Contract.UsageSnapshot | undefined) {
if (next.fetchState !== "unavailable" && next.fetchState !== "error") return next
if (!previous || (previous.fetchState !== "ready" && previous.fetchState !== "stale")) return next
return {
...previous,
fetchState: "stale" as const,
planState: next.planState,
routingState: next.routingState,
managementUrl: next.managementUrl,
error: next.error,
}
}
function source(
state: State,
id: string,
force: boolean,
load: () => Promise<Contract.UsageSnapshot>,
identity?: string,
) {
const existing = state.sources.get(id)
const cell: SourceCell = existing && existing.identity === identity ? existing : { expires: 0, identity }
state.sources.set(id, cell)
if (!force && cell.value && cell.expires > Date.now()) return Promise.resolve(cell.value)
if (cell.inflight) return cell.inflight
const task = load()
.then((item) => {
const value = stale(item, cell.value)
if (state.sources.get(id) !== cell) return value
cell.value = value
cell.updatedAt = new Date().toISOString()
cell.expires = Date.now() + (value.fetchState === "ready" ? successTtl : errorTtl)
return value
})
.finally(() => {
cell.inflight = undefined
})
cell.inflight = task
return task
}
function preserve(state: State, prefix: string, identity?: string) {
const items: Contract.UsageSnapshot[] = []
for (const [id, cell] of state.sources) {
if (!id.startsWith(prefix) || (identity !== undefined && cell.identity !== identity) || !cell.value) continue
const loaded = cell.value.fetchState === "ready" || cell.value.fetchState === "stale"
const value = loaded
? {
...cell.value,
fetchState: "stale" as const,
error: {
code: "source_refresh_unavailable",
message: "The latest usage could not be loaded.",
retryable: true,
},
}
: cell.value
cell.value = value
cell.updatedAt = new Date().toISOString()
cell.expires = Date.now() + errorTtl
items.push(value)
}
return items
}
function prune(state: State, prefix: string, keep: string[]) {
const ids = new Set(keep)
for (const id of state.sources.keys()) {
if (!id.startsWith(prefix) || ids.has(id)) continue
state.sources.delete(id)
}
}
function cloud(state: State, token: string, identity: string, force: boolean, transport: TransportInterface) {
if (state.cloudIdentity !== identity) return Cloud.load(token, transport)
const cell = state.cloud
if (!force && cell.value && cell.expires > Date.now()) return Promise.resolve(cell.value)
if (cell.inflight) return cell.inflight
const task = Cloud.load(token, transport)
.then((value) => {
if (state.cloudIdentity !== identity) return value
const failed = Object.values(value).some((result) => !result.ok)
const previous = cell.value
if (failed && previous) {
cell.expires = Date.now() + errorTtl
return previous
}
cell.value = value
cell.updatedAt = new Date().toISOString()
cell.expires = Date.now() + (failed ? errorTtl : successTtl)
return value
})
.finally(() => {
cell.inflight = undefined
})
cell.inflight = task
return task
}
export interface Interface {
readonly get: () => Effect.Effect<Contract.Info, ServiceError>
readonly refresh: () => Effect.Effect<Contract.Info, ServiceError>
}
export class Service extends Context.Service<Service, Interface>()("@kilocode/ProviderUsage") {}
export interface TransportInterface {
readonly fetch: typeof fetch
readonly plans: typeof Cloud.fetchCodingPlanSubscriptions
readonly byok: typeof Cloud.fetchByokEntries
readonly usage: typeof Cloud.fetchCodingPlanUsage
}
export class Transport extends Context.Service<Transport, TransportInterface>()("@kilocode/ProviderUsageTransport") {}
const transportLayer = Layer.succeed(Transport, {
fetch,
plans: Cloud.fetchCodingPlanSubscriptions,
byok: Cloud.fetchByokEntries,
usage: Cloud.fetchCodingPlanUsage,
})
export const transportNode = makeGlobalNode({ service: Transport, layer: transportLayer, deps: [] })
const credential = Effect.fn("ProviderUsage.credential")(function* (
integrations: Integration.Interface,
id: Integration.ID,
) {
const connection = yield* integrations.connection.active(id)
if (!connection) return undefined
return yield* integrations.connection
.resolve(connection)
.pipe(Effect.mapError(() => new ServiceError({ message: `Unable to resolve provider credential: ${id}` })))
})
const resolve = Effect.fn("ProviderUsage.resolveCredential")(function* (
integrations: Integration.Interface,
id: Integration.ID,
) {
return yield* credential(integrations, id).pipe(
Effect.map((value) => ({ ok: true as const, value })),
Effect.catch(() => Effect.succeed({ ok: false as const })),
)
})
function configuredKey(provider: ProviderV2.Info) {
const header = Object.entries(provider.request.headers).find(([key]) => key.toLowerCase() === "x-api-key")?.[1]
const value = provider.request.body.apiKey ?? provider.api.settings?.apiKey ?? header
return typeof value === "string" ? value : undefined
}
function nonempty(value: unknown) {
if (typeof value !== "string") return undefined
const text = value.trim()
return text || undefined
}
const inputs = Effect.fn("ProviderUsage.inputs")(function* (
catalog: Catalog.Interface,
integrations: Integration.Interface,
) {
const providers = yield* catalog.provider.all()
const byID = new Map(providers.map((provider) => [provider.id, provider]))
const failedCandidates: Candidate["providerID"][] = []
const candidates = yield* Effect.forEach(Object.keys(bindings) as (keyof typeof bindings)[], (providerID) =>
Effect.gen(function* () {
const provider = byID.get(ProviderV2.ID.make(providerID))
if (!provider || provider.disabled) return undefined
const resolved = yield* resolve(integrations, provider.integrationID ?? Integration.ID.make(provider.id))
if (!resolved.ok) failedCandidates.push(providerID)
const value = resolved.ok
? resolved.value?.type === "key"
? resolved.value.key
: configuredKey(provider)
: undefined
if (typeof value !== "string" || !value.trim().startsWith("sk-cp")) return undefined
return { providerID, label: provider.name, key: value.trim() } satisfies Candidate
}),
)
const kilo = yield* resolve(integrations, Integration.ID.make("kilo"))
const kiloProvider = byID.get(ProviderV2.ID.kilo)
const configuredOrg = nonempty(process.env.KILO_ORG_ID) ?? nonempty(kiloProvider?.request.body.kilocodeOrganizationId)
const organization =
configuredOrg !== undefined ||
(kilo.ok && kilo.value?.type === "oauth" && nonempty(kilo.value.metadata?.accountID) !== undefined)
const cloudReliable = organization || kilo.ok
const token =
kilo.ok && kilo.value?.type === "oauth" && !organization && kilo.value.access ? kilo.value.access : undefined
return {
candidates: candidates.filter((item): item is Candidate => item !== undefined),
failedCandidates,
token,
cloudReliable,
}
})
function makeService(
catalog: Catalog.Interface,
integrations: Integration.Interface,
transport: TransportInterface,
ready: Effect.Effect<void>,
) {
const state: State = { sources: new Map(), cloud: { expires: 0 } }
const evaluate = Effect.fn("ProviderUsage.evaluate")(function* (force: boolean) {
yield* ready
const current = yield* inputs(catalog, integrations)
const cloudIdentity = current.cloudReliable ? scopeCloudCache(state, current.token) : state.cloudIdentity
const ctx: AdapterContext = {
candidates: current.candidates,
failedCandidates: current.failedCandidates,
cloud:
current.token && cloudIdentity
? () => cloud(state, current.token!, cloudIdentity, force, transport)
: undefined,
token: current.token,
cloudIdentity,
cloudReliable: current.cloudReliable,
fetch: transport.fetch,
usage: transport.usage,
identityCurrent: (identity) => state.cloudIdentity === identity,
source: (id, load, identity) => source(state, id, force, load, identity),
preserve: (prefix, identity) => preserve(state, prefix, identity),
prune: (prefix, keep) => prune(state, prefix, keep),
}
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) =>
ctx.preserve(prefix, adapter.cloudScoped ? ctx.cloudIdentity : undefined),
),
}),
),
),
),
)
const stamps = [state.cloud.updatedAt, ...state.sources.values().map((cell) => cell.updatedAt)].filter(
(value): value is string => value !== undefined,
)
return {
items: results.flatMap((result) => result.items),
generatedAt: stamps.toSorted().at(-1) ?? new Date().toISOString(),
} satisfies Contract.Info
})
return Service.of({
get: () => evaluate(false),
refresh: () => evaluate(true),
})
}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const plugins = yield* PluginV2.Service
return makeService(yield* Catalog.Service, yield* Integration.Service, yield* Transport, plugins.wait(readyPlugin))
}),
)
export const node = makeLocationNode({
service: Service,
layer,
deps: [Catalog.node, Integration.node, PluginV2.node, transportNode],
})
export { Contract as Schema }
@@ -0,0 +1,138 @@
import {
fetchByokEntries,
fetchCodingPlanSubscriptions,
fetchCodingPlanUsage,
type ByokEntry,
type CodingPlanQuotaWindow,
type CodingPlanSubscription,
} from "@kilocode/kilo-gateway"
import type { ProviderUsage } from "@opencode-ai/schema/kilocode/provider-usage"
export { fetchByokEntries, fetchCodingPlanSubscriptions, fetchCodingPlanUsage }
export interface CloudState {
plans: Result<CodingPlanSubscription[]>
byok: Result<ByokEntry[]>
}
type Result<T> = { ok: true; value: T } | { ok: false }
const safe = async <T>(promise: Promise<T>): Promise<Result<T>> =>
promise.then(
(value) => ({ ok: true, value }),
() => ({ ok: false }),
)
export async function load(
token: string,
transport: {
plans: typeof fetchCodingPlanSubscriptions
byok: typeof fetchByokEntries
} = { plans: fetchCodingPlanSubscriptions, byok: fetchByokEntries },
): Promise<CloudState> {
const [plans, byok] = await Promise.all([safe(transport.plans(token)), safe(transport.byok(token))])
return { plans, byok }
}
function base() {
if (!process.env.KILO_API_URL) return "https://app.kilo.ai"
try {
return new URL(process.env.KILO_API_URL).origin
} catch {
return "https://app.kilo.ai"
}
}
const error = (code: string, message: string) => ({ code, message, retryable: true })
function installed(subscription: CodingPlanSubscription, state: Result<ByokEntry[]>) {
if (!state.ok || !subscription.canQueryUsage || !subscription.hasInstalledByokKey) return false
return state.value.some(
(item) =>
item.provider_id === subscription.providerId && item.management_source === "coding_plan" && item.is_enabled,
)
}
export function plans(state: CloudState) {
if (!state.plans.ok) return []
return state.plans.value
.filter((item) => (item.status === "active" || item.status === "past_due") && installed(item, state.byok))
.sort((a, b) => a.id.localeCompare(b.id))
}
function durationMs(period: CodingPlanQuotaWindow["period"]) {
const multipliers = {
hour: 60 * 60 * 1000,
day: 24 * 60 * 60 * 1000,
week: 7 * 24 * 60 * 60 * 1000,
} as const
if (period.unit === "month") return undefined
return period.value * multipliers[period.unit]
}
function window(subscriptionId: string, value: CodingPlanQuotaWindow): ProviderUsage.UsageWindow {
const remaining = value.remainingPercent
const duration = durationMs(value.period)
return {
id: `${subscriptionId}:${value.id}`,
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",
}
}
export async function managed(
token: string,
subscription: CodingPlanSubscription,
usage: typeof fetchCodingPlanUsage = fetchCodingPlanUsage,
): Promise<ProviderUsage.UsageSnapshot> {
const fetchedAt = new Date().toISOString()
const planState = subscription.cancelAtPeriodEnd
? "canceling"
: subscription.status === "past_due"
? "past_due"
: "active"
const id = `kilo-managed:${subscription.id}`
const managementUrl = `${base()}/subscriptions/coding-plans/${subscription.id}`
return usage(token, subscription.id)
.then((usage) => {
const windows = usage.subscription.windows.map((item) => window(usage.subscription.id, item))
return {
id,
providerID: usage.subscription.providerId,
sourceKind: "kilo_managed",
providerLabel: usage.subscription.providerName,
planLabel: usage.subscription.planName,
sourceLabel: "via Kilo",
fetchState: "ready",
planState,
routingState: "active",
fetchedAt: usage.fetchedAt,
managementUrl,
windows,
} satisfies ProviderUsage.UsageSnapshot
})
.catch(() => ({
id,
providerID: subscription.providerId,
sourceKind: "kilo_managed",
providerLabel: subscription.providerName,
planLabel: subscription.planName,
sourceLabel: "via Kilo",
fetchState: "unavailable",
planState,
routingState: "active",
fetchedAt,
managementUrl,
windows: [],
error: error("managed_subscription_unavailable", "Usage unavailable."),
}))
}
@@ -0,0 +1,34 @@
import { Schema } from "effect"
const IntegerField = Schema.Int
// Matches the cloud schema: remaining percent is a 0-100 share of the base quota; boosts scale it separately.
const PercentField = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(100))
export const ModelRemains = Schema.Struct({
model_name: Schema.String,
current_interval_total_count: Schema.optional(IntegerField),
current_interval_usage_count: Schema.optional(IntegerField),
start_time: Schema.optional(IntegerField),
end_time: Schema.optional(IntegerField),
remains_time: Schema.optional(IntegerField),
interval_boost_permille: Schema.optional(IntegerField),
current_interval_remaining_percent: Schema.optional(PercentField),
current_interval_status: Schema.optional(IntegerField),
current_weekly_total_count: Schema.optional(IntegerField),
current_weekly_usage_count: Schema.optional(IntegerField),
weekly_start_time: Schema.optional(IntegerField),
weekly_end_time: Schema.optional(IntegerField),
weekly_remains_time: Schema.optional(IntegerField),
weekly_boost_permille: Schema.optional(IntegerField),
current_weekly_remaining_percent: Schema.optional(PercentField),
current_weekly_status: Schema.optional(IntegerField),
}).annotate({ identifier: "MiniMaxModelRemains" })
export type ModelRemains = typeof ModelRemains.Type
export const Native = Schema.Struct({
base_resp: Schema.Struct({ status_code: IntegerField }),
model_remains: Schema.Array(ModelRemains),
}).annotate({ identifier: "MiniMaxNativeUsage" })
export type Native = typeof Native.Type
export const decode = Schema.decodeUnknownSync(Native)
@@ -0,0 +1,284 @@
import { createHash } from "node:crypto"
import { decode, type ModelRemains, type Native } from "./native"
import type { ProviderUsage } from "@opencode-ai/schema/kilocode/provider-usage"
export const bindings = {
"minimax-coding-plan": {
region: "global",
url: "https://api.minimax.io/v1/token_plan/remains",
manage: "https://platform.minimax.io/subscribe/token-plan",
},
"minimax-cn-coding-plan": {
region: "china",
url: "https://api.minimaxi.com/v1/token_plan/remains",
manage: "https://platform.minimaxi.com/subscribe/token-plan",
},
} as const
type ProviderID = keyof typeof bindings
const timeout = 5_000
const limit = 64 * 1024
class MiniMaxUsageError extends Error {
constructor(readonly code: "network" | "http" | "too_large" | "invalid" | "application") {
super("MiniMax usage is temporarily unavailable.")
this.name = "MiniMaxUsageError"
}
}
async function text(response: Response) {
const declared = Number(response.headers.get("content-length"))
if (Number.isFinite(declared) && declared > limit) {
response.body?.cancel().catch(() => undefined)
throw new MiniMaxUsageError("too_large")
}
if (!response.body) {
const value = await response.arrayBuffer()
if (value.byteLength > limit) throw new MiniMaxUsageError("too_large")
return new TextDecoder().decode(value)
}
const reader = response.body.getReader()
const chunks: Uint8Array[] = []
let size = 0
while (true) {
const chunk = await reader.read()
if (chunk.done) break
if (!chunk.value) continue
size += chunk.value.byteLength
if (size > limit) {
await reader.cancel().catch(() => undefined)
throw new MiniMaxUsageError("too_large")
}
chunks.push(chunk.value)
}
const value = new Uint8Array(size)
let offset = 0
for (const chunk of chunks) {
value.set(chunk, offset)
offset += chunk.byteLength
}
return new TextDecoder().decode(value)
}
export async function query(providerID: ProviderID, key: string, fetcher: typeof fetch = fetch): Promise<Native> {
const response = await fetcher(bindings[providerID].url, {
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${key}`,
},
cache: "no-store",
redirect: "error",
signal: AbortSignal.timeout(timeout),
}).catch(() => {
throw new MiniMaxUsageError("network")
})
if (!response.ok) {
response.body?.cancel().catch(() => undefined)
throw new MiniMaxUsageError("http")
}
const body = await text(response)
const native = (() => {
try {
return decode(JSON.parse(body))
} catch {
throw new MiniMaxUsageError("invalid")
}
})()
if (native.base_resp.status_code !== 0) throw new MiniMaxUsageError("application")
return native
}
function reset(end: number | undefined, remains: number | undefined, fetchedAt: string) {
if (end !== undefined && end > 0) return new Date(end).toISOString()
if (remains !== undefined && remains > 0) return new Date(Date.parse(fetchedAt) + remains).toISOString()
return undefined
}
function duration(start: number | undefined, end: number | undefined) {
if (start === undefined || end === undefined || end <= start) return undefined
return end - start
}
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(
row: ModelRemains,
kind: "interval" | "weekly",
fetchedAt: string,
): ProviderUsage.UsageWindow | undefined {
const weekly = kind === "weekly"
const percent = weekly ? row.current_weekly_remaining_percent : row.current_interval_remaining_percent
const status = weekly ? row.current_weekly_status : row.current_interval_status
const total = weekly ? row.current_weekly_total_count : row.current_interval_total_count
const count = weekly ? row.current_weekly_usage_count : row.current_interval_usage_count
const start = weekly ? row.weekly_start_time : row.start_time
const end = weekly ? row.weekly_end_time : row.end_time
const remains = weekly ? row.weekly_remains_time : row.remains_time
const boost = weekly ? row.weekly_boost_permille : row.interval_boost_permille
const span = duration(start, end)
const base = {
id: `${row.model_name}-${kind}`,
resource: row.model_name,
period: cadence(kind, span),
durationMs: span,
resetAt: reset(end, remains, fetchedAt),
}
if (status === 3) return { ...base, unit: "unknown", orientation: "amount", state: "not_in_plan" }
if (percent !== undefined) {
const factor = boost !== undefined && boost > 0 ? boost / 1000 : 1
const cap = 100 * factor
// The status flag is authoritative: an exhausted window has zero remaining even when the percent field lags.
const remaining = status === 2 ? 0 : percent * factor
return {
...base,
unit: factor === 1 ? "percent" : "standard_units",
orientation: factor === 1 ? "remaining_percent" : "amount",
used: Math.max(0, cap - remaining),
remaining,
limit: cap,
state: status === 2 || remaining <= 0 ? "exhausted" : "active",
}
}
if (total !== undefined && total > 0 && count !== undefined && count >= 0) {
// Despite the name, MiniMax's *_usage_count fields report the remaining quota, not the consumed amount.
const remaining = status === 2 ? 0 : count
return {
...base,
unit: "count",
orientation: "count",
used: Math.max(0, total - remaining),
remaining,
limit: total,
state: status === 2 || remaining === 0 ? "exhausted" : "active",
}
}
if (status === undefined && total === undefined && count === undefined) return undefined
return {
...base,
unit: "unknown",
orientation: "amount",
state: status === 2 ? "exhausted" : "unknown",
}
}
export function normalize(
native: Native,
input: {
id: string
providerID: string
sourceLabel: string
managementUrl: string
fetchedAt: string
},
): ProviderUsage.UsageSnapshot {
const windows = native.model_remains
.filter((row) => row.model_name !== "video")
.flatMap((row) =>
(["interval", "weekly"] as const).flatMap((kind) => {
const value = window(row, kind, input.fetchedAt)
return value ? [value] : []
}),
)
return {
id: input.id,
providerID: input.providerID,
sourceKind: "direct",
providerLabel: "MiniMax",
planLabel: "MiniMax Token Plan",
sourceLabel: input.sourceLabel,
fetchState: "ready",
planState: "active",
routingState: "not_applicable",
fetchedAt: input.fetchedAt,
managementUrl: input.managementUrl,
windows,
}
}
const unavailable = (
id: string,
providerID: string,
label: string,
managementUrl: string,
): ProviderUsage.UsageSnapshot => ({
id,
providerID,
sourceKind: "direct",
providerLabel: "MiniMax",
planLabel: "MiniMax Token Plan",
sourceLabel: label,
fetchState: "unavailable",
planState: "unknown",
routingState: "not_applicable",
managementUrl,
windows: [],
error: { code: "direct_minimax_unavailable", message: "Usage unavailable.", retryable: true },
})
export interface Candidate {
providerID: ProviderID
label: string
key: string
}
export async function direct(
candidates: readonly Candidate[],
fetcher: typeof fetch = fetch,
cached: (
id: string,
load: () => Promise<ProviderUsage.UsageSnapshot>,
identity?: string,
) => Promise<ProviderUsage.UsageSnapshot> = (_id, load) => load(),
) {
// Each configured provider is an independent plan with a stable per-region
// cache cell, even when providers share a credential.
return Promise.all(
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,
)
}),
)
}
+2
View File
@@ -12,6 +12,7 @@ import { FileSystemSearch } from "./filesystem/search"
import { Watcher } from "./filesystem/watcher"
import { Image } from "./image"
import { Integration } from "./integration"
import { ProviderUsage } from "./kilocode/provider-usage" // kilocode_change
import { Location } from "./location"
import { LocationMutation } from "./location-mutation"
import { LocationServiceMap } from "./location-service-map"
@@ -48,6 +49,7 @@ export const locationServices = LayerNode.group([
Reference.node,
Integration.node,
Catalog.node,
ProviderUsage.node, // kilocode_change
AISDK.node,
PluginV2.node,
PluginInternal.node,
@@ -0,0 +1,93 @@
import { describe, expect, test } from "bun:test"
import * as Cloud from "../src/kilocode/provider-usage/cloud"
const subscription = {
id: "byteplus-plan",
planId: "byteplus-coding-plan-team-lite",
planName: "BytePlus Coding Plan Lite",
providerName: "BytePlus",
providerId: "byteplus-coding",
canQueryUsage: true,
hasInstalledByokKey: true,
status: "active" as const,
cancelAtPeriodEnd: false,
}
const state = (enabled = true) => ({
plans: { ok: true as const, value: [subscription] },
byok: {
ok: true as const,
value: [
{
id: "managed-byteplus",
provider_id: "byteplus-coding",
management_source: "coding_plan" as const,
is_enabled: enabled,
},
],
},
})
describe("managed provider usage", () => {
test("requires Cloud usage readiness and a matching enabled managed key", () => {
expect(Cloud.plans(state())).toEqual([subscription])
expect(Cloud.plans(state(false))).toEqual([])
expect(
Cloud.plans({ ...state(), plans: { ok: true, value: [{ ...subscription, canQueryUsage: false }] } }),
).toEqual([])
expect(
Cloud.plans({ ...state(), plans: { ok: true, value: [{ ...subscription, hasInstalledByokKey: false }] } }),
).toEqual([])
expect(
Cloud.plans({
...state(),
byok: { ok: true, value: [{ ...state().byok.value[0]!, management_source: "user" as const }] },
}),
).toEqual([])
expect(
Cloud.plans({
...state(),
byok: { ok: true, value: [{ ...state().byok.value[0]!, provider_id: "minimax" }] },
}),
).toEqual([])
})
test("normalizes BytePlus windows through the generic managed path", async () => {
const result = await Cloud.managed("token", subscription, async () => ({
schemaVersion: 1,
fetchedAt: "2026-08-07T12:00:00.000Z",
subscription: {
id: subscription.id,
planName: subscription.planName,
providerId: subscription.providerId,
providerName: subscription.providerName,
windows: [
{
id: "monthly",
remainingPercent: 75,
resetsAt: "2026-09-01T00:00:00.000Z",
period: { unit: "month", value: 1 },
},
],
},
}))
expect(result).toMatchObject({
providerID: "byteplus-coding",
providerLabel: "BytePlus",
planLabel: "BytePlus Coding Plan Lite",
sourceKind: "kilo_managed",
windows: [
{
id: "byteplus-plan:monthly",
resource: "subscription",
period: { unit: "month", value: 1 },
remaining: 75,
used: 25,
limit: 100,
},
],
})
expect(result.windows[0]).not.toHaveProperty("durationMs")
})
})
@@ -0,0 +1,37 @@
import { describe, expect } from "bun:test"
import { Context, Effect } from "effect"
import { AppNodeBuilder } from "../src/effect/app-node-builder"
import { ProviderUsage } from "../src/kilocode/provider-usage"
import { Location } from "../src/location"
import { LocationServiceMap } from "../src/location-services"
import { AbsolutePath } from "../src/schema"
import { WorkspaceV2 } from "../src/workspace"
import { tmpdir } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect"
const it = testEffect(AppNodeBuilder.build(LocationServiceMap.node))
describe("ProviderUsage location lifecycle", () => {
it.live("reuses the same location and isolates workspace-qualified locations", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((dir) =>
Effect.scoped(
Effect.gen(function* () {
const locations = yield* LocationServiceMap.Service
const directory = AbsolutePath.make(dir.path)
const first = Location.Ref.make({ directory, workspaceID: WorkspaceV2.ID.make("wrk_workspace_a") })
const same = Location.Ref.make({ directory, workspaceID: WorkspaceV2.ID.make("wrk_workspace_a") })
const second = Location.Ref.make({ directory, workspaceID: WorkspaceV2.ID.make("wrk_workspace_b") })
const firstService = Context.get(yield* locations.contextEffect(first), ProviderUsage.Service)
expect(Context.get(yield* locations.contextEffect(same), ProviderUsage.Service)).toBe(firstService)
expect(Context.get(yield* locations.contextEffect(second), ProviderUsage.Service)).not.toBe(firstService)
}),
),
),
),
)
})
@@ -0,0 +1,396 @@
import { describe, expect, mock, test } from "bun:test"
import { decode } from "../src/kilocode/provider-usage/minimax/native"
import { direct, normalize, query, type Candidate } from "../src/kilocode/provider-usage/minimax/usage"
import type { ProviderUsage } from "@opencode-ai/schema/kilocode/provider-usage"
const native = (row: Record<string, unknown>) =>
decode({
base_resp: { status_code: 0, status_msg: "stripped" },
model_remains: [{ model_name: "general", ...row }],
unknown: "stripped",
})
const options = {
id: "usage",
providerID: "minimax-coding-plan",
sourceLabel: "Direct",
managementUrl: "https://platform.minimax.io/subscribe/token-plan",
fetchedAt: "2026-06-19T00:00:00.000Z",
}
const candidate = (providerID: Candidate["providerID"], key: string): Candidate => ({
providerID,
label: providerID === "minimax-cn-coding-plan" ? "MiniMax China" : "MiniMax Global",
key,
})
const response = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } })
describe("MiniMax usage normalization", () => {
test("normalizes direct native percentage payloads", () => {
const payload = native({
current_interval_total_count: 1500,
current_interval_usage_count: 1,
current_interval_remaining_percent: 80,
current_interval_status: 1,
start_time: 1_781_827_200_000,
end_time: 1_781_845_200_000,
})
const direct = normalize(payload, options)
expect(direct.windows[0]).toMatchObject({
orientation: "remaining_percent",
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)
})
test("omits video quotas while preserving image quota state", () => {
const value = decode({
base_resp: { status_code: 0 },
model_remains: [
{
model_name: "video",
current_interval_remaining_percent: 100,
current_interval_status: 1,
},
{
model_name: "image",
current_interval_remaining_percent: 70,
current_interval_status: 1,
current_weekly_status: 3,
},
{
model_name: "general",
current_interval_total_count: 0,
current_interval_usage_count: 0,
current_interval_status: 1,
},
],
})
const direct = normalize(value, options)
expect(direct.windows.some((window) => window.resource === "video")).toBe(false)
expect(direct.windows.find((window) => window.id === "image-interval")).toMatchObject({
resource: "image",
remaining: 70,
state: "active",
})
expect(direct.windows.find((window) => window.id === "image-weekly")?.state).toBe("not_in_plan")
expect(direct.windows.find((window) => window.id === "general-interval")?.state).toBe("unknown")
})
test("uses positive count-only usage_count as remaining", () => {
const item = normalize(
native({
current_interval_total_count: 1500,
current_interval_usage_count: 1200,
current_interval_status: 1,
}),
options,
)
expect(item.windows[0]).toMatchObject({ orientation: "count", remaining: 1200, used: 300, limit: 1500 })
})
test("applies weekly boosts as capacity without clamping to 100", () => {
const item = normalize(
native({
current_weekly_remaining_percent: 100,
current_weekly_status: 1,
weekly_boost_permille: 1500,
}),
options,
)
expect(item.windows[0]).toMatchObject({
unit: "standard_units",
orientation: "amount",
remaining: 150,
limit: 150,
period: { unit: "week", value: 1 },
})
})
test("prefers absolute reset timestamps over remaining duration", () => {
const item = normalize(
native({
current_interval_remaining_percent: 50,
current_interval_status: 1,
end_time: 1_781_845_200_000,
remains_time: 60_000,
}),
options,
)
expect(item.windows[0]?.resetAt).toBe("2026-06-19T05:00:00.000Z")
})
})
describe("MiniMax usage window calculations", () => {
test("clamps used at zero when the remaining count exceeds the total", () => {
const item = normalize(
native({
current_interval_total_count: 1500,
current_interval_usage_count: 1600,
current_interval_status: 1,
}),
options,
)
expect(item.windows[0]).toMatchObject({
orientation: "count",
remaining: 1600,
used: 0,
limit: 1500,
state: "active",
})
})
test("marks count windows exhausted when nothing remains or the status flags it", () => {
const drained = normalize(
native({
current_interval_total_count: 1500,
current_interval_usage_count: 0,
current_interval_status: 1,
}),
options,
)
expect(drained.windows[0]).toMatchObject({ remaining: 0, used: 1500, state: "exhausted" })
const flagged = normalize(
native({
current_interval_total_count: 1500,
current_interval_usage_count: 800,
current_interval_status: 2,
}),
options,
)
expect(flagged.windows[0]).toMatchObject({ remaining: 0, used: 1500, state: "exhausted" })
})
test("treats an exhausted status as authoritative over lagging percent fields", () => {
const item = normalize(native({ current_interval_remaining_percent: 12, current_interval_status: 2 }), options)
expect(item.windows[0]).toMatchObject({ remaining: 0, used: 100, limit: 100, state: "exhausted" })
})
test("reads weekly count windows from the weekly fields", () => {
const item = normalize(
native({
current_weekly_total_count: 6000,
current_weekly_usage_count: 4500,
current_weekly_status: 1,
}),
options,
)
expect(item.windows).toHaveLength(1)
expect(item.windows[0]).toMatchObject({
id: "general-weekly",
orientation: "count",
remaining: 4500,
used: 1500,
limit: 6000,
period: { unit: "week", value: 1 },
})
})
test("rejects out-of-range percent values like the cloud schema does", () => {
expect(() => native({ current_interval_remaining_percent: 150 })).toThrow()
expect(() => native({ current_weekly_remaining_percent: -1 })).toThrow()
})
test("accepts the permille boost spelling and falls back to plain percent without a boost", () => {
const boosted = normalize(
native({
current_interval_remaining_percent: 50,
current_interval_status: 1,
interval_boost_permille: 2000,
}),
options,
)
expect(boosted.windows[0]).toMatchObject({
unit: "standard_units",
orientation: "amount",
remaining: 100,
used: 100,
limit: 200,
})
const plain = normalize(native({ current_interval_remaining_percent: 50, current_interval_status: 1 }), options)
expect(plain.windows[0]).toMatchObject({
unit: "percent",
orientation: "remaining_percent",
remaining: 50,
used: 50,
limit: 100,
})
})
test("marks percent windows exhausted when nothing remains", () => {
const item = normalize(native({ current_interval_remaining_percent: 0, current_interval_status: 1 }), options)
expect(item.windows[0]).toMatchObject({ remaining: 0, used: 100, state: "exhausted" })
})
test("derives the period from the window span when it is a round unit", () => {
const start = 1_781_827_200_000
const item = normalize(
native({
current_interval_remaining_percent: 80,
current_interval_status: 1,
start_time: start,
end_time: start + 14 * 24 * 60 * 60 * 1000,
}),
options,
)
expect(item.windows[0]?.period).toEqual({ unit: "week", value: 2 })
expect(item.windows[0]?.durationMs).toBe(14 * 24 * 60 * 60 * 1000)
})
test("omits the period when the span is not a round hour, day, or week", () => {
const start = 1_781_827_200_000
const item = normalize(
native({
current_interval_remaining_percent: 80,
current_interval_status: 1,
start_time: start,
end_time: start + 90 * 60 * 1000,
}),
options,
)
expect(item.windows[0]?.period).toBeUndefined()
expect(item.windows[0]?.durationMs).toBe(90 * 60 * 1000)
})
test("omits the duration and period when the window timestamps are inverted", () => {
const start = 1_781_827_200_000
const item = normalize(
native({
current_interval_remaining_percent: 80,
current_interval_status: 1,
start_time: start,
end_time: start,
}),
options,
)
expect(item.windows[0]?.durationMs).toBeUndefined()
expect(item.windows[0]?.period).toBeUndefined()
})
test("falls back to remains_time when end_time is missing or zero", () => {
const item = normalize(
native({
current_interval_remaining_percent: 80,
current_interval_status: 1,
end_time: 0,
remains_time: 3_600_000,
}),
options,
)
expect(item.windows[0]?.resetAt).toBe("2026-06-19T01:00:00.000Z")
const none = normalize(native({ current_interval_remaining_percent: 80, current_interval_status: 1 }), options)
expect(none.windows[0]?.resetAt).toBeUndefined()
})
test("omits windows with no usage signals and keeps status-only windows as unknown", () => {
const empty = normalize(native({}), options)
expect(empty.windows).toEqual([])
const item = normalize(native({ current_interval_status: 1 }), options)
expect(item.windows).toHaveLength(1)
expect(item.windows[0]).toMatchObject({ unit: "unknown", orientation: "amount", state: "unknown" })
})
})
describe("MiniMax usage transport and detection", () => {
test("uses fixed hosts and ignores configured base URLs", async () => {
const fn = mock(() => Promise.resolve(response({ base_resp: { status_code: 0 }, model_remains: [] })))
await query("minimax-coding-plan", "sk-cp-secret", fn as unknown as typeof fetch)
expect(fn).toHaveBeenCalledTimes(1)
const call = fn.mock.calls[0] as unknown as [string, RequestInit]
expect(call[0]).toBe("https://api.minimax.io/v1/token_plan/remains")
expect(call[1]).toMatchObject({ method: "GET", cache: "no-store", redirect: "error" })
expect(new Headers(call[1].headers).get("authorization")).toBe("Bearer sk-cp-secret")
})
test("does not query PAYG keys or provider IDs", async () => {
const fn = mock(() => Promise.resolve(response({})))
const items = await direct([candidate("minimax-coding-plan", "sk-api-payg")], fn as unknown as typeof fetch)
expect(items).toEqual([])
expect(fn).not.toHaveBeenCalled()
})
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")
? response({}, 401)
: response({
base_resp: { status_code: 0 },
model_remains: [{ model_name: "general", current_interval_remaining_percent: 90 }],
}),
),
)
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(fn).toHaveBeenCalledTimes(2)
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")
})
test("scopes the cache cell identity to the credential fingerprint", async () => {
const fn = mock(() => Promise.resolve(response({ base_resp: { status_code: 0 }, model_remains: [] })))
const seen: Array<{ id: string; identity?: string }> = []
const cached = (id: string, load: () => Promise<ProviderUsage.UsageSnapshot>, identity?: string) => {
seen.push({ id, identity })
return load()
}
await direct([candidate("minimax-coding-plan", "sk-cp-key-a")], fn as unknown as typeof fetch, cached)
await direct([candidate("minimax-coding-plan", "sk-cp-key-b")], fn as unknown as typeof fetch, cached)
expect(seen.map((item) => item.id)).toEqual(["minimax-direct-global", "minimax-direct-global"])
expect(seen[0].identity).toHaveLength(64)
expect(seen[0].identity).not.toBe(seen[1].identity)
expect(JSON.stringify(seen)).not.toContain("sk-cp")
})
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(2)
expect(items.every((item) => item.fetchState === "unavailable")).toBe(true)
expect(JSON.stringify(items)).not.toContain("raw failure")
})
})
@@ -0,0 +1,694 @@
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"
import { Integration } from "../src/integration"
import { ProviderUsage } from "../src/kilocode/provider-usage"
import { PluginV2 } from "../src/plugin"
import { ProviderV2 } from "../src/provider"
import { testEffect } from "./lib/effect"
const provider = ProviderV2.ID.make("minimax-coding-plan")
const chinaProvider = ProviderV2.ID.make("minimax-cn-coding-plan")
const integration = Integration.ID.make("minimax-coding-plan")
const chinaIntegration = Integration.ID.make("minimax-cn-coding-plan")
const kilo = Integration.ID.make("kilo")
type CatalogInput = {
apiKey?: string
setting?: string
header?: string
headerName?: string
organization?: string | (() => string | undefined)
china?: boolean
}
const catalog = (input?: CatalogInput) => {
const providers = () => {
const organization = typeof input?.organization === "function" ? input.organization() : input?.organization
const info = ProviderV2.Info.make({
id: provider,
name: "MiniMax Global",
api: { type: "native", settings: input?.setting ? { apiKey: input.setting } : {} },
request: {
headers: input?.header ? { [input.headerName ?? "x-api-key"]: input.header } : {},
body: input?.apiKey ? { apiKey: input.apiKey } : {},
},
})
const kiloInfo = ProviderV2.Info.make({
id: ProviderV2.ID.kilo,
name: "Kilo",
api: { type: "native", settings: {} },
request: { headers: {}, body: organization ? { kilocodeOrganizationId: organization } : {} },
})
const china = ProviderV2.Info.make({
id: chinaProvider,
name: "MiniMax China",
api: { type: "native", settings: {} },
request: { headers: {}, body: {} },
})
return input?.china ? [info, china, kiloInfo] : [info, kiloInfo]
}
return Layer.mock(Catalog.Service)({
provider: {
get: () => Effect.succeed(undefined),
all: () => Effect.sync(providers),
available: () => Effect.sync(providers),
},
model: {
get: () => Effect.succeed(undefined),
all: () => Effect.succeed([]),
available: () => Effect.succeed([]),
default: () => Effect.succeed(undefined),
small: () => Effect.succeed(undefined),
},
})
}
type DirectInput = string | ((id: Integration.ID) => string | undefined) | undefined
const directValue = (input: DirectInput, id: Integration.ID) => (typeof input === "function" ? input(id) : input)
const connections = (input: DirectInput, accountID?: string, failure?: () => "global" | "china" | "kilo" | undefined) =>
Layer.mock(Integration.Service)({
connection: {
active: (id) =>
Effect.sync(() => {
const direct = directValue(input, id)
return id === kilo || ((id === integration || id === chinaIntegration) && direct)
? {
type: "credential" as const,
id: Credential.ID.make(
id === kilo ? "cred_kilo" : id === chinaIntegration ? "cred_direct_cn" : "cred_direct",
),
label: "test",
}
: undefined
}),
resolve: (connection) =>
Effect.suspend(() => {
const target =
connection.type === "credential" && connection.id === "cred_direct_cn" ? chinaIntegration : integration
const direct = directValue(input, target)
const kind =
connection.type === "credential" && connection.id === "cred_kilo"
? "kilo"
: target === chinaIntegration
? "china"
: "global"
if (failure?.() === kind)
return Effect.fail(new Integration.AuthorizationError({ cause: `${kind} credential failure` }))
return Effect.succeed(
kind === "kilo"
? Credential.OAuth.make({
type: "oauth",
methodID: Integration.MethodID.make("oauth"),
access: "cloud-token",
refresh: "cloud-refresh",
expires: Date.now() + 60_000,
metadata: accountID ? { accountID } : undefined,
})
: direct
? Credential.Key.make({ type: "key", key: direct })
: undefined,
)
}),
key: () => Effect.void,
oauth: () => Effect.die("unused"),
update: () => Effect.void,
remove: () => Effect.void,
},
attempt: {
status: () => Effect.die("unused"),
complete: () => Effect.void,
cancel: () => Effect.void,
},
})
const native = (remaining: number) =>
Response.json({
base_resp: { status_code: 0 },
model_remains: [
{
model_name: "general",
current_interval_remaining_percent: remaining,
current_interval_status: 1,
},
],
})
const subscription = {
id: "byteplus-plan",
planId: "byteplus-coding-plan-team-lite",
planName: "BytePlus Coding Plan Lite",
providerName: "BytePlus",
providerId: "byteplus-coding",
canQueryUsage: true,
hasInstalledByokKey: true,
status: "active" as const,
cancelAtPeriodEnd: false,
}
const byok = {
id: "managed-byteplus",
provider_id: "byteplus-coding",
management_source: "coding_plan" as const,
is_enabled: true,
}
const transport = (calls: { direct: number; cloud: number }, remaining = 80) =>
Layer.succeed(ProviderUsage.Transport, {
fetch: mock(() => {
calls.direct++
return Promise.resolve(native(remaining))
}) as unknown as typeof fetch,
plans: async () => {
calls.cloud++
return []
},
byok: async () => [],
usage: async () => {
throw new Error("unused")
},
})
const plugins = Layer.mock(PluginV2.Service)({
add: () => Effect.void,
remove: () => Effect.void,
wait: () => Effect.void,
})
const configuredLayer = (input: {
calls: { direct: number; cloud: number }
direct?: DirectInput
accountID?: string
config?: CatalogInput
failure?: () => "global" | "china" | "kilo" | undefined
transport?: ProviderUsage.TransportInterface
}) =>
Layer.fresh(ProviderUsage.layer).pipe(
Layer.provide(catalog(input.config)),
Layer.provide(connections(input.direct, input.accountID, input.failure)),
Layer.provide(input.transport ? Layer.succeed(ProviderUsage.Transport, input.transport) : transport(input.calls)),
Layer.provide(plugins),
)
const layer = (
calls: { direct: number; cloud: number },
direct: DirectInput = "sk-cp-direct",
accountID?: string,
config?: CatalogInput,
failure?: () => "global" | "china" | "kilo" | undefined,
) => configuredLayer({ calls, direct, accountID, config, failure })
const it = testEffect(Layer.empty)
const service = Effect.fn("ProviderUsageTest.service")(function* (service: ProviderUsage.Interface) {
const first = yield* service.get()
const cached = yield* service.get()
const refreshed = yield* service.refresh()
return { first, cached, refreshed }
})
describe("ProviderUsage location service", () => {
it.live("caches one location and forces every source on refresh", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
const scope = yield* Scope.make()
const usage = Context.get(yield* Layer.buildWithScope(layer(calls), scope), ProviderUsage.Service)
const result = yield* service(usage)
expect(result.first.items).toHaveLength(1)
expect(result.cached).toEqual(result.first)
expect(result.refreshed.items).toHaveLength(1)
expect(calls).toEqual({ direct: 2, cloud: 2 })
yield* Scope.close(scope, Exit.void)
}),
)
it.live("isolates state between location-layer instances", () =>
Effect.gen(function* () {
const firstCalls = { direct: 0, cloud: 0 }
const secondCalls = { direct: 0, cloud: 0 }
const firstScope = yield* Scope.make()
const secondScope = yield* Scope.make()
const first = Context.get(yield* Layer.buildWithScope(layer(firstCalls), firstScope), ProviderUsage.Service)
const second = Context.get(yield* Layer.buildWithScope(layer(secondCalls), secondScope), ProviderUsage.Service)
expect((yield* first.get()).items[0]?.windows[0]?.remaining).toBe(80)
expect((yield* second.get()).items[0]?.windows[0]?.remaining).toBe(80)
expect(firstCalls.direct).toBe(1)
expect(secondCalls.direct).toBe(1)
}),
)
it.live("suppresses personal Cloud calls for organization OAuth", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
const scope = yield* Scope.make()
const usage = Context.get(
yield* Layer.buildWithScope(layer(calls, "sk-cp-direct", "org"), scope),
ProviderUsage.Service,
)
expect((yield* usage.get()).items).toHaveLength(1)
expect(calls).toEqual({ direct: 1, cloud: 0 })
}),
)
it.live("uses every canonical config-defined coding-plan key location", () =>
Effect.gen(function* () {
for (const config of [
{ apiKey: "sk-cp-body" },
{ setting: "sk-cp-setting" },
{ header: "sk-cp-header", headerName: "X-API-Key" },
]) {
const calls = { direct: 0, cloud: 0 }
const scope = yield* Scope.make()
const usage = Context.get(
yield* Layer.buildWithScope(layer(calls, undefined, "org", config), scope),
ProviderUsage.Service,
)
expect((yield* usage.get()).items[0]?.providerID).toBe("minimax-coding-plan")
expect(calls.direct).toBe(1)
}
}),
)
it.live("suppresses personal Cloud calls for configured organization routing", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
const scope = yield* Scope.make()
const usage = Context.get(
yield* Layer.buildWithScope(layer(calls, "sk-cp-direct", undefined, { organization: "org" }), scope),
ProviderUsage.Service,
)
expect((yield* usage.get()).items).toHaveLength(1)
expect(calls).toEqual({ direct: 1, cloud: 0 })
}),
)
it.live("ignores empty configured organization routing", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
const scope = yield* Scope.make()
const usage = Context.get(
yield* Layer.buildWithScope(layer(calls, "sk-cp-direct", undefined, { organization: " " }), scope),
ProviderUsage.Service,
)
expect((yield* usage.get()).items).toHaveLength(1)
expect(calls).toEqual({ direct: 1, cloud: 1 })
}),
)
it.live("replaces cached direct usage when the credential changes", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
let key = "sk-cp-first"
const scope = yield* Scope.make()
const usage = Context.get(
yield* Layer.buildWithScope(
layer(calls, () => key, "org"),
scope,
),
ProviderUsage.Service,
)
expect((yield* usage.get()).items[0]?.windows[0]?.remaining).toBe(80)
key = "sk-cp-second"
expect((yield* usage.get()).items[0]?.windows[0]?.remaining).toBe(80)
expect(calls.direct).toBe(2)
}),
)
it.live("coalesces a forced refresh with an in-flight read", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
const started = yield* Deferred.make<void>()
let release!: (value: Response) => void
const response = new Promise<Response>((resolve) => (release = resolve))
const scope = yield* Scope.make()
const usage = Context.get(
yield* Layer.buildWithScope(
configuredLayer({
calls,
direct: "sk-cp-direct",
accountID: "org",
transport: {
fetch: (() => {
calls.direct++
Effect.runSync(Deferred.succeed(started, undefined))
return response
}) as unknown as typeof fetch,
plans: async () => [],
byok: async () => [],
usage: async () => {
throw new Error("unused")
},
},
}),
scope,
),
ProviderUsage.Service,
)
const first = yield* usage.get().pipe(Effect.forkChild)
yield* Deferred.await(started)
const second = yield* usage.refresh().pipe(Effect.forkChild)
yield* Effect.yieldNow
release(native(80))
expect((yield* Fiber.join(first)).items).toHaveLength(1)
expect((yield* Fiber.join(second)).items).toHaveLength(1)
expect(calls.direct).toBe(1)
}),
)
it.live("serves stale data after a failed refresh and recovers on retry", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
const responses = [native(80), new Response("private upstream error", { status: 503 }), native(70)]
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,
)
const ready = yield* usage.get()
const stale = yield* usage.refresh()
const recovered = yield* usage.refresh()
expect(ready.items[0]).toMatchObject({ fetchState: "ready", windows: [{ remaining: 80 }] })
expect(stale.items[0]).toMatchObject({ fetchState: "stale", windows: [{ remaining: 80 }] })
expect(recovered.items[0]).toMatchObject({ fetchState: "ready", windows: [{ remaining: 70 }] })
expect(JSON.stringify(stale)).not.toContain("private upstream error")
}),
)
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 }
let removedKey: string | undefined = "sk-cp-present"
const removedScope = yield* Scope.make()
const removed = Context.get(
yield* Layer.buildWithScope(
layer(removedCalls, () => removedKey, "org"),
removedScope,
),
ProviderUsage.Service,
)
expect((yield* removed.get()).items).toHaveLength(1)
removedKey = undefined
expect((yield* removed.get()).items).toEqual([])
const failedCalls = { direct: 0, cloud: 0 }
let failure: "global" | undefined
const failedScope = yield* Scope.make()
const failed = Context.get(
yield* Layer.buildWithScope(
layer(failedCalls, "sk-cp-present", "org", undefined, () => failure),
failedScope,
),
ProviderUsage.Service,
)
expect((yield* failed.get()).items).toHaveLength(1)
failure = "global"
expect((yield* failed.get()).items[0]).toMatchObject({ fetchState: "stale" })
expect(failedCalls.direct).toBe(1)
}),
)
it.live("preserves only the failed direct provider while pruning a removed sibling", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
let chinaKey: string | undefined = "sk-cp-china"
let failure: "global" | undefined
const scope = yield* Scope.make()
const usage = Context.get(
yield* Layer.buildWithScope(
configuredLayer({
calls,
accountID: "org",
config: { china: true, apiKey: "sk-cp-fallback" },
direct: (id) => (id === chinaIntegration ? chinaKey : "sk-cp-global"),
failure: () => failure,
}),
scope,
),
ProviderUsage.Service,
)
expect((yield* usage.get()).items).toHaveLength(2)
failure = "global"
chinaKey = undefined
const result = yield* usage.get()
expect(result.items).toHaveLength(1)
expect(result.items[0]).toMatchObject({ providerID: "minimax-coding-plan", fetchState: "stale" })
expect(calls.direct).toBe(2)
}),
)
it.live("keeps same-key providers independent when one credential fails", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
let failure: "global" | undefined
const scope = yield* Scope.make()
const usage = Context.get(
yield* Layer.buildWithScope(
configuredLayer({
calls,
accountID: "org",
config: { china: true },
direct: "sk-cp-shared",
failure: () => failure,
}),
scope,
),
ProviderUsage.Service,
)
expect((yield* usage.get()).items).toHaveLength(2)
failure = "global"
const result = yield* usage.get()
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("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
let key = "sk-cp-shared"
const scope = yield* Scope.make()
const usage = Context.get(
yield* Layer.buildWithScope(
configuredLayer({
calls,
accountID: "org",
config: { china: true },
direct: () => key,
failure: () => failure,
}),
scope,
),
ProviderUsage.Service,
)
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-global")).toMatchObject({ fetchState: "stale" })
expect(result.items.find((item) => item.id === "minimax-direct-china")).toMatchObject({ fetchState: "ready" })
expect(calls.direct).toBe(3)
}),
)
it.live("omits a failed provider with no cached usage", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
const scope = yield* Scope.make()
const usage = Context.get(
yield* Layer.buildWithScope(
configuredLayer({
calls,
accountID: "org",
config: { china: true },
direct: "sk-cp-shared",
failure: () => "global" as const,
}),
scope,
),
ProviderUsage.Service,
)
const result = yield* usage.get()
expect(result.items).toHaveLength(1)
expect(result.items[0]).toMatchObject({ id: "minimax-direct-china", fetchState: "ready" })
expect(calls.direct).toBe(1)
}),
)
it.live("preserves managed usage across metadata and credential failures while direct usage refreshes", () =>
Effect.gen(function* () {
const calls = { direct: 0, cloud: 0 }
let byokFailure = false
let usageFailure = false
let credentialFailure: "kilo" | undefined
let organization: string | undefined
const scope = yield* Scope.make()
const usage = Context.get(
yield* Layer.buildWithScope(
configuredLayer({
calls,
direct: "sk-cp-direct",
config: { organization: () => organization },
failure: () => credentialFailure,
transport: {
fetch: mock(() => {
calls.direct++
return Promise.resolve(native(80))
}) as unknown as typeof fetch,
plans: async () => {
calls.cloud++
return [subscription]
},
byok: async () => {
if (byokFailure) throw new Error("private metadata failure")
return [byok]
},
usage: async () => {
if (usageFailure) throw new Error("private usage failure")
return {
schemaVersion: 1,
fetchedAt: "2026-08-09T12:00:00.000Z",
subscription: {
id: subscription.id,
planName: subscription.planName,
providerId: subscription.providerId,
providerName: subscription.providerName,
windows: [
{
id: "monthly",
remainingPercent: 75,
resetsAt: "2026-09-01T00:00:00.000Z",
period: { unit: "month", value: 1 },
},
],
},
}
},
},
}),
scope,
),
ProviderUsage.Service,
)
expect((yield* usage.get()).items).toHaveLength(2)
byokFailure = true
const partial = yield* usage.refresh()
// Discovery failure retains the last good Cloud state, so usage still refreshes.
expect(partial.items.find((item) => item.sourceKind === "kilo_managed")).toMatchObject({ fetchState: "ready" })
expect(partial.items.find((item) => item.sourceKind === "direct")).toMatchObject({ fetchState: "ready" })
expect(JSON.stringify(partial)).not.toContain("private metadata failure")
usageFailure = true
const degraded = yield* usage.refresh()
expect(degraded.items.find((item) => item.sourceKind === "kilo_managed")).toMatchObject({ fetchState: "stale" })
expect(JSON.stringify(degraded)).not.toContain("private usage failure")
byokFailure = false
usageFailure = false
credentialFailure = "kilo"
const credential = yield* usage.get()
expect(credential.items.find((item) => item.sourceKind === "kilo_managed")).toMatchObject({
fetchState: "stale",
})
expect(credential.items.find((item) => item.sourceKind === "direct")).toBeDefined()
organization = "org"
const organizationResult = yield* usage.get()
expect(organizationResult.items.find((item) => item.sourceKind === "kilo_managed")).toBeUndefined()
expect(organizationResult.items.find((item) => item.sourceKind === "direct")).toBeDefined()
}),
)
})
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f3bb2c260c3294003140fe03f1732915536aab2733151ac17e27bf2a7c40dff2
size 23882
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2f353022aa26937291c9d2d3a34bba222e87695e609cc5de4f98cbe57e90a7c0
size 19828
oid sha256:9b4c9589d542c89b2f2ce43773088e144da6fba554da790251df9b7df70326a2
size 33911
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f5fe1c7f2960984736863aac8dc720eff1cf60cc5b1991e966740b395565b8ad
size 16069
oid sha256:b5807f0f023b36cf7f2d85a2891108275a513c31fa2cc6b76b6356e6ec1dba3b
size 33844
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b70dcaf2b3f89e0644fb47273881076cbb84cf5d36ce506685689968edb021dc
size 5527
oid sha256:4ff5dbb499e51fa149032b492a6d9a61ec2f54ddd5af3d3f786794d9bbee3d20
size 17803
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a634c466f2d854a245c210b695f86363ec9bfe76cbf4cb2c1e1baf174e010935
size 22571
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:65c9606f05c45b1f143e6283c41a667322d13d728c6c728e627c8075a1df1fc0
size 22986
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:445a492fadd3bda00cb28c4942ea57e003be9788bd920414a20f75491a8ea817
size 38665
+1
View File
@@ -21,6 +21,7 @@
"./fim": "./src/fim.ts",
"./edit": "./src/edit.ts",
"./edit-prompt": "./src/edit-prompt.ts",
"./provider-usage": "./src/provider-usage.ts",
"./tui": "./src/tui.ts"
},
"files": [
@@ -10,12 +10,17 @@ function num(value: unknown) {
return typeof value === "number" && Number.isFinite(value) ? value : 0
}
// Cloud returns the full subscription record even after cancellation; only
// these statuses represent a pass the user can actually consume.
const live = new Set(["active", "past_due", "trialing"])
export function parseKiloPassState(value: unknown): KiloPassState | null {
const item = Array.isArray(value) ? value[0] : value
const data = record(record(record(item)?.result)?.data)
const root = record(data?.json) ?? data ?? record(value)
const sub = record(root?.subscription)
if (!sub || (sub.currentPeriodBaseCreditsUsd == null && sub.currentPeriodUsageUsd == null)) return null
if (typeof sub.status === "string" && !live.has(sub.status)) return null
const next = sub.nextBillingAt ?? sub.nextRenewalAt
return {
+175
View File
@@ -0,0 +1,175 @@
import { z } from "zod"
import { buildKiloHeaders } from "../headers.js"
import { KILO_API_BASE } from "./constants.js"
const timeout = 5_000
const limit = 512 * 1024
const CodingPlanSubscriptionSchema = z.object({
id: z.string(),
planId: z.string(),
planName: z.string(),
providerName: z.string(),
providerId: z.string(),
canQueryUsage: z.boolean(),
hasInstalledByokKey: z.boolean(),
status: z.enum(["active", "past_due", "canceled"]),
cancelAtPeriodEnd: z.boolean(),
})
const ByokEntrySchema = z.object({
id: z.string(),
provider_id: z.string(),
management_source: z.enum(["user", "coding_plan"]),
is_enabled: z.boolean(),
})
const CodingPlanQuotaWindowSchema = z.object({
id: z
.string()
.min(1)
.max(64)
.regex(/^[a-z][a-z0-9_]*$/),
remainingPercent: z.number().finite().nonnegative(),
resetsAt: z.iso.datetime(),
startsAt: z.iso.datetime().optional(),
period: z.object({
unit: z.enum(["hour", "day", "week", "month"]),
value: z.number().int().positive(),
}),
})
const CodingPlanQuotaWindowsSchema = z
.array(CodingPlanQuotaWindowSchema)
.min(1)
.max(16)
.superRefine((windows, ctx) => {
const ids = new Set<string>()
for (const [index, window] of windows.entries()) {
if (ids.has(window.id)) {
ctx.addIssue({ code: "custom", message: "Quota window IDs must be unique.", path: [index, "id"] })
}
ids.add(window.id)
}
})
export const CodingPlanUsageSchema = z.object({
schemaVersion: z.literal(1),
fetchedAt: z.iso.datetime(),
subscription: z.object({
id: z.string(),
planName: z.string().min(1),
providerId: z.string().min(1),
providerName: z.string().min(1),
windows: CodingPlanQuotaWindowsSchema,
}),
})
const envelope = z.object({
result: z.object({ data: z.unknown() }).optional(),
error: z.unknown().optional(),
})
export type CodingPlanSubscription = z.infer<typeof CodingPlanSubscriptionSchema>
export type ByokEntry = z.infer<typeof ByokEntrySchema>
export type CodingPlanUsage = z.infer<typeof CodingPlanUsageSchema>
export type CodingPlanQuotaWindow = z.infer<typeof CodingPlanQuotaWindowSchema>
async function read(response: Response) {
const declared = Number(response.headers.get("content-length"))
if (Number.isFinite(declared) && declared > limit) {
response.body?.cancel().catch(() => undefined)
throw new CloudTrpcError("protocol", response.status)
}
if (!response.body) {
const body = await response.arrayBuffer()
if (body.byteLength > limit) throw new CloudTrpcError("protocol", response.status)
return new TextDecoder().decode(body)
}
const reader = response.body.getReader()
const chunks: Uint8Array[] = []
let size = 0
while (true) {
const chunk = await reader.read()
if (chunk.done) break
if (!chunk.value) continue
size += chunk.value.byteLength
if (size > limit) {
await reader.cancel().catch(() => undefined)
throw new CloudTrpcError("protocol", response.status)
}
chunks.push(chunk.value)
}
const body = new Uint8Array(size)
let offset = 0
for (const chunk of chunks) {
body.set(chunk, offset)
offset += chunk.byteLength
}
return new TextDecoder().decode(body)
}
export class CloudTrpcError extends Error {
constructor(
readonly kind: "network" | "http" | "protocol" | "procedure" | "schema",
readonly status?: number,
) {
super("Kilo Cloud data is temporarily unavailable.")
this.name = "CloudTrpcError"
}
}
async function query<T>(procedure: string, token: string, schema: z.ZodType<T>, input?: unknown): Promise<T> {
const params = new URLSearchParams()
if (input !== undefined) params.set("input", JSON.stringify(input))
const suffix = params.size ? `?${params.toString()}` : ""
const response = await fetch(`${KILO_API_BASE}/api/trpc/${procedure}${suffix}`, {
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${token}`,
...buildKiloHeaders(),
},
redirect: "error",
signal: AbortSignal.timeout(timeout),
}).catch(() => {
throw new CloudTrpcError("network")
})
const body = await read(response).catch((error) => {
if (error instanceof CloudTrpcError) throw error
throw new CloudTrpcError("protocol", response.status)
})
const parsed = (() => {
try {
return envelope.parse(JSON.parse(body))
} catch {
throw new CloudTrpcError("protocol", response.status)
}
})()
if (parsed.error != null) throw new CloudTrpcError("procedure", response.status)
if (!response.ok) throw new CloudTrpcError("http", response.status)
if (!parsed.result) throw new CloudTrpcError("protocol", response.status)
const data = parsed.result.data
const value = typeof data === "object" && data !== null && "json" in data ? (data as { json: unknown }).json : data
const result = schema.safeParse(value)
if (!result.success) throw new CloudTrpcError("schema", response.status)
return result.data
}
export function fetchCodingPlanSubscriptions(token: string) {
return query("codingPlans.listSubscriptions", token, z.array(CodingPlanSubscriptionSchema))
}
export function fetchByokEntries(token: string) {
return query("byok.list", token, z.array(ByokEntrySchema), {})
}
export async function fetchCodingPlanUsage(token: string, subscriptionId: string) {
const usage = await query("codingPlans.getUsage", token, CodingPlanUsageSchema, { subscriptionId })
if (usage.subscription.id !== subscriptionId) throw new CloudTrpcError("schema")
return usage
}
+8
View File
@@ -70,6 +70,14 @@ export {
type OrganizationModeConfig,
} from "./api/modes.js"
export { fetchKilocodeNotifications, type KilocodeNotification } from "./api/notifications.js"
export {
fetchByokEntries,
fetchCodingPlanSubscriptions,
fetchCodingPlanUsage,
type ByokEntry,
type CodingPlanSubscription,
type CodingPlanQuotaWindow,
} from "./api/trpc.js"
export {
fetchCloudSession,
fetchCloudSessionForImport,
+110
View File
@@ -0,0 +1,110 @@
/**
* Shared display formatting for provider usage windows.
*
* This is the single source of truth for how a quota window is presented.
* Both the TUI dialog (packages/opencode) and the VS Code webview consume it
* 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
}
export interface UsageLabels {
unlimited: string
notInPlan: string
unknown: string
exhausted: string
used(value: string): string
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 = {
unlimited: "Unlimited",
notInPlan: "Not in plan",
unknown: "Unknown",
exhausted: "Exhausted",
used: (value) => `${value} used`,
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 })
const amount = (value: number, unit: string) => {
if (unit === "USD") return `$${value.toFixed(2)}`
if (unit === "percent") return `${number(value)}%`
if (unit === "count") return number(value)
return `${number(value)} ${unit}`
}
export const formatWindow = (window: UsageWindowLike, labels: UsageLabels = english) => {
if (window.state === "unlimited") return labels.unlimited
if (window.state === "not_in_plan") return labels.notInPlan
if (window.state === "unknown") return labels.unknown
if (window.orientation === "used_percent" && window.used !== undefined) return labels.used(`${number(window.used)}%`)
if (window.orientation === "remaining_percent" && window.remaining !== undefined)
return labels.remaining(`${number(window.remaining)}%`)
if (window.remaining !== undefined && window.limit !== undefined)
return labels.remainingOf(amount(window.remaining, window.unit), amount(window.limit, window.unit))
if (window.used !== undefined && window.limit !== undefined)
return labels.usedOf(amount(window.used, window.unit), amount(window.limit, window.unit))
return window.state === "exhausted" ? labels.exhausted : labels.unknown
}
export const windowProgress = (window: UsageWindowLike) => {
if (window.limit === undefined || window.limit <= 0) return undefined
if (window.used !== undefined) return Math.min(100, Math.max(0, (window.used / window.limit) * 100))
if (window.remaining !== undefined) return Math.min(100, Math.max(0, 100 - (window.remaining / window.limit) * 100))
return undefined
}
@@ -61,6 +61,30 @@ describe("parseKiloPassState", () => {
expect(parseKiloPassState({ status: "none" })).toBeNull()
})
test("hides canceled and expired subscriptions that still report period credits", () => {
const payload = (status: string) => [
{
result: {
data: {
subscription: {
tier: "tier_19",
status,
cancelAtPeriodEnd: true,
currentPeriodBaseCreditsUsd: 19,
currentPeriodUsageUsd: 0,
currentPeriodBonusCreditsUsd: null,
nextBillingAt: null,
},
},
},
},
]
expect(parseKiloPassState(payload("canceled"))).toBeNull()
expect(parseKiloPassState(payload("expired"))).toBeNull()
expect(parseKiloPassState(payload("past_due"))).toMatchObject({ currentPeriodBaseCreditsUsd: 19 })
})
test("silently ignores transport failures", async () => {
const prev = global.fetch
const warn = spyOn(console, "warn").mockImplementation(() => undefined)
+190
View File
@@ -0,0 +1,190 @@
import { afterEach, describe, expect, mock, test } from "bun:test"
import {
CloudTrpcError,
fetchByokEntries,
fetchCodingPlanSubscriptions,
fetchCodingPlanUsage,
} from "../../src/api/trpc"
const original = global.fetch
const result = (data: unknown, status = 200) =>
new Response(JSON.stringify({ result: { data: { json: data } } }), {
status,
headers: { "content-type": "application/json" },
})
const subscription = {
id: "subscription",
planId: "minimax-token-plan-plus",
planName: "Token Plan Plus",
providerName: "MiniMax",
providerId: "minimax",
canQueryUsage: true,
hasInstalledByokKey: true,
status: "active",
cancelAtPeriodEnd: false,
}
const quota = (id = "plan") => ({
schemaVersion: 1,
fetchedAt: "2026-06-19T00:00:00.000Z",
subscription: {
id,
planName: "Token Plan Plus",
providerId: "minimax",
providerName: "MiniMax",
windows: [
{
id: "short_term",
remainingPercent: 80,
resetsAt: "2026-06-19T05:00:00.000Z",
period: { unit: "hour", value: 5 },
},
{
id: "weekly",
remainingPercent: 150,
resetsAt: "2026-06-26T00:00:00.000Z",
period: { unit: "week", value: 1 },
},
],
},
})
afterEach(() => {
global.fetch = original
})
describe("Cloud tRPC client", () => {
test("uses unbatched GET queries without an organization header", async () => {
const fn = mock(() =>
Promise.resolve(
result([
{
...subscription,
routeLabel: "MiniMax via Kilo Gateway",
billingPeriodDays: 30,
currentPeriodStart: "2026-06-01T00:00:00.000Z",
currentPeriodEnd: "2026-07-01T00:00:00.000Z",
creditRenewalAt: "2026-07-01T00:00:00.000Z",
paymentGraceExpiresAt: null,
canceledAt: null,
cancellationReason: null,
createdAt: "2026-06-01T00:00:00.000Z",
costKiloCredits: 20,
additive: "ignored",
},
]),
),
)
global.fetch = fn as unknown as typeof fetch
const subscriptions = await fetchCodingPlanSubscriptions("secret-token")
expect(subscriptions).toHaveLength(1)
expect(subscriptions[0]).not.toHaveProperty("additive")
const call = fn.mock.calls[0] as unknown as [string, RequestInit]
const url = new URL(call[0])
expect(url.pathname).toBe("/api/trpc/codingPlans.listSubscriptions")
expect(url.searchParams.has("batch")).toBe(false)
expect(call[1].method).toBe("GET")
expect(new Headers(call[1].headers).get("authorization")).toBe("Bearer secret-token")
expect(new Headers(call[1].headers).has("x-kilocode-organizationid")).toBe(false)
expect(call[1].redirect).toBe("error")
expect(call[1].signal).toBeInstanceOf(AbortSignal)
})
test("encodes query input", async () => {
global.fetch = mock(() => Promise.resolve(result([]))) as unknown as typeof fetch
await fetchByokEntries("token")
const call = (global.fetch as unknown as { mock: { calls: Array<[string, RequestInit]> } }).mock.calls[0]
const url = new URL(call[0])
expect(url.pathname).toBe("/api/trpc/byok.list")
expect(JSON.parse(url.searchParams.get("input") ?? "null")).toEqual({})
})
test("validates every supported procedure projection", async () => {
const payloads: Record<string, unknown> = {
"codingPlans.getUsage": {
...quota(),
additive: "stripped",
subscription: {
...quota().subscription,
windows: quota().subscription.windows.map((window, index) =>
index === 0 ? { ...window, providerPrivate: "stripped" } : window,
),
},
},
}
global.fetch = mock((input: string | URL | Request) => {
const procedure = new URL(String(input)).pathname.split("/").at(-1) ?? ""
return Promise.resolve(result(payloads[procedure]))
}) as unknown as typeof fetch
const usage = await fetchCodingPlanUsage("token", "plan")
expect(usage).toEqual(quota())
const call = (global.fetch as unknown as { mock: { calls: Array<[string]> } }).mock.calls[0]
expect(JSON.parse(new URL(call[0]).searchParams.get("input") ?? "null")).toEqual({ subscriptionId: "plan" })
})
test("decodes procedure errors even when HTTP is successful", async () => {
global.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify({ error: { json: { message: "raw private error" } } }), { status: 200 }),
),
) as unknown as typeof fetch
const error = await fetchCodingPlanSubscriptions("secret-token").catch((value) => value)
expect(error).toBeInstanceOf(CloudTrpcError)
expect(error).toMatchObject({ kind: "procedure", message: "Kilo Cloud data is temporarily unavailable." })
// Include non-enumerable Error surfaces that JSON.stringify would omit.
const surface = `${error.name} ${error.message} ${error.stack} ${JSON.stringify(error)}`
expect(surface).not.toContain("raw private error")
expect(surface).not.toContain("secret-token")
})
test("tolerates an explicit null error field in successful envelopes", async () => {
global.fetch = mock(() =>
Promise.resolve(new Response(JSON.stringify({ result: { data: { json: [] } }, error: null }))),
) as unknown as typeof fetch
await expect(fetchCodingPlanSubscriptions("token")).resolves.toEqual([])
})
test("maps malformed envelopes and schema failures safely", async () => {
global.fetch = mock(() => Promise.resolve(new Response("not-json"))) as unknown as typeof fetch
await expect(fetchCodingPlanSubscriptions("token")).rejects.toMatchObject({ kind: "protocol" })
global.fetch = mock(() => Promise.resolve(result({ status: "unknown" }))) as unknown as typeof fetch
await expect(fetchCodingPlanSubscriptions("token")).rejects.toMatchObject({ kind: "schema" })
})
test.each([
["unknown version", { schemaVersion: 2 }],
["mismatched subscription", quota("other")],
[
"duplicate windows",
{
...quota(),
subscription: {
...quota().subscription,
windows: [quota().subscription.windows[1], quota().subscription.windows[1]],
},
},
],
[
"missing period",
{
...quota(),
subscription: {
...quota().subscription,
windows: [{ ...quota().subscription.windows[1], period: undefined }],
},
},
],
])("rejects %s usage payloads", async (_description, payload) => {
global.fetch = mock(() => Promise.resolve(result(payload))) as unknown as typeof fetch
await expect(fetchCodingPlanUsage("token", "plan")).rejects.toMatchObject({ kind: "schema" })
})
})
+1
View File
@@ -32,6 +32,7 @@
"./app-icon": "./src/components/app-icon.tsx",
"./markdown": "./src/components/markdown.tsx",
"./keybind": "./src/components/keybind.tsx",
"./kilo-pass-meter": "./src/components/kilo-pass-meter.tsx",
"./popover": "./src/components/popover.tsx",
"./hover-card": "./src/components/hover-card.tsx",
"./dropdown-menu": "./src/components/dropdown-menu.tsx",
+8
View File
@@ -5,6 +5,14 @@
padding: 8px;
border: 1px solid var(--border-weak-base);
[data-slot="card-header"] {
display: flex;
align-items: flex-start;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px 12px;
}
&[data-variant="error"] {
padding: 12px;
background-color: color-mix(in srgb, var(--surface-critical-strong) 10%, var(--surface-inset-base));
+11
View File
@@ -1 +1,12 @@
import { type ComponentProps, splitProps } from "solid-js"
export * from "@opencode-ai/ui/card"
export function CardHeader(props: ComponentProps<"div">) {
const [local, rest] = splitProps(props, ["children", "class", "classList"])
return (
<div {...rest} data-slot="card-header" classList={{ ...local.classList, [local.class ?? ""]: !!local.class }}>
{local.children}
</div>
)
}
@@ -0,0 +1,130 @@
[data-component="kilo-pass-meter"] {
display: flex;
flex-direction: column;
gap: 5px;
[data-slot="kilo-pass-meter-header"] {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
color: var(--text-base);
font-size: var(--font-size-small);
}
[data-slot="kilo-pass-meter-header"] strong {
text-align: right;
font-weight: var(--font-weight-medium);
font-variant-numeric: tabular-nums;
}
[data-slot="kilo-pass-meter-track"] {
position: relative;
height: 8px;
overflow: visible;
border-radius: 4px;
background: var(--border-weak-base);
}
:is(
[data-slot="kilo-pass-meter-paid-background"],
[data-slot="kilo-pass-meter-bonus-background"],
[data-slot="kilo-pass-meter-paid-fill"],
[data-slot="kilo-pass-meter-bonus-fill"]
) {
position: absolute;
inset-block: 0;
}
[data-slot="kilo-pass-meter-paid-background"] {
left: 0;
background: color-mix(in srgb, var(--icon-warning-base) 18%, transparent);
}
[data-slot="kilo-pass-meter-bonus-background"] {
background: color-mix(in srgb, var(--icon-success-base) 18%, transparent);
}
[data-slot="kilo-pass-meter-paid-fill"] {
left: 0;
border-radius: 4px 0 0 4px;
background: var(--icon-warning-base);
}
[data-slot="kilo-pass-meter-bonus-fill"] {
border-radius: 0 4px 4px 0;
background: var(--icon-success-base);
}
[data-slot="kilo-pass-meter-boundary"] {
position: absolute;
top: 100%;
width: 2px;
height: 5px;
margin-left: -1px;
background: var(--text-weak);
}
[data-slot="kilo-pass-meter-amounts"] {
position: relative;
height: 15px;
color: var(--icon-warning-base);
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
font-variant-numeric: tabular-nums;
}
[data-slot="kilo-pass-meter-amounts"] > span {
position: absolute;
transform: translateX(-50%);
}
/* Boundary labels at the track edges stay inside the card. */
[data-slot="kilo-pass-meter-amounts"] > span[data-pin="end"] {
right: 0;
left: auto !important;
transform: none;
}
[data-slot="kilo-pass-meter-amounts"] > span[data-pin="start"] {
left: 0 !important;
transform: none;
}
[data-slot="kilo-pass-meter-bonus-amount"] {
right: 0;
left: auto !important;
color: var(--icon-success-base);
transform: none !important;
}
[data-slot="kilo-pass-meter-legend"],
[data-slot="kilo-pass-meter-legend"] > span {
display: flex;
align-items: center;
}
[data-slot="kilo-pass-meter-legend"] {
justify-content: space-between;
color: var(--text-weak);
font-size: var(--font-size-small);
}
[data-slot="kilo-pass-meter-legend"] > span {
gap: 6px;
}
[data-slot="kilo-pass-meter-legend"] i {
width: 8px;
height: 8px;
border-radius: 50%;
}
[data-slot="kilo-pass-meter-paid-dot"] {
background: var(--icon-warning-base);
}
[data-slot="kilo-pass-meter-bonus-dot"] {
background: var(--icon-success-base);
}
}
@@ -0,0 +1,105 @@
import { type ComponentProps, type JSX, splitProps } from "solid-js"
export interface KiloPassMeterProps extends Omit<ComponentProps<"div">, "children"> {
used: number
paid: number
bonus: number
label: JSX.Element
paidLabel: JSX.Element
bonusLabel: JSX.Element
format: (value: number) => string
}
export function KiloPassMeter(props: KiloPassMeterProps) {
const [local, rest] = splitProps(props, [
"used",
"paid",
"bonus",
"label",
"paidLabel",
"bonusLabel",
"format",
"class",
"classList",
])
const model = () => {
const paid = Math.max(0, local.paid)
const bonus = Math.max(0, local.bonus)
const used = Math.max(0, local.used)
const total = paid + bonus
// With no credits at all the track stays empty instead of rendering a
// full-width paid allocation for a $0 pass.
const boundary = total > 0 ? (paid / total) * 100 : 0
const filled = total > 0 ? Math.min(100, (used / total) * 100) : 0
return {
paid,
bonus,
used,
total,
boundary,
paidFill: Math.min(filled, boundary),
bonusFill: Math.max(0, filled - boundary),
}
}
return (
<div
{...rest}
data-component="kilo-pass-meter"
role="meter"
aria-valuemin={0}
aria-valuemax={Math.max(model().total, 1)}
aria-valuenow={Math.min(model().used, Math.max(model().total, 1))}
aria-valuetext={`${local.format(model().used)} / ${local.format(model().total)}`}
classList={{ ...local.classList, [local.class ?? ""]: !!local.class }}
>
<div data-slot="kilo-pass-meter-header">
<span>{local.label}</span>
<strong>
{local.format(model().used)} / {local.format(model().total)}
</strong>
</div>
<div data-slot="kilo-pass-meter-track" aria-hidden="true">
<div data-slot="kilo-pass-meter-paid-background" style={{ width: `${model().boundary}%` }} />
<div
data-slot="kilo-pass-meter-bonus-background"
hidden={model().bonus <= 0}
style={{ left: `${model().boundary}%`, width: `${100 - model().boundary}%` }}
/>
<div data-slot="kilo-pass-meter-paid-fill" style={{ width: `${model().paidFill}%` }} />
<div
data-slot="kilo-pass-meter-bonus-fill"
hidden={model().bonusFill <= 0}
style={{ left: `${model().boundary}%`, width: `${model().bonusFill}%` }}
/>
<div
data-slot="kilo-pass-meter-boundary"
hidden={model().bonus <= 0}
style={{ left: `${model().boundary}%` }}
/>
</div>
<div data-slot="kilo-pass-meter-amounts" aria-hidden="true">
<span
hidden={model().total <= 0}
data-pin={model().bonus <= 0 ? "end" : model().paid <= 0 ? "start" : undefined}
style={{ left: `${model().boundary}%` }}
>
{local.format(model().paid)}
</span>
<span data-slot="kilo-pass-meter-bonus-amount" hidden={model().bonus <= 0}>
{local.format(model().bonus)}
</span>
</div>
<div data-slot="kilo-pass-meter-legend">
<span>
<i data-slot="kilo-pass-meter-paid-dot" aria-hidden="true" />
{local.paidLabel}
</span>
<span hidden={model().bonus <= 0}>
<i data-slot="kilo-pass-meter-bonus-dot" aria-hidden="true" />
{local.bonusLabel}
</span>
</div>
</div>
)
}
+16 -1
View File
@@ -1,6 +1,7 @@
/** @jsxImportSource solid-js */
import type { Meta, StoryObj } from "storybook-solidjs-vite"
import { Card } from "@opencode-ai/ui/card"
import { Card, CardDescription, CardHeader, CardTitle } from "../components/card"
import { Tag } from "../components/tag"
const meta: Meta<typeof Card> = {
title: "Components/Card",
@@ -33,6 +34,20 @@ export const Info: Story = {
args: { variant: "info", children: "This is an info card" },
}
export const WithHeader: Story = {
render: () => (
<Card>
<CardHeader>
<div>
<CardTitle icon={false}>MiniMax</CardTitle>
<CardDescription>Token Plan Plus</CardDescription>
</div>
<Tag>Direct</Tag>
</CardHeader>
</Card>
),
}
export const AllVariants: Story = {
render: () => (
<div style={{ display: "flex", "flex-direction": "column", gap: "8px", width: "300px" }}>
@@ -0,0 +1,39 @@
/** @jsxImportSource solid-js */
import type { Meta, StoryObj } from "storybook-solidjs-vite"
import { KiloPassMeter } from "../components/kilo-pass-meter"
const meta: Meta<typeof KiloPassMeter> = {
title: "Components/Kilo Pass Meter",
component: KiloPassMeter,
decorators: [
(Story) => (
<div style={{ padding: "16px", width: "320px" }}>
<Story />
</div>
),
],
parameters: { layout: "centered" },
}
export default meta
type Story = StoryObj<typeof KiloPassMeter>
const format = (value: number) => `$${value.toFixed(2)}`
const render = (used: number, paid: number, bonus: number) => (
<KiloPassMeter
used={used}
paid={paid}
bonus={bonus}
label="This month's usage"
paidLabel="Paid"
bonusLabel="Bonus"
format={format}
aria-label="Kilo Pass monthly usage"
/>
)
export const CurrentPlan: Story = { render: () => render(73.27, 199, 99.5) }
export const UsingBonus: Story = { render: () => render(240, 199, 99.5) }
export const PaidOnly: Story = { render: () => render(73.27, 199, 0) }
export const Empty: Story = { render: () => render(0, 0, 0) }
export const OverLimit: Story = { render: () => render(325, 199, 99.5) }
+1
View File
@@ -22,6 +22,7 @@
@import "../components/error-details.css";
@import "../components/icon-button.css";
@import "../components/inline-input.css";
@import "../components/kilo-pass-meter.css";
@import "../components/list.css";
@import "../components/markdown.css";
@import "../components/message-part.css";
+70 -3
View File
@@ -4,6 +4,7 @@ import * as vscode from "vscode"
import { TRANSIENT as MEMORY_TRANSIENT } from "@kilocode/kilo-memory/schema"
import type {
KiloClient,
ProviderUsage,
Session,
SessionStatus,
Event,
@@ -369,6 +370,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private configWarningsShown = false
/** Cached notificationsLoaded payload */
private cachedNotificationsMessage: NotificationsMessage | null = null
/** Cached provider usage payload for profile view remounts and temporary disconnects. */
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 }[] = []
private readyResolvers: (() => void)[] = []
@@ -553,6 +557,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
public setProjectDirectory(directory: string | null): void {
if (this.projectDirectory === directory) return
this.projectDirectory = directory
this.providerUsageGeneration++
this.cachedProviderUsageMessage = null
this.configBindings.clear()
this.cachedConfigMessage = null
this.postMessage({ type: "workspaceDirectoryChanged", directory: directory ?? "" })
@@ -1078,6 +1084,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.handleStreamVisibilityMessage(message)
if (this.handleChildSyncMessage(message)) return
if (await this.handleMemoryMessage(message)) return
if (await this.handleProfileDataMessage(message)) return
if (this.handleLegacyMigrationMessage(message)) return
switch (message.type) {
case "webviewReady":
@@ -1193,9 +1200,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
await handleSetOrganization(this.authCtx, message.organizationId)
}
break
case "refreshProfile":
await handleRefreshProfile(this.authCtx)
break
case "openSettingsPanel":
vscode.commands.executeCommand("kilo-code.new.settingsButtonClicked", message.tab)
break
@@ -1550,6 +1554,22 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.webviewMessageDisposable = watchWorkStyleConfig((msg) => this.postMessage(msg), this.webviewMessageDisposable)
}
private async handleProfileDataMessage(message: TypedWebviewMessage): Promise<boolean> {
if (message.type === "refreshProfile") {
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
}
return false
}
private handleWebviewFocusMessage(message: TypedWebviewMessage & { focused?: unknown; target?: unknown }): void {
if (message.type === "webviewFocusChanged" && this.opts.focusContext) {
void vscode.commands.executeCommand("setContext", this.opts.focusContext, message.focused === true)
@@ -2441,6 +2461,45 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}
private async fetchAndSendProviderUsage(force = false): Promise<void> {
const generation = ++this.providerUsageGeneration
const client = this.client
if (!client) {
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) => {
console.error("[Kilo New] KiloProvider: Failed to fetch provider usage:", error)
return undefined
})
if (generation !== this.providerUsageGeneration) return
if (!result?.data) {
if (this.cachedProviderUsageMessage) {
this.postMessage(
force
? { ...this.cachedProviderUsageMessage, error: "Provider usage could not be refreshed." }
: this.cachedProviderUsageMessage,
)
return
}
this.postMessage({ type: "providerUsageLoaded", error: "Provider usage could not be loaded." })
return
}
const message = { type: "providerUsageLoaded" as const, data: result.data }
this.cachedProviderUsageMessage = message
this.postMessage(message)
}
/** Fetch providers and send to webview. Coalesced: at most one in-flight + one queued. */
private async fetchAndSendProviders(): Promise<void> {
const next = ++this.providersGeneration
@@ -4128,12 +4187,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
@@ -4277,6 +4343,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
/** Re-fetch all server-side state after an auth change. */
private async reloadAfterAuthChange(): Promise<void> {
this.invalidateProviderUsage()
await this.fetchAndSendConfig()
await Promise.all([
this.fetchAndSendProviders(),
@@ -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)
@@ -10,6 +10,9 @@ const STORIES = [
{ id: "profile--not-logged-in", name: "Profile / not logged in" },
{ id: "profile--logged-in-personal", name: "Profile / personal account" },
{ id: "profile--logged-in", name: "Profile / organization account" },
{ id: "profile--organization-context", name: "Profile / selected organization" },
{ id: "profile--stale-and-unavailable", name: "Profile / stale usage" },
{ id: "profile--empty-usage", name: "Profile / empty usage" },
{ id: "settings--providers-configure", name: "Settings / providers empty state" },
{ id: "marketplace--empty-list", name: "Marketplace / empty state" },
{ id: "agentmanager--sidebar-search-open", name: "Agent Manager / sidebar search" },
@@ -0,0 +1,209 @@
import { describe, expect, it } from "bun:test"
import type { ProviderUsage, ProviderUsageWindow } from "@kilocode/sdk/v2/client"
import { formatWindow, windowLabel, windowProgress } from "@kilocode/kilo-gateway/provider-usage"
const { KiloProvider } = await import("../../src/KiloProvider")
const data: ProviderUsage = {
generatedAt: "2026-06-19T00:00:00.000Z",
items: [],
}
type Internals = {
cachedProviderUsageMessage: unknown
fetchAndSendProviderUsage: (force?: boolean) => Promise<void>
reloadAfterAuthChange: () => Promise<void>
postMessage: (message: unknown) => void
}
type UsageClient = {
get: (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: () => benign({ kilocode: { providerUsage: usage } }) } as never,
undefined,
{ projectDirectory: "/repo" },
)
const internal = provider as unknown as Internals
internal.postMessage = (message) => messages.push(message)
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",
resource: "general",
unit: "percent",
orientation: "remaining_percent",
state: "active",
...value,
})
it("formats used and remaining orientations without provider branching", () => {
expect(formatWindow(window({ remaining: 75, limit: 100 }))).toBe("75% remaining")
expect(formatWindow(window({ orientation: "used_percent", used: 25, limit: 100 }))).toBe("25% used")
expect(windowProgress(window({ remaining: 75, limit: 100 }))).toBe(25)
})
it("keeps known zero distinct from unknown and preserves contract states", () => {
expect(formatWindow(window({ remaining: 0, limit: 100, state: "exhausted" }))).toBe("0% remaining")
expect(formatWindow(window({ state: "unknown" }))).toBe("Unknown")
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 on open and forced POST for refresh", async () => {
const get: Array<{ directory?: string }> = []
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 }
},
})
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.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(true)
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()
expect(messages).toEqual([{ type: "providerUsageLoaded", error: "Provider usage could not be loaded." }])
})
it("invalidates cached usage without reloading on auth change", async () => {
const requests: unknown[] = []
const { internal, messages } = bridge({
get: async (input) => {
requests.push(input)
return { data: { generatedAt: "a", items: [] } }
},
refresh: async () => ({ data }),
})
await internal.fetchAndSendProviderUsage()
await internal.reloadAfterAuthChange()
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({
get: async () => ({ data }),
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({
get: (input) => {
calls.push(input)
return first
},
refresh: async () => ({ data }),
})
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({
get: async (input) => {
requests.push(input)
return { data }
},
refresh: async () => ({ data }),
})
await internal.fetchAndSendProviderUsage()
provider.setProjectDirectory("/other")
expect(requests).toHaveLength(1)
expect(internal.cachedProviderUsageMessage).toBeNull()
expect(messages).toContainEqual({ type: "workspaceDirectoryChanged", directory: "/other" })
})
})
@@ -384,8 +384,13 @@ const AppContent: Component = () => {
<Match when={currentView() === "profile"}>
<ProfileView
profileData={server.profileData()}
providerUsage={server.providerUsage()}
providerUsageLoading={server.providerUsageLoading()}
providerUsageError={server.providerUsageError()}
deviceAuth={server.deviceAuth()}
onLogin={server.startLogin}
onRequestProviderUsage={server.requestProviderUsage}
onRefreshProviderUsage={server.refreshProviderUsage}
/>
</Match>
<Match when={currentView() === "settings"}>
@@ -6,31 +6,27 @@ import { Select } from "@kilocode/kilo-ui/select"
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
import { useVSCode } from "../../context/vscode"
import { useLanguage } from "../../context/language"
import { localeToBcp47, type Locale } from "../../context/language-utils"
import DeviceAuthCard from "./DeviceAuthCard"
import type { ProfileData, DeviceAuthState } from "../../types/messages"
import type { ProfileData, ProviderUsageData, DeviceAuthState } from "../../types/messages"
import { ProviderUsageCards } from "./ProviderUsageCards"
export type { ProfileData }
export interface ProfileViewProps {
profileData: ProfileData | null | undefined
deviceAuth: DeviceAuthState
providerUsage?: ProviderUsageData
providerUsageLoading?: boolean
providerUsageError?: string
onLogin: () => void
onRequestProviderUsage?: () => void
onRefreshProviderUsage?: () => void
}
const formatBalance = (amount: number): string => {
return `$${amount.toFixed(2)}`
}
const short = (amount: number): string => `$${Math.round(amount)}`
const resetLabel = (iso: string | null | undefined, loc: Locale): string | undefined => {
if (!iso) return undefined
const date = new Date(iso)
if (Number.isNaN(date.getTime())) return undefined
return new Intl.DateTimeFormat(localeToBcp47(loc), { month: "short", day: "numeric", timeZone: "UTC" }).format(date)
}
const PERSONAL = "personal"
interface OrgOption {
@@ -46,9 +42,10 @@ 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?.()
})
// Reset pending target whenever profileData changes (success or failure both send a fresh profile)
@@ -106,6 +103,10 @@ const ProfileView: Component<ProfileViewProps> = (props) => {
vscode.postMessage({ type: "openExternal", url: "https://app.kilo.ai/profile" })
}
const openExternal = (url: string) => {
vscode.postMessage({ type: "openExternal", url })
}
const handleTopUp = () => {
vscode.postMessage({ type: "openExternal", url: "https://app.kilo.ai/credits" })
}
@@ -118,8 +119,25 @@ const ProfileView: Component<ProfileViewProps> = (props) => {
vscode.postMessage({ type: "cancelLogin" })
}
const usage = () => (
<ProviderUsageCards
data={props.providerUsage}
loading={props.providerUsageLoading ?? !props.providerUsage}
error={props.providerUsageError}
onRefresh={() => props.onRefreshProviderUsage?.()}
onOpen={openExternal}
kiloPass={props.profileData?.kiloPass}
showKiloPass={
!!props.profileData &&
(props.profileData.currentOrgId ?? null) === null &&
props.profileData.profile.hasPersonalAccount !== false
}
onGetKiloPass={handleGetPass}
/>
)
return (
<div style={{ display: "flex", "flex-direction": "column", height: "100%" }}>
<div style={{ display: "flex", "flex-direction": "column", height: "100%", "min-height": 0, overflow: "hidden" }}>
<div
style={{
padding: "12px 16px",
@@ -134,7 +152,18 @@ const ProfileView: Component<ProfileViewProps> = (props) => {
</h2>
</div>
<div
style={{ padding: "16px", "max-width": "480px", margin: "0 auto", width: "100%", "box-sizing": "border-box" }}
data-profile-scroll
style={{
flex: 1,
"min-height": 0,
"overflow-y": "auto",
"overflow-x": "hidden",
padding: "16px",
"max-width": "480px",
margin: "0 auto",
width: "100%",
"box-sizing": "border-box",
}}
>
<Show
when={props.profileData}
@@ -259,118 +288,6 @@ const ProfileView: Component<ProfileViewProps> = (props) => {
</Button>
</Tooltip>
</div>
{/* Kilo Pass is part of personal credits, so only show it on the personal account */}
<Show
when={
(data().currentOrgId ?? null) === null && data().profile.hasPersonalAccount !== false
? data().kiloPass
: null
}
>
{(pass) => (
<div
style={{
"border-top": "1px solid var(--border-weak-base)",
"padding-top": "12px",
display: "flex",
"flex-direction": "column",
gap: "6px",
}}
>
<div
style={{
display: "flex",
"align-items": "baseline",
"justify-content": "space-between",
"font-size": "var(--kilo-font-size-13)",
}}
>
<span style={{ "font-weight": "600", color: "var(--vscode-foreground)" }}>Kilo Pass</span>
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
{short(pass().currentPeriodUsageUsd)} / {short(pass().currentPeriodBaseCreditsUsd)}
</span>
</div>
<div
style={{
height: "6px",
"border-radius": "3px",
background: "var(--border-weak-base)",
overflow: "hidden",
}}
>
<div
style={{
height: "100%",
width: `${Math.min(100, (pass().currentPeriodUsageUsd / Math.max(1, pass().currentPeriodBaseCreditsUsd)) * 100)}%`,
background: "var(--vscode-progressBar-background, var(--vscode-button-background))",
}}
/>
</div>
<Show when={pass().currentPeriodBonusCreditsUsd > 0}>
<div
style={{
display: "flex",
"justify-content": "space-between",
"font-size": "var(--kilo-font-size-11)",
color: "var(--vscode-descriptionForeground)",
}}
>
<span>{language.t("profile.pass.bonus")}</span>
<span>+{formatBalance(pass().currentPeriodBonusCreditsUsd)}</span>
</div>
</Show>
<Show when={resetLabel(pass().nextBillingAt, language.locale())}>
{(date) => (
<div
style={{
display: "flex",
"justify-content": "space-between",
"font-size": "var(--kilo-font-size-11)",
color: "var(--vscode-descriptionForeground)",
}}
>
<span>{language.t("profile.pass.renews")}</span>
<span>{date()}</span>
</div>
)}
</Show>
</div>
)}
</Show>
{/* No active Kilo Pass on the personal account — nudge to subscribe */}
<Show
when={
(data().currentOrgId ?? null) === null &&
data().profile.hasPersonalAccount !== false &&
!data().kiloPass
}
>
<div
style={{
"border-top": "1px solid var(--border-weak-base)",
"padding-top": "12px",
}}
>
<button
type="button"
onClick={handleGetPass}
style={{
background: "none",
border: 0,
padding: 0,
"font-size": "var(--kilo-font-size-13)",
"font-family": "inherit",
color: "var(--vscode-textLink-foreground)",
cursor: "pointer",
"text-align": "left",
}}
>
{language.t("profile.pass.subscribe")}
</button>
</div>
</Show>
</Card>
)}
</Show>
@@ -391,9 +308,13 @@ const ProfileView: Component<ProfileViewProps> = (props) => {
{language.t("profile.action.logout")}
</Button>
</div>
{usage()}
</div>
)}
</Show>
<Show when={!props.profileData}>{usage()}</Show>
</div>
</div>
)
@@ -0,0 +1,315 @@
import { Component, For, Show } from "solid-js"
import type { KiloPassState, ProviderUsageData } from "../../types/messages"
import type { ProviderUsageSnapshot } from "@kilocode/sdk/v2/client"
import { Button } from "@kilocode/kilo-ui/button"
import { Card, CardActions, CardDescription, CardHeader, CardTitle } from "@kilocode/kilo-ui/card"
import { KiloPassMeter } from "@kilocode/kilo-ui/kilo-pass-meter"
import { Progress } from "@kilocode/kilo-ui/progress"
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, windowLabel, windowProgress } from "@kilocode/kilo-gateway/provider-usage"
export interface ProviderUsageCardsProps {
data: ProviderUsageData | undefined
loading: boolean
error?: string
kiloPass?: KiloPassState | null
showKiloPass: boolean
onRefresh: () => void
onOpen: (url: string) => void
onGetKiloPass: () => void
}
type Language = ReturnType<typeof useLanguage>
const source = (item: ProviderUsageSnapshot, language: Language) => {
if (item.sourceKind === "kilo_managed") return "Kilo Gateway"
return language.t("profile.usage.source.direct")
}
const labels = (language: Language) => ({
unlimited: language.t("profile.usage.status.unlimited"),
notInPlan: language.t("profile.usage.status.notInPlan"),
unknown: language.t("profile.usage.status.unknown"),
exhausted: language.t("profile.usage.status.exhausted"),
used: (value: string) => language.t("profile.usage.window.used", { value }),
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) => {
if (item.fetchState === "error") return "error" as const
if (item.fetchState !== "ready" || item.planState === "past_due") return "warning" as const
return "normal" as const
}
const stale = (item: ProviderUsageSnapshot, language: Language) => {
const notice = `${language.t("profile.usage.state.unavailable")} ${language.t("profile.usage.state.stale")}`
if (!item.fetchedAt) return notice
const date = new Date(item.fetchedAt)
if (Number.isNaN(date.getTime())) return notice
return `${notice} (${date.toLocaleString(localeToBcp47(language.locale()))})`
}
const order = (items: ProviderUsageSnapshot[]) =>
[...items].sort(
(left, right) => Number(left.sourceKind !== "kilo_managed") - Number(right.sourceKind !== "kilo_managed"),
)
const UsageCard: Component<{
item: ProviderUsageSnapshot
onOpen: (url: string) => void
language: Language
}> = (props) => (
<Card variant={variant(props.item)}>
<CardHeader>
<div>
<CardTitle icon={false} role="heading" aria-level={4}>
{props.item.providerLabel}
</CardTitle>
<CardDescription>{props.item.planLabel}</CardDescription>
</div>
<Tag>{source(props.item, props.language)}</Tag>
</CardHeader>
<Show
when={
props.item.planState !== "active" && !(props.item.planState === "unknown" && props.item.fetchState !== "ready")
}
>
<CardDescription>
{props.language.t(
props.item.planState === "past_due"
? "profile.usage.plan.pastDue"
: props.item.planState === "canceling"
? "profile.usage.plan.canceling"
: "profile.usage.plan.unknown",
)}
</CardDescription>
</Show>
<Show when={props.item.fetchState !== "ready"}>
<p class="provider-usage-notice">
{props.item.fetchState === "stale"
? stale(props.item, props.language)
: props.language.t("profile.usage.state.unavailable")}
</p>
</Show>
<div class="provider-usage-resources">
<For each={props.item.windows}>
{(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}>
<Progress
value={progress()}
minValue={0}
maxValue={100}
showValueLabel
getValueLabel={value}
aria-label={`${title()}: ${value()}`}
aria-valuetext={value()}
>
{title()}
</Progress>
</Show>
<Show when={progress() === undefined}>
<div class="provider-usage-summary">
<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(localeToBcp47(props.language.locale())),
})}
</CardDescription>
)}
</Show>
</div>
)
}}
</For>
</div>
<Show when={props.item.routingState !== "active" && props.item.routingState !== "not_applicable"}>
<p class="provider-usage-notice">
{props.language.t("profile.usage.routing", {
state: props.language.t(`profile.usage.routingState.${props.item.routingState}`),
})}
</p>
</Show>
<Show when={props.item.managementUrl}>
{(url) => (
<CardActions>
<Button
variant="secondary"
size="small"
onClick={() => props.onOpen(url())}
aria-label={props.language.t("profile.usage.action.managePlan", { plan: props.item.planLabel })}
>
{props.language.t("profile.usage.action.manage")}
</Button>
</CardActions>
)}
</Show>
</Card>
)
const money = (value: number) => `$${value.toFixed(2)}`
const KiloPassCard: Component<{
pass?: KiloPassState | null
onGet: () => void
language: Language
}> = (props) => {
const renewal = () => {
const value = props.pass?.nextBillingAt
if (!value) return undefined
const date = new Date(value)
if (Number.isNaN(date.getTime())) return undefined
return new Intl.DateTimeFormat(localeToBcp47(props.language.locale()), {
month: "short",
day: "numeric",
timeZone: "UTC",
}).format(date)
}
return (
<Card>
<CardHeader>
<div>
<CardTitle icon={false} role="heading" aria-level={4}>
Kilo
</CardTitle>
<CardDescription>Kilo Pass</CardDescription>
</div>
<Tag>Kilo Gateway</Tag>
</CardHeader>
<Show
when={props.pass}
fallback={
<CardActions>
<Button variant="secondary" size="small" onClick={props.onGet}>
{props.language.t("profile.pass.subscribe")}
</Button>
</CardActions>
}
>
{(pass) => (
<div class="provider-usage-resources">
<KiloPassMeter
used={pass().currentPeriodUsageUsd}
paid={pass().currentPeriodBaseCreditsUsd}
bonus={pass().currentPeriodBonusCreditsUsd}
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={props.language.t("profile.pass.meter")}
/>
<Show when={renewal()}>
{(date) => (
<div class="provider-usage-summary">
<span>{props.language.t("profile.pass.renews")}</span>
<strong>{date()}</strong>
</div>
)}
</Show>
</div>
)}
</Show>
</Card>
)
}
export const ProviderUsageCards: Component<ProviderUsageCardsProps> = (props) => {
const language = useLanguage()
return (
<section class="provider-usage-section" aria-labelledby="provider-usage-title">
<div class="provider-usage-section-heading">
<div>
<h3 id="provider-usage-title">{language.t("profile.usage.title")}</h3>
<p>{language.t("profile.usage.description")}</p>
</div>
<Button
variant="ghost"
size="small"
onClick={props.onRefresh}
disabled={props.loading}
aria-label={language.t("profile.usage.refresh")}
>
{props.loading ? <Spinner style={{ width: "14px", height: "14px" }} /> : `${language.t("common.refresh")}`}
</Button>
</div>
<div class="provider-usage-list">
<Show when={props.data && props.error}>
<p class="provider-usage-notice" role="alert">
{props.error}
</p>
</Show>
<Show when={props.showKiloPass}>
<KiloPassCard pass={props.kiloPass} onGet={props.onGetKiloPass} language={language} />
</Show>
<Show
when={props.data}
fallback={
<>
<Show when={props.loading}>
<div class="provider-usage-loading" role="status" aria-label={language.t("profile.usage.title")}>
<Spinner />
</div>
</Show>
<Show when={!props.loading && props.error}>
{(error) => (
<Card variant="warning" role="alert">
<CardDescription>{error()}</CardDescription>
</Card>
)}
</Show>
</>
}
>
{(data) => (
<>
<Show
when={data().items.length > 0}
fallback={
<Show when={!props.showKiloPass}>
<Card>
<CardDescription>{language.t("profile.usage.empty")}</CardDescription>
</Card>
</Show>
}
>
<For each={order(data().items)}>
{(item) => <UsageCard item={item} onOpen={props.onOpen} language={language} />}
</For>
</Show>
</>
)}
</Show>
</div>
</section>
)
}
@@ -5,7 +5,14 @@
import { createContext, useContext, createSignal, onMount, onCleanup, ParentComponent, Accessor } from "solid-js"
import { useVSCode } from "./vscode"
import type { ConnectionState, ServerInfo, ProfileData, DeviceAuthState, ExtensionMessage } from "../types/messages"
import type {
ConnectionState,
ServerInfo,
ProfileData,
ProviderUsageData,
DeviceAuthState,
ExtensionMessage,
} from "../types/messages"
import { applyFontSize } from "../font-size"
interface ServerContextValue {
@@ -16,6 +23,11 @@ interface ServerContextValue {
errorDetails: Accessor<string | undefined>
isConnected: Accessor<boolean>
profileData: Accessor<ProfileData | null>
providerUsage: Accessor<ProviderUsageData | undefined>
providerUsageLoading: Accessor<boolean>
providerUsageError: Accessor<string | undefined>
requestProviderUsage: () => void
refreshProviderUsage: () => void
deviceAuth: Accessor<DeviceAuthState>
startLogin: () => void
goToLogin: () => void
@@ -38,6 +50,9 @@ export const ServerProvider: ParentComponent = (props) => {
const [errorMessage, setErrorMessage] = createSignal<string | undefined>()
const [errorDetails, setErrorDetails] = createSignal<string | undefined>()
const [profileData, setProfileData] = createSignal<ProfileData | null>(null)
const [providerUsage, setProviderUsage] = createSignal<ProviderUsageData>()
const [providerUsageLoading, setProviderUsageLoading] = createSignal(false)
const [providerUsageError, setProviderUsageError] = createSignal<string>()
const [deviceAuth, setDeviceAuth] = createSignal<DeviceAuthState>(initialDeviceAuth)
const [vscodeLanguage, setVscodeLanguage] = createSignal<string | undefined>()
const [languageOverride, setLanguageOverride] = createSignal<string | undefined>()
@@ -53,6 +68,30 @@ export const ServerProvider: ParentComponent = (props) => {
if (m.type === "fontSizeChanged") applyFontSize(m.fontSize)
})
const usageSub = vscode.onMessage((m: ExtensionMessage) => {
if (m.type !== "providerUsageLoaded") return
if (m.reset) {
setProviderUsage(undefined)
setProviderUsageError(undefined)
// The reset itself means previous account/project usage was invalidated:
// never show it again, and reload once via the cache-aware endpoint.
setProviderUsageLoading(true)
vscode.postMessage({ type: "requestProviderUsage" })
return
}
if (m.data) setProviderUsage(m.data)
setProviderUsageError(m.error)
setProviderUsageLoading(false)
})
const resetProviderUsageForDirectory = () => {
if (providerUsage() === undefined && !providerUsageLoading()) return
setProviderUsage(undefined)
setProviderUsageError(undefined)
setProviderUsageLoading(true)
vscode.postMessage({ type: "requestProviderUsage" })
}
onMount(() => {
const unsubscribe = vscode.onMessage((message: ExtensionMessage) => {
switch (message.type) {
@@ -76,6 +115,7 @@ export const ServerProvider: ParentComponent = (props) => {
case "workspaceDirectoryChanged":
setWorkspaceDirectory(message.directory)
resetProviderUsageForDirectory()
break
case "languageChanged":
@@ -137,6 +177,7 @@ export const ServerProvider: ParentComponent = (props) => {
onCleanup(() => {
gitSub()
fontSub()
usageSub()
unsubscribe()
})
@@ -168,6 +209,18 @@ export const ServerProvider: ParentComponent = (props) => {
startLogin()
}
const requestProviderUsage = () => {
setProviderUsageLoading(true)
setProviderUsageError(undefined)
vscode.postMessage({ type: "requestProviderUsage" })
}
const refreshProviderUsage = () => {
setProviderUsageLoading(true)
setProviderUsageError(undefined)
vscode.postMessage({ type: "refreshProviderUsage" })
}
const value: ServerContextValue = {
connectionState,
serverInfo,
@@ -176,6 +229,11 @@ export const ServerProvider: ParentComponent = (props) => {
errorDetails,
isConnected: () => connectionState() === "connected",
profileData,
providerUsage,
providerUsageLoading,
providerUsageError,
requestProviderUsage,
refreshProviderUsage,
deviceAuth,
startLogin,
goToLogin,
+39
View File
@@ -597,10 +597,49 @@ export const dict = {
"profile.action.login": "تسجيل الدخول باستخدام Kilo Code",
"profile.balance.title": "الرصيد",
"profile.balance.refresh": "تحديث الرصيد",
"profile.usage.title": "الخطط والاستخدام",
"profile.usage.description": "حصة الخطة الحالية والأرصدة",
"profile.usage.refresh": "تحديث استخدام مزودي الخدمة",
"profile.usage.empty": "لم يتم اكتشاف أي مصادر لاستخدام مزودي الخدمة.",
"profile.usage.source.direct": "مباشر",
"profile.usage.state.stale": "يتم عرض بيانات الاستخدام في آخر تحديث.",
"profile.usage.state.unavailable": "بيانات الاستخدام غير متوفرة.",
"profile.usage.plan.pastDue": "الخطة: الدفع متأخر",
"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": "مفقود",
"profile.usage.routingState.replaced": "تم استبداله",
"profile.usage.routingState.unknown": "غير معروف",
"profile.usage.window.used": "تم استخدام {{value}}",
"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": "غير محدود",
"profile.usage.status.notInPlan": "غير مشمول في الخطة",
"profile.usage.status.exhausted": "مستنفد",
"profile.action.dashboard": "لوحة التحكم",
"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": "تسجيل الخروج",
+39
View File
@@ -612,10 +612,49 @@ export const dict = {
"profile.action.login": "Entrar com Kilo Code",
"profile.balance.title": "Saldo",
"profile.balance.refresh": "Atualizar saldo",
"profile.usage.title": "Planos e uso",
"profile.usage.description": "Cota e saldos do plano atual",
"profile.usage.refresh": "Atualizar uso dos provedores",
"profile.usage.empty": "Nenhuma fonte de uso dos provedores detectada.",
"profile.usage.source.direct": "Direto",
"profile.usage.state.stale": "Exibindo os dados de uso da última atualização.",
"profile.usage.state.unavailable": "Dados de uso indisponíveis.",
"profile.usage.plan.pastDue": "Plano: Pagamento em atraso",
"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",
"profile.usage.routingState.replaced": "substituído",
"profile.usage.routingState.unknown": "desconhecido",
"profile.usage.window.used": "{{value}} usado",
"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",
"profile.usage.status.notInPlan": "Não incluído no plano",
"profile.usage.status.exhausted": "Esgotado",
"profile.action.dashboard": "Painel",
"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",
+39
View File
@@ -652,10 +652,49 @@ export const dict = {
"profile.action.login": "Prijavite se putem Kilo Code",
"profile.balance.title": "Stanje",
"profile.balance.refresh": "Osvježi stanje",
"profile.usage.title": "Planovi i korištenje",
"profile.usage.description": "Kvota i stanja trenutnog plana",
"profile.usage.refresh": "Osvježi korištenje provajdera",
"profile.usage.empty": "Nisu otkriveni izvori korištenja provajdera.",
"profile.usage.source.direct": "Direktno",
"profile.usage.state.stale": "Prikazuju se posljednji ažurirani podaci o korištenju.",
"profile.usage.state.unavailable": "Podaci o korištenju nisu dostupni.",
"profile.usage.plan.pastDue": "Plan: Plaćanje kasni",
"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",
"profile.usage.routingState.replaced": "zamijenjeno",
"profile.usage.routingState.unknown": "nepoznato",
"profile.usage.window.used": "Iskorišteno {{value}}",
"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",
"profile.usage.status.notInPlan": "Nije u planu",
"profile.usage.status.exhausted": "Iscrpljeno",
"profile.action.dashboard": "Kontrolna ploča",
"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",
+39
View File
@@ -650,10 +650,49 @@ export const dict = {
"profile.action.login": "Log ind med Kilo Code",
"profile.balance.title": "Saldo",
"profile.balance.refresh": "Opdatér saldo",
"profile.usage.title": "Abonnementer og forbrug",
"profile.usage.description": "Kvote og saldi for det aktuelle abonnement",
"profile.usage.refresh": "Opdatér udbyderforbrug",
"profile.usage.empty": "Ingen kilder til udbyderforbrug fundet.",
"profile.usage.source.direct": "Direkte",
"profile.usage.state.stale": "Viser de senest opdaterede forbrugsdata.",
"profile.usage.state.unavailable": "Forbrugsdata er ikke tilgængelige.",
"profile.usage.plan.pastDue": "Abonnement: Betaling forfalden",
"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",
"profile.usage.routingState.replaced": "erstattet",
"profile.usage.routingState.unknown": "ukendt",
"profile.usage.window.used": "{{value}} brugt",
"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",
"profile.usage.status.notInPlan": "Ikke i abonnement",
"profile.usage.status.exhausted": "Opbrugt",
"profile.action.dashboard": "Dashboard",
"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",
@@ -662,10 +662,49 @@ export const dict = {
"profile.action.login": "Mit Kilo Code anmelden",
"profile.balance.title": "Guthaben",
"profile.balance.refresh": "Guthaben aktualisieren",
"profile.usage.title": "Tarife & Nutzung",
"profile.usage.description": "Kontingent und Guthaben des aktuellen Tarifs",
"profile.usage.refresh": "Anbieternutzung aktualisieren",
"profile.usage.empty": "Keine Quellen für Anbieternutzung erkannt.",
"profile.usage.source.direct": "Direkt",
"profile.usage.state.stale": "Zuletzt aktualisierte Nutzungsdaten werden angezeigt.",
"profile.usage.state.unavailable": "Nutzungsdaten nicht verfügbar.",
"profile.usage.plan.pastDue": "Tarif: Zahlung überfällig",
"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",
"profile.usage.routingState.replaced": "ersetzt",
"profile.usage.routingState.unknown": "unbekannt",
"profile.usage.window.used": "{{value}} verwendet",
"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",
"profile.usage.status.notInPlan": "Nicht im Tarif",
"profile.usage.status.exhausted": "Aufgebraucht",
"profile.action.dashboard": "Dashboard",
"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",
@@ -561,10 +561,49 @@ export const dict = {
"profile.action.login": "Login with Kilo Code",
"profile.balance.title": "Balance",
"profile.balance.refresh": "Refresh balance",
"profile.usage.title": "Plans & usage",
"profile.usage.description": "Current plan quota and balances",
"profile.usage.refresh": "Refresh provider usage",
"profile.usage.empty": "No provider usage sources detected.",
"profile.usage.source.direct": "Direct",
"profile.usage.state.stale": "Showing last updated usage.",
"profile.usage.state.unavailable": "Usage unavailable.",
"profile.usage.plan.pastDue": "Plan: Past due",
"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",
"profile.usage.routingState.replaced": "replaced",
"profile.usage.routingState.unknown": "unknown",
"profile.usage.window.used": "{{value}} used",
"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",
"profile.usage.status.notInPlan": "Not in plan",
"profile.usage.status.exhausted": "Exhausted",
"profile.action.dashboard": "Dashboard",
"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",
+39
View File
@@ -656,10 +656,49 @@ export const dict = {
"profile.action.login": "Iniciar sesión con Kilo Code",
"profile.balance.title": "Saldo",
"profile.balance.refresh": "Actualizar saldo",
"profile.usage.title": "Planes y uso",
"profile.usage.description": "Cuota y saldos del plan actual",
"profile.usage.refresh": "Actualizar uso del proveedor",
"profile.usage.empty": "No se detectaron fuentes de uso de proveedores.",
"profile.usage.source.direct": "Directo",
"profile.usage.state.stale": "Se muestran los últimos datos de uso actualizados.",
"profile.usage.state.unavailable": "Datos de uso no disponibles.",
"profile.usage.plan.pastDue": "Plan: Pago atrasado",
"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",
"profile.usage.routingState.replaced": "reemplazado",
"profile.usage.routingState.unknown": "desconocido",
"profile.usage.window.used": "{{value}} usado",
"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",
"profile.usage.status.notInPlan": "No incluido en el plan",
"profile.usage.status.exhausted": "Agotado",
"profile.action.dashboard": "Panel",
"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",
+39
View File
@@ -564,10 +564,49 @@ export const dict = {
"profile.action.login": "ورود با Kilo Code",
"profile.balance.title": "موجودی",
"profile.balance.refresh": "بازخوانی موجودی",
"profile.usage.title": "طرح‌ها و میزان استفاده",
"profile.usage.description": "سهمیه و موجودی طرح فعلی",
"profile.usage.refresh": "بازخوانی میزان استفاده ارائه‌دهنده",
"profile.usage.empty": "هیچ منبع استفاده‌ای از ارائه‌دهنده شناسایی نشد.",
"profile.usage.source.direct": "مستقیم",
"profile.usage.state.stale": "آخرین میزان استفاده به‌روزشده نمایش داده می‌شود.",
"profile.usage.state.unavailable": "میزان استفاده در دسترس نیست.",
"profile.usage.plan.pastDue": "طرح: سررسید گذشته",
"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": "ناموجود",
"profile.usage.routingState.replaced": "جایگزین‌شده",
"profile.usage.routingState.unknown": "نامشخص",
"profile.usage.window.used": "{{value}} استفاده‌شده",
"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": "نامحدود",
"profile.usage.status.notInPlan": "در طرح نیست",
"profile.usage.status.exhausted": "تمام‌شده",
"profile.action.dashboard": "داشبورد",
"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": "خروج",
+39
View File
@@ -663,10 +663,49 @@ export const dict = {
"profile.action.login": "Se connecter avec Kilo Code",
"profile.balance.title": "Solde",
"profile.balance.refresh": "Actualiser le solde",
"profile.usage.title": "Forfaits et utilisation",
"profile.usage.description": "Quota et soldes du forfait actuel",
"profile.usage.refresh": "Actualiser l'utilisation du fournisseur",
"profile.usage.empty": "Aucune source d'utilisation de fournisseur détectée.",
"profile.usage.source.direct": "Direct",
"profile.usage.state.stale": "Affichage des dernières données d'utilisation mises à jour.",
"profile.usage.state.unavailable": "Données d'utilisation indisponibles.",
"profile.usage.plan.pastDue": "Forfait : paiement en retard",
"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",
"profile.usage.routingState.replaced": "remplacé",
"profile.usage.routingState.unknown": "inconnu",
"profile.usage.window.used": "{{value}} utilisé",
"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é",
"profile.usage.status.notInPlan": "Non inclus dans le forfait",
"profile.usage.status.exhausted": "Épuisé",
"profile.action.dashboard": "Tableau de bord",
"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",
+39
View File
@@ -491,10 +491,49 @@ export const dict = {
"profile.action.login": "Accedi con Kilo Code",
"profile.balance.title": "Saldo",
"profile.balance.refresh": "Aggiorna saldo",
"profile.usage.title": "Piani e utilizzo",
"profile.usage.description": "Quota e saldi del piano attuale",
"profile.usage.refresh": "Aggiorna l'utilizzo dei provider",
"profile.usage.empty": "Non è stata rilevata alcuna fonte di utilizzo dei provider.",
"profile.usage.source.direct": "Diretto",
"profile.usage.state.stale": "Vengono mostrati i dati di utilizzo dell'ultimo aggiornamento.",
"profile.usage.state.unavailable": "Dati di utilizzo non disponibili.",
"profile.usage.plan.pastDue": "Piano: Pagamento scaduto",
"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",
"profile.usage.routingState.replaced": "sostituito",
"profile.usage.routingState.unknown": "sconosciuto",
"profile.usage.window.used": "{{value}} utilizzato",
"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",
"profile.usage.status.notInPlan": "Non incluso nel piano",
"profile.usage.status.exhausted": "Esaurito",
"profile.action.dashboard": "Dashboard",
"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",
+39
View File
@@ -645,10 +645,49 @@ export const dict = {
"profile.action.login": "Kilo Codeでログイン",
"profile.balance.title": "残高",
"profile.balance.refresh": "残高を更新",
"profile.usage.title": "プランと使用状況",
"profile.usage.description": "現在のプランの利用枠と残高",
"profile.usage.refresh": "プロバイダーの使用状況を更新",
"profile.usage.empty": "プロバイダー使用量の取得元が検出されませんでした。",
"profile.usage.source.direct": "直接",
"profile.usage.state.stale": "最後に更新された使用状況を表示しています。",
"profile.usage.state.unavailable": "使用状況を取得できません。",
"profile.usage.plan.pastDue": "プラン:支払い期限切れ",
"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": "欠落",
"profile.usage.routingState.replaced": "置換済み",
"profile.usage.routingState.unknown": "不明",
"profile.usage.window.used": "{{value}} 使用済み",
"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": "無制限",
"profile.usage.status.notInPlan": "プラン対象外",
"profile.usage.status.exhausted": "使い切り",
"profile.action.dashboard": "ダッシュボード",
"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": "ログアウト",
+39
View File
@@ -605,10 +605,49 @@ export const dict = {
"profile.action.login": "Kilo Code로 로그인",
"profile.balance.title": "잔액",
"profile.balance.refresh": "잔액 새로고침",
"profile.usage.title": "요금제 및 사용량",
"profile.usage.description": "현재 요금제 할당량 및 잔액",
"profile.usage.refresh": "공급자 사용량 새로고침",
"profile.usage.empty": "감지된 공급자 사용량 소스가 없습니다.",
"profile.usage.source.direct": "직접",
"profile.usage.state.stale": "마지막으로 업데이트된 사용량을 표시합니다.",
"profile.usage.state.unavailable": "사용량을 확인할 수 없습니다.",
"profile.usage.plan.pastDue": "요금제: 결제 기한 지남",
"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": "누락된 상태",
"profile.usage.routingState.replaced": "대체된 상태",
"profile.usage.routingState.unknown": "알 수 없는 상태",
"profile.usage.window.used": "{{value}} 사용됨",
"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": "무제한",
"profile.usage.status.notInPlan": "요금제에 포함되지 않음",
"profile.usage.status.exhausted": "소진됨",
"profile.action.dashboard": "대시보드",
"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": "로그아웃",
+39
View File
@@ -604,10 +604,49 @@ export const dict = {
"profile.action.login": "Inloggen met Kilo Code",
"profile.balance.title": "Saldo",
"profile.balance.refresh": "Saldo vernieuwen",
"profile.usage.title": "Abonnementen en gebruik",
"profile.usage.description": "Quota en saldi van het huidige abonnement",
"profile.usage.refresh": "Providergebruik vernieuwen",
"profile.usage.empty": "Geen bronnen voor providergebruik gedetecteerd.",
"profile.usage.source.direct": "Direct",
"profile.usage.state.stale": "De laatst bijgewerkte gebruiksgegevens worden weergegeven.",
"profile.usage.state.unavailable": "Gebruiksgegevens niet beschikbaar.",
"profile.usage.plan.pastDue": "Abonnement: Betaling achterstallig",
"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",
"profile.usage.routingState.replaced": "vervangen",
"profile.usage.routingState.unknown": "onbekend",
"profile.usage.window.used": "{{value}} gebruikt",
"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",
"profile.usage.status.notInPlan": "Niet in abonnement",
"profile.usage.status.exhausted": "Opgebruikt",
"profile.action.dashboard": "Dashboard",
"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",
+39
View File
@@ -612,10 +612,49 @@ export const dict = {
"profile.action.login": "Logg inn med Kilo Code",
"profile.balance.title": "Saldo",
"profile.balance.refresh": "Oppdater saldo",
"profile.usage.title": "Abonnementer og forbruk",
"profile.usage.description": "Kvote og saldoer for gjeldende abonnement",
"profile.usage.refresh": "Oppdater leverandørforbruk",
"profile.usage.empty": "Ingen kilder til leverandørforbruk oppdaget.",
"profile.usage.source.direct": "Direkte",
"profile.usage.state.stale": "Viser sist oppdaterte forbruksdata.",
"profile.usage.state.unavailable": "Forbruksdata er utilgjengelige.",
"profile.usage.plan.pastDue": "Abonnement: Betaling forfalt",
"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",
"profile.usage.routingState.replaced": "erstattet",
"profile.usage.routingState.unknown": "ukjent",
"profile.usage.window.used": "{{value}} brukt",
"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",
"profile.usage.status.notInPlan": "Ikke i abonnement",
"profile.usage.status.exhausted": "Oppbrukt",
"profile.action.dashboard": "Kontrollpanel",
"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",
+39
View File
@@ -608,10 +608,49 @@ export const dict = {
"profile.action.login": "Zaloguj się przez Kilo Code",
"profile.balance.title": "Saldo",
"profile.balance.refresh": "Odśwież saldo",
"profile.usage.title": "Plany i wykorzystanie",
"profile.usage.description": "Limit i salda bieżącego planu",
"profile.usage.refresh": "Odśwież wykorzystanie dostawców",
"profile.usage.empty": "Nie wykryto źródeł wykorzystania dostawców.",
"profile.usage.source.direct": "Bezpośrednio",
"profile.usage.state.stale": "Wyświetlane są ostatnio zaktualizowane dane o wykorzystaniu.",
"profile.usage.state.unavailable": "Dane o wykorzystaniu są niedostępne.",
"profile.usage.plan.pastDue": "Plan: Zaległa płatność",
"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",
"profile.usage.routingState.replaced": "zastąpiony",
"profile.usage.routingState.unknown": "nieznany",
"profile.usage.window.used": "Wykorzystano {{value}}",
"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",
"profile.usage.status.notInPlan": "Poza planem",
"profile.usage.status.exhausted": "Wyczerpano",
"profile.action.dashboard": "Panel",
"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ę",
+39
View File
@@ -649,10 +649,49 @@ export const dict = {
"profile.action.login": "Войти через Kilo Code",
"profile.balance.title": "Баланс",
"profile.balance.refresh": "Обновить баланс",
"profile.usage.title": "Тарифы и использование",
"profile.usage.description": "Квота и балансы текущего тарифа",
"profile.usage.refresh": "Обновить данные об использовании провайдеров",
"profile.usage.empty": "Источники данных об использовании провайдеров не обнаружены.",
"profile.usage.source.direct": "Напрямую",
"profile.usage.state.stale": "Показаны последние обновлённые данные об использовании.",
"profile.usage.state.unavailable": "Данные об использовании недоступны.",
"profile.usage.plan.pastDue": "Тариф: Платёж просрочен",
"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": "отсутствует",
"profile.usage.routingState.replaced": "заменена",
"profile.usage.routingState.unknown": "неизвестна",
"profile.usage.window.used": "Использовано {{value}}",
"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": "Без ограничений",
"profile.usage.status.notInPlan": "Не входит в тариф",
"profile.usage.status.exhausted": "Исчерпано",
"profile.action.dashboard": "Панель управления",
"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": "Выйти",
+39
View File
@@ -642,10 +642,49 @@ export const dict = {
"profile.action.login": "เข้าสู่ระบบด้วย Kilo Code",
"profile.balance.title": "ยอดคงเหลือ",
"profile.balance.refresh": "รีเฟรชยอดคงเหลือ",
"profile.usage.title": "แผนและการใช้งาน",
"profile.usage.description": "โควตาและยอดคงเหลือของแผนปัจจุบัน",
"profile.usage.refresh": "รีเฟรชการใช้งานของผู้ให้บริการ",
"profile.usage.empty": "ไม่พบแหล่งข้อมูลการใช้งานของผู้ให้บริการ",
"profile.usage.source.direct": "โดยตรง",
"profile.usage.state.stale": "กำลังแสดงข้อมูลการใช้งานที่อัปเดตล่าสุด",
"profile.usage.state.unavailable": "ไม่มีข้อมูลการใช้งาน",
"profile.usage.plan.pastDue": "แผน: ค้างชำระ",
"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": "ขาดหาย",
"profile.usage.routingState.replaced": "ถูกแทนที่",
"profile.usage.routingState.unknown": "ไม่ทราบ",
"profile.usage.window.used": "ใช้ไป {{value}}",
"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": "ไม่จำกัด",
"profile.usage.status.notInPlan": "ไม่รวมอยู่ในแผน",
"profile.usage.status.exhausted": "ใช้หมดแล้ว",
"profile.action.dashboard": "แดชบอร์ด",
"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": "ออกจากระบบ",
+39
View File
@@ -601,10 +601,49 @@ export const dict = {
"profile.action.login": "Kilo Code ile giriş yap",
"profile.balance.title": "Bakiye",
"profile.balance.refresh": "Bakiyeyi yenile",
"profile.usage.title": "Planlar ve kullanım",
"profile.usage.description": "Mevcut plan kotası ve bakiyeleri",
"profile.usage.refresh": "Sağlayıcı kullanımını yenile",
"profile.usage.empty": "Hiçbir sağlayıcı kullanım kaynağı algılanmadı.",
"profile.usage.source.direct": "Doğrudan",
"profile.usage.state.stale": "Son güncellenen kullanım verileri gösteriliyor.",
"profile.usage.state.unavailable": "Kullanım verileri kullanılamıyor.",
"profile.usage.plan.pastDue": "Plan: Ödeme gecikmiş",
"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",
"profile.usage.routingState.replaced": "değiştirildi",
"profile.usage.routingState.unknown": "bilinmiyor",
"profile.usage.window.used": "{{value}} kullanıldı",
"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",
"profile.usage.status.notInPlan": "Plana dahil değil",
"profile.usage.status.exhausted": "Tükendi",
"profile.action.dashboard": "Kontrol Paneli",
"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",
+39
View File
@@ -602,10 +602,49 @@ export const dict = {
"profile.action.login": "Увійти через Kilo Code",
"profile.balance.title": "Баланс",
"profile.balance.refresh": "Оновити баланс",
"profile.usage.title": "Плани та використання",
"profile.usage.description": "Квота й баланси поточного плану",
"profile.usage.refresh": "Оновити дані про використання провайдерів",
"profile.usage.empty": "Джерел даних про використання провайдерів не виявлено.",
"profile.usage.source.direct": "Напряму",
"profile.usage.state.stale": "Показано останні оновлені дані про використання.",
"profile.usage.state.unavailable": "Дані про використання недоступні.",
"profile.usage.plan.pastDue": "План: Платіж прострочено",
"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": "відсутня",
"profile.usage.routingState.replaced": "замінена",
"profile.usage.routingState.unknown": "невідома",
"profile.usage.window.used": "Використано {{value}}",
"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": "Без обмежень",
"profile.usage.status.notInPlan": "Не входить до плану",
"profile.usage.status.exhausted": "Вичерпано",
"profile.action.dashboard": "Панель керування",
"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": "Вийти",
+39
View File
@@ -626,10 +626,49 @@ export const dict = {
"profile.action.login": "使用 Kilo Code 登录",
"profile.balance.title": "余额",
"profile.balance.refresh": "刷新余额",
"profile.usage.title": "套餐与用量",
"profile.usage.description": "当前套餐额度和余额",
"profile.usage.refresh": "刷新提供商用量",
"profile.usage.empty": "未检测到提供商用量来源。",
"profile.usage.source.direct": "直接",
"profile.usage.state.stale": "正在显示上次更新的用量。",
"profile.usage.state.unavailable": "用量数据不可用。",
"profile.usage.plan.pastDue": "套餐:付款逾期",
"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": "缺失",
"profile.usage.routingState.replaced": "已替换",
"profile.usage.routingState.unknown": "未知",
"profile.usage.window.used": "已使用 {{value}}",
"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": "无限制",
"profile.usage.status.notInPlan": "不在套餐内",
"profile.usage.status.exhausted": "已用尽",
"profile.action.dashboard": "控制面板",
"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": "退出登录",
+39
View File
@@ -586,10 +586,49 @@ export const dict = {
"profile.action.login": "使用 Kilo Code 登入",
"profile.balance.title": "餘額",
"profile.balance.refresh": "重新整理餘額",
"profile.usage.title": "方案與用量",
"profile.usage.description": "目前方案配額和餘額",
"profile.usage.refresh": "重新整理供應商用量",
"profile.usage.empty": "未偵測到供應商用量來源。",
"profile.usage.source.direct": "直接",
"profile.usage.state.stale": "正在顯示上次更新的用量。",
"profile.usage.state.unavailable": "無法取得用量資料。",
"profile.usage.plan.pastDue": "方案:付款逾期",
"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": "缺失",
"profile.usage.routingState.replaced": "已取代",
"profile.usage.routingState.unknown": "未知",
"profile.usage.window.used": "已使用 {{value}}",
"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": "無限制",
"profile.usage.status.notInPlan": "不在方案內",
"profile.usage.status.exhausted": "已用盡",
"profile.action.dashboard": "控制面板",
"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": "登出",
@@ -229,6 +229,11 @@ const chatServer = {
errorDetails: () => undefined,
isConnected: () => true,
profileData: () => null,
providerUsage: () => undefined,
providerUsageLoading: () => false,
providerUsageError: () => undefined,
requestProviderUsage: () => undefined,
refreshProviderUsage: () => undefined,
deviceAuth: () => ({ status: "idle" as const }),
startLogin: () => undefined,
goToLogin: () => undefined,
@@ -6,7 +6,7 @@
import type { Meta, StoryObj } from "storybook-solidjs-vite"
import { StoryProviders } from "./StoryProviders"
import ProfileView from "../components/profile/ProfileView"
import type { ProfileData, DeviceAuthState } from "../types/messages"
import type { ProfileData, ProviderUsageData, DeviceAuthState } from "../types/messages"
const meta: Meta = {
title: "Profile",
@@ -46,37 +46,127 @@ const personalProfile: ProfileData = {
const idleAuth: DeviceAuthState = { status: "idle" }
const usage: ProviderUsageData = {
generatedAt: "2026-06-19T12:00:00.000Z",
items: [
{
id: "kilo-managed:plan",
providerID: "minimax",
sourceKind: "kilo_managed",
providerLabel: "MiniMax",
planLabel: "Token Plan Plus",
sourceLabel: "via Kilo",
fetchState: "ready",
planState: "active",
routingState: "active",
fetchedAt: "2026-06-19T12:00:00.000Z",
managementUrl: "https://app.kilo.ai/subscriptions/coding-plans/plan",
windows: [
{
id: "general-interval",
resource: "general",
period: { unit: "hour", value: 5 },
unit: "percent",
orientation: "remaining_percent",
used: 24,
remaining: 76,
limit: 100,
state: "active",
},
],
},
],
}
const directUsage: ProviderUsageData = {
generatedAt: usage.generatedAt,
items: [
{
...usage.items[0],
id: "minimax-direct-global",
providerID: "minimax-coding-plan",
sourceKind: "direct",
sourceLabel: "MiniMax Global",
routingState: "not_applicable",
managementUrl: "https://platform.minimax.io/subscribe/token-plan",
},
],
}
const noop = () => {}
const render = (profileData: ProfileData | null, providerUsage: ProviderUsageData, height: number, error?: string) => (
<StoryProviders noPadding>
<div style={{ width: "420px", height: `${height}px` }}>
<ProfileView
profileData={profileData}
providerUsage={providerUsage}
providerUsageError={error}
deviceAuth={idleAuth}
onLogin={noop}
/>
</div>
</StoryProviders>
)
export const LoggedIn: Story = {
name: "ProfileView — logged in with orgs",
render: () => (
<StoryProviders noPadding>
<div style={{ width: "420px", height: "500px" }}>
<ProfileView profileData={loggedInProfile} deviceAuth={idleAuth} onLogin={noop} />
</div>
</StoryProviders>
),
render: () => render(loggedInProfile, usage, 900),
}
export const LoggedInPersonal: Story = {
name: "ProfileView — personal account",
render: () => (
<StoryProviders noPadding>
<div style={{ width: "420px", height: "400px" }}>
<ProfileView profileData={personalProfile} deviceAuth={idleAuth} onLogin={noop} />
</div>
</StoryProviders>
),
render: () => render(personalProfile, usage, 900),
}
export const ScrollableUsage: Story = {
name: "ProfileView — scrollable usage",
render: () => render(personalProfile, usage, 480),
play: (context: { canvasElement: HTMLElement }) => {
const pane = context.canvasElement.querySelector<HTMLElement>("[data-profile-scroll]")
if (pane) pane.scrollTop = pane.scrollHeight
},
}
export const NotLoggedIn: Story = {
name: "ProfileView — not logged in",
render: () => (
<StoryProviders noPadding>
<div style={{ width: "420px", height: "300px" }}>
<ProfileView profileData={null} deviceAuth={idleAuth} onLogin={noop} />
</div>
</StoryProviders>
),
render: () => render(null, directUsage, 620),
}
export const OrganizationContext: Story = {
name: "ProfileView — organization context",
render: () =>
render({ ...loggedInProfile, currentOrgId: "org-1" }, { generatedAt: usage.generatedAt, items: [] }, 620),
}
export const StaleAndUnavailable: Story = {
name: "ProfileView — stale and unavailable usage",
render: () =>
render(
personalProfile,
{
generatedAt: usage.generatedAt,
items: [
{
...directUsage.items[0],
fetchState: "stale",
planState: "unknown",
error: { code: "timeout", message: "The latest usage could not be loaded.", retryable: true },
},
{
...usage.items[0],
id: "managed-unavailable",
fetchState: "unavailable",
windows: [],
error: { code: "upstream", message: "Usage unavailable.", retryable: true },
},
],
},
760,
"Provider usage could not be refreshed.",
),
}
export const EmptyUsage: Story = {
name: "ProfileView — no usage sources",
render: () => render(personalProfile, { generatedAt: usage.generatedAt, items: [] }, 480),
}
@@ -26,4 +26,5 @@
@import "./plan-exit.css";
@import "./suggest-bar.css";
@import "./settings.css";
@import "./provider-usage.css";
@import "./high-contrast.css";
@@ -0,0 +1,82 @@
/* Provider Usage */
.provider-usage-section {
margin-top: 24px;
padding-top: 18px;
border-top: 1px solid var(--border-weak-base);
}
.provider-usage-section-heading,
.provider-usage-summary {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.provider-usage-section-heading {
margin-bottom: 12px;
/* Optically align with the text inside the cards (1px border + 8px card padding) */
padding: 0 9px;
}
.provider-usage-section-heading h3 {
margin: 0;
color: var(--text-base);
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
}
.provider-usage-section-heading p,
.provider-usage-notice {
margin: 3px 0 0;
color: var(--text-weak);
font-size: var(--font-size-small);
line-height: var(--line-height-large);
}
.provider-usage-list,
.provider-usage-resources {
display: flex;
flex-direction: column;
gap: 10px;
}
.provider-usage-row {
display: grid;
gap: 4px;
}
.provider-usage-summary {
align-items: baseline;
color: var(--text-base);
font-size: var(--font-size-small);
}
.provider-usage-summary strong {
text-align: right;
font-weight: var(--font-weight-medium);
font-variant-numeric: tabular-nums;
}
.provider-usage-notice {
padding-left: 8px;
border-left: 2px solid var(--icon-warning-base);
}
/* Align the action button label with the card content; small buttons have 12px inline padding */
.provider-usage-list [data-slot="card-actions"] {
margin-left: -12px;
}
.provider-usage-loading {
display: grid;
min-height: 92px;
place-items: center;
}
@media (max-width: 360px) {
.provider-usage-section-heading {
align-items: stretch;
flex-direction: column;
}
}
@@ -54,6 +54,7 @@ import type {
} from "./config"
import type { WorkStyle, WorkStyleState } from "../../../../src/shared/work-style-presets"
import type { KilocodeNotification, ProfileData } from "./profile"
import type { ProviderUsageLoadedMessage } from "./provider-usage"
import type {
AgentManagerApplyWorktreeDiffConflict,
AgentManagerApplyWorktreeDiffStatus,
@@ -1400,6 +1401,7 @@ export type ExtensionMessage =
| GitRemoteUrlLoadedMessage
| ActionMessage
| ProfileDataMessage
| ProviderUsageLoadedMessage
| DeviceAuthStartedMessage
| DeviceAuthCompleteMessage
| DeviceAuthFailedMessage
@@ -11,6 +11,7 @@ export * from "./providers"
export * from "./agents"
export * from "./config"
export * from "./profile"
export * from "./provider-usage"
export * from "./agent-manager"
export * from "./migration"
export * from "./memory"
@@ -0,0 +1,18 @@
import type { ProviderUsage } from "@kilocode/sdk/v2/client"
export type ProviderUsageData = ProviderUsage
export interface ProviderUsageLoadedMessage {
type: "providerUsageLoaded"
data?: ProviderUsageData
error?: string
reset?: boolean
}
export interface RequestProviderUsageMessage {
type: "requestProviderUsage"
}
export interface RefreshProviderUsageMessage {
type: "refreshProviderUsage"
}
@@ -7,6 +7,7 @@ import type { Config } from "./config"
import type { ModelAllocation, ReviewCommentEntry, 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, RequestProviderUsageMessage } from "./provider-usage"
import type { AnacondaDesktopWebviewMessage } from "../../../../src/shared/anaconda-desktop-messages"
import type {
ClearLegacyDataMessage,
@@ -1478,6 +1479,8 @@ export type WebviewMessage =
| LoginRequest
| LogoutRequest
| RefreshProfileRequest
| RequestProviderUsageMessage
| RefreshProviderUsageMessage
| OpenExternalRequest
| OpenSettingsPanelRequest
| OpenProfilePanelRequest
@@ -0,0 +1,122 @@
import { TextAttributes } from "@opentui/core"
import { useKeyboard } from "@opentui/solid"
import type { ProviderUsage, ProviderUsageSnapshot } from "@kilocode/sdk/v2"
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"
import { Link } from "@tui/ui/link"
import { Spinner } from "@tui/component/spinner"
import { For, Show, createSignal, onMount } from "solid-js"
function Item(props: { item: ProviderUsageSnapshot }) {
const { theme } = useTheme()
return (
<box border={true} borderColor={theme.border} paddingLeft={1} paddingRight={1} marginBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text} attributes={TextAttributes.BOLD}>
{props.item.providerLabel} - {props.item.planLabel}
</text>
<text fg={theme.textMuted}>{props.item.sourceLabel}</text>
</box>
<text fg={props.item.fetchState === "ready" ? theme.textMuted : theme.warning}>
{props.item.fetchState === "ready" ? props.item.planState : props.item.fetchState}
</text>
<For each={props.item.windows}>
{(window) => (
<box>
<text fg={theme.text}>
{windowLabel(window)}: {formatWindow(window)}
</text>
<Show when={window.resetAt}>
{(reset) => <text fg={theme.textMuted}>Resets {new Date(reset()).toLocaleString()}</text>}
</Show>
</box>
)}
</For>
<Show when={props.item.routingState !== "not_applicable" && props.item.routingState !== "active"}>
<text fg={theme.warning}>Routing: {props.item.routingState}</text>
</Show>
<Show when={props.item.error}>{(error) => <text fg={theme.warning}>{error().message}</text>}</Show>
<Show when={props.item.managementUrl}>
{(url) => (
<box flexDirection="row">
<text fg={theme.textMuted}>Manage: </text>
<Link href={url()} fg={theme.primary}>
{url()}
</Link>
</box>
)}
</Show>
</box>
)
}
function ProviderUsageBody(props: { data: ProviderUsage }) {
const { theme } = useTheme()
return (
<box>
<Show
when={props.data.items.length > 0}
fallback={<text fg={theme.textMuted}>No provider usage sources detected.</text>}
>
<For each={props.data.items}>{(item) => <Item item={item} />}</For>
</Show>
</box>
)
}
export function DialogProviderUsage() {
const dialog = useDialog()
const { theme } = useTheme()
const sdk = useSDK()
const [data, setData] = createSignal<ProviderUsage>()
const [loading, setLoading] = createSignal(false)
const [failure, setFailure] = createSignal<string>()
async function load(force: boolean) {
if (loading()) return
setLoading(true)
setFailure(undefined)
const response = await (force
? sdk.client.kilocode.providerUsage.refresh().catch(() => undefined)
: sdk.client.kilocode.providerUsage.get().catch(() => undefined))
if (response?.data) setData(response.data)
if (response?.error || !response?.data) setFailure("Provider usage could not be loaded.")
setLoading(false)
}
onMount(() => {
dialog.setSize("xlarge")
void load(false)
})
useKeyboard((event) => {
if (event.ctrl && event.name === "r") void load(true)
})
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text} attributes={TextAttributes.BOLD}>
Plans & usage
</text>
<text fg={theme.textMuted}>esc</text>
</box>
<scrollbox maxHeight={24} flexGrow={1}>
<box>
<Show when={data()}>{(value) => <ProviderUsageBody data={value()} />}</Show>
<Show when={loading() && !data()}>
<Spinner />
</Show>
<Show when={failure()}>{(message) => <text fg={theme.warning}>{message()}</text>}</Show>
</box>
</scrollbox>
<box flexDirection="row" justifyContent="flex-end" gap={2}>
<text fg={loading() ? theme.textMuted : theme.primary} onMouseUp={() => !loading() && void load(true)}>
refresh ctrl+r
</text>
</box>
</box>
)
}
@@ -20,6 +20,7 @@ import { DialogKiloProfile } from "./components/dialog-kilo-profile.js"
import { DialogClawSetup } from "./components/dialog-claw-setup.js"
import { DialogClawUpgrade } from "./components/dialog-claw-upgrade.js"
import { DialogIndexing } from "./components/dialog-indexing.js"
import { DialogProviderUsage } from "./components/dialog-provider-usage.js"
import { indexingEnabled } from "./indexing-feature"
import { refreshBalance } from "./balance-refresh"
@@ -127,6 +128,18 @@ export function registerKiloCommands(useSDK: () => UseSDK) {
},
},
{
name: "kilo.usage",
title: "Plans & usage",
desc: "View provider plans and quota",
category: "Kilo",
slashName: "usage",
slashAliases: ["plans", "quota"],
run: () => {
dialog.replace(() => <DialogProviderUsage />)
},
},
// /profile command
{
name: "kilo.profile",
@@ -8,6 +8,7 @@ import {
WorkspaceRoutingQueryFields,
} from "@/server/routes/instance/httpapi/middleware/workspace-routing"
import { described } from "@/server/routes/instance/httpapi/groups/metadata"
import { ProviderUsage } from "@opencode-ai/schema/kilocode/provider-usage"
import { AnacondaDesktopApi } from "./anaconda-desktop"
import {
Failure as AgentManagerFailure,
@@ -68,6 +69,8 @@ export const KilocodePaths = {
removeCommand: `${root}/command/remove`,
removeSkill: `${root}/skill/remove`,
removeAgent: `${root}/agent/remove`,
providerUsage: `${root}/provider-usage`,
providerUsageRefresh: `${root}/provider-usage/refresh`,
notebookList: `${root}/notebook`,
notebookReply: `${root}/notebook/:requestID/reply`,
notebookReject: `${root}/notebook/:requestID/reject`,
@@ -141,6 +144,28 @@ export const KilocodeApi = HttpApi.make("kilocode")
"Remove a custom (non-native) agent from one writable configuration scope, or every writable scope when omitted, and dispose cached instance state.",
}),
),
HttpApiEndpoint.get("providerUsage", KilocodePaths.providerUsage, {
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.",
}),
),
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.",
}),
),
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"
@@ -13,6 +14,10 @@ import { AgentManager } from "@/kilocode/agent-manager/service"
import type { RequestID as NotebookRequestID } from "@/kilocode/notebook/protocol"
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 { InvalidRequestError } from "@/server/routes/instance/httpapi/errors"
@@ -43,6 +48,21 @@ export const kilocodeHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilocode"
const notebook = yield* Notebook.Service
const background = yield* BackgroundJob.Service
const runState = yield* SessionRunState.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())
@@ -117,6 +137,18 @@ export const kilocodeHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilocode"
return true
})
const providerUsage = Effect.fn("KilocodeHttpApi.providerUsage")(function* () {
return yield* located(ProviderUsage.Service.use((usage) => usage.get())).pipe(
Effect.mapError(() => new HttpApiError.ServiceUnavailable({})),
)
})
const providerUsageRefresh = Effect.fn("KilocodeHttpApi.providerUsageRefresh")(function* () {
return yield* located(ProviderUsage.Service.use((usage) => usage.refresh())).pipe(
Effect.mapError(() => new HttpApiError.ServiceUnavailable({})),
)
})
const notebookList = Effect.fn("KilocodeHttpApi.notebookList")(function* () {
return yield* notebook.list()
})
@@ -208,6 +240,8 @@ export const kilocodeHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilocode"
.handle("removeCommand", removeCommand)
.handle("removeSkill", removeSkill)
.handle("removeAgent", removeAgent)
.handle("providerUsage", providerUsage)
.handle("providerUsageRefresh", providerUsageRefresh)
.handle("notebookList", notebookList)
.handle("notebookReply", notebookReply)
.handle("notebookReject", notebookReject)
@@ -664,6 +664,20 @@ export const kiloScenarios: Scenario[] = [
check(!(yield* Effect.promise(() => Bun.file(location).exists())), "removed agent should not remain on disk")
}),
),
http.protected
.get("/kilocode/provider-usage", "kilocode.providerUsage.get")
.inProject({ git: true })
.json(200, (body) => {
object(body)
array(body.items)
}),
http.protected
.post("/kilocode/provider-usage/refresh", "kilocode.providerUsage.refresh")
.inProject({ git: true })
.json(200, (body) => {
object(body)
array(body.items)
}),
http.protected
.post("/kilocode/agent/remove", "kilocode.removeAgent.duplicates")
.inProject({ git: true, init: duplicates })
@@ -152,6 +152,8 @@ describe("Kilo PublicApi OpenAPI contract", () => {
{ method: "get", path: ConfigConsolePaths.tuiConfig },
{ method: "get", path: ConfigConsolePaths.tuiKeybinds },
{ method: "patch", path: ConfigConsolePaths.tuiConfig },
{ method: "get", path: KilocodePaths.providerUsage },
{ method: "post", path: KilocodePaths.providerUsageRefresh },
{ method: "get", path: KilocodePaths.sessionModelUsage },
{ method: "post", path: BranchNamePaths.generate },
{ method: "get", path: MemoryPaths.status },
@@ -238,6 +240,39 @@ describe("Kilo PublicApi OpenAPI contract", () => {
expect(schema?.properties?.prompt).toEqual({ type: "string" })
})
test("keeps provider usage flat and credential-free", () => {
const spec = OpenApi.fromApi(PublicApi)
const schemas = (spec.components?.schemas ?? {}) as Record<string, Schema>
const usage = Object.fromEntries(Object.entries(schemas).filter(([name]) => name.startsWith("ProviderUsage")))
const keys = (value: unknown): string[] => {
if (Array.isArray(value)) return value.flatMap(keys)
if (!value || typeof value !== "object") return []
return Object.entries(value).flatMap(([key, item]) => [key, ...keys(item)])
}
const fields = keys(usage).map((key) => key.toLowerCase())
expect(spec.paths[KilocodePaths.providerUsage]?.get?.responses?.["200"]).toBeDefined()
expect(spec.paths[KilocodePaths.providerUsageRefresh]?.post?.responses?.["200"]).toBeDefined()
expect(schemas.ProviderUsageSnapshot?.properties?.windows).toBeDefined()
for (const forbidden of [
"key",
"token",
"authorization",
"raw",
"endpoint",
"stripepaymentmethodid",
"inventoryid",
"upstreamplanid",
"fingerprint",
"ciphertext",
]) {
expect(
fields.filter((field) => field.includes(forbidden)),
forbidden,
).toEqual([])
}
})
test("documents the transcription model catalog route", () => {
const spec = OpenApi.fromApi(PublicApi)
const route = spec.paths[KiloGatewayPaths.transcriptionModels]?.get
@@ -0,0 +1,55 @@
export * as ProviderUsage from "./provider-usage"
import { Schema } from "effect"
import { optional } from "../schema"
export interface UsageError extends Schema.Schema.Type<typeof UsageError> {}
export const UsageError = Schema.Struct({
code: Schema.String,
message: Schema.String,
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,
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"]),
}).annotate({ identifier: "ProviderUsageWindow" })
export interface UsageSnapshot extends Schema.Schema.Type<typeof UsageSnapshot> {}
export const UsageSnapshot = Schema.Struct({
id: Schema.String,
providerID: Schema.String,
sourceKind: Schema.Literals(["kilo_managed", "direct"]),
providerLabel: Schema.String,
planLabel: Schema.String,
sourceLabel: Schema.String,
fetchState: Schema.Literals(["ready", "stale", "unavailable", "error"]),
planState: Schema.Literals(["active", "past_due", "canceling", "unknown"]),
routingState: Schema.Literals(["active", "disabled", "missing", "replaced", "not_applicable", "unknown"]),
fetchedAt: optional(Schema.String),
managementUrl: optional(Schema.String),
windows: Schema.Array(UsageWindow),
error: optional(UsageError),
}).annotate({ identifier: "ProviderUsageSnapshot" })
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const Info = Schema.Struct({
items: Schema.Array(UsageSnapshot),
generatedAt: Schema.String,
}).annotate({ identifier: "ProviderUsage" })
+79
View File
@@ -195,6 +195,10 @@ import type {
KilocodeNotebookRejectResponses,
KilocodeNotebookReplyErrors,
KilocodeNotebookReplyResponses,
KilocodeProviderUsageGetErrors,
KilocodeProviderUsageGetResponses,
KilocodeProviderUsageRefreshErrors,
KilocodeProviderUsageRefreshResponses,
KilocodeRemoveAgentErrors,
KilocodeRemoveAgentResponses,
KilocodeRemoveCommandErrors,
@@ -7446,6 +7450,76 @@ export class Heap extends HeyApiClient {
}
}
export class ProviderUsage extends HeyApiClient {
/**
* Get provider usage
*
* Get cache-aware, secret-free provider plan usage and personal billing status.
*/
public get<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
return (options?.client ?? this.client).get<
KilocodeProviderUsageGetResponses,
KilocodeProviderUsageGetErrors,
ThrowOnError
>({
url: "/kilocode/provider-usage",
...options,
...params,
})
}
/**
* Refresh provider usage
*
* Refresh provider plan usage while coalescing concurrent source requests.
*/
public refresh<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
return (options?.client ?? this.client).post<
KilocodeProviderUsageRefreshResponses,
KilocodeProviderUsageRefreshErrors,
ThrowOnError
>({
url: "/kilocode/provider-usage/refresh",
...options,
...params,
})
}
}
export class Notebook extends HeyApiClient {
/**
* List pending notebook requests
@@ -8346,6 +8420,11 @@ export class Kilocode extends HeyApiClient {
return (this._heap ??= new Heap({ client: this.client }))
}
private _providerUsage?: ProviderUsage
get providerUsage(): ProviderUsage {
return (this._providerUsage ??= new ProviderUsage({ client: this.client }))
}
private _notebook?: Notebook
get notebook(): Notebook {
return (this._notebook ??= new Notebook({ client: this.client }))
+113
View File
@@ -4155,6 +4155,52 @@ export type CommandFile = {
hints: Array<string>
}
export type ProviderUsagePeriod = {
unit: "hour" | "day" | "week" | "month"
value: number
}
export type ProviderUsageWindow = {
id: 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"
}
export type ProviderUsageError = {
code: string
message: string
retryable: boolean
}
export type ProviderUsageSnapshot = {
id: string
providerID: string
sourceKind: "kilo_managed" | "direct"
providerLabel: string
planLabel: string
sourceLabel: string
fetchState: "ready" | "stale" | "unavailable" | "error"
planState: "active" | "past_due" | "canceling" | "unknown"
routingState: "active" | "disabled" | "missing" | "replaced" | "not_applicable" | "unknown"
fetchedAt?: string
managementUrl?: string
windows: Array<ProviderUsageWindow>
error?: ProviderUsageError
}
export type ProviderUsage = {
items: Array<ProviderUsageSnapshot>
generatedAt: string
}
export type NotebookOutput = {
mime: string
text?: string
@@ -16611,6 +16657,73 @@ export type KilocodeRemoveAgentResponses = {
export type KilocodeRemoveAgentResponse = KilocodeRemoveAgentResponses[keyof KilocodeRemoveAgentResponses]
export type KilocodeProviderUsageGetData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/kilocode/provider-usage"
}
export type KilocodeProviderUsageGetErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* ServiceUnavailable
*/
503: EffectHttpApiErrorServiceUnavailable
}
export type KilocodeProviderUsageGetError = KilocodeProviderUsageGetErrors[keyof KilocodeProviderUsageGetErrors]
export type KilocodeProviderUsageGetResponses = {
/**
* Current provider usage
*/
200: ProviderUsage
}
export type KilocodeProviderUsageGetResponse =
KilocodeProviderUsageGetResponses[keyof KilocodeProviderUsageGetResponses]
export type KilocodeProviderUsageRefreshData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/kilocode/provider-usage/refresh"
}
export type KilocodeProviderUsageRefreshErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* ServiceUnavailable
*/
503: EffectHttpApiErrorServiceUnavailable
}
export type KilocodeProviderUsageRefreshError =
KilocodeProviderUsageRefreshErrors[keyof KilocodeProviderUsageRefreshErrors]
export type KilocodeProviderUsageRefreshResponses = {
/**
* Refreshed provider usage
*/
200: ProviderUsage
}
export type KilocodeProviderUsageRefreshResponse =
KilocodeProviderUsageRefreshResponses[keyof KilocodeProviderUsageRefreshResponses]
export type KilocodeNotebookListData = {
body?: never
path?: never
+280
View File
@@ -15226,6 +15226,134 @@
]
}
},
"/kilocode/provider-usage": {
"get": {
"tags": ["kilocode"],
"operationId": "kilocode.providerUsage.get",
"parameters": [
{
"name": "directory",
"in": "query",
"schema": {
"type": "string"
},
"required": false
},
{
"name": "workspace",
"in": "query",
"schema": {
"type": "string"
},
"required": false
}
],
"responses": {
"200": {
"description": "Current provider usage",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProviderUsage"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
},
"503": {
"description": "ServiceUnavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/effect_HttpApiError_ServiceUnavailable"
}
}
}
}
},
"description": "Get cache-aware, secret-free provider plan usage and personal billing status.",
"summary": "Get provider usage",
"x-codeSamples": [
{
"lang": "js",
"source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.kilocode.providerUsage.get({\n ...\n})"
}
]
}
},
"/kilocode/provider-usage/refresh": {
"post": {
"tags": ["kilocode"],
"operationId": "kilocode.providerUsage.refresh",
"parameters": [
{
"name": "directory",
"in": "query",
"schema": {
"type": "string"
},
"required": false
},
{
"name": "workspace",
"in": "query",
"schema": {
"type": "string"
},
"required": false
}
],
"responses": {
"200": {
"description": "Refreshed provider usage",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProviderUsage"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
},
"503": {
"description": "ServiceUnavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/effect_HttpApiError_ServiceUnavailable"
}
}
}
}
},
"description": "Refresh provider plan usage while coalescing concurrent source requests.",
"summary": "Refresh provider usage",
"x-codeSamples": [
{
"lang": "js",
"source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.kilocode.providerUsage.refresh({\n ...\n})"
}
]
}
},
"/kilocode/notebook": {
"get": {
"tags": ["kilocode"],
@@ -38656,6 +38784,158 @@
"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"
},
"resource": {
"type": "string"
},
"unit": {
"type": "string"
},
"orientation": {
"type": "string",
"enum": ["used_percent", "remaining_percent", "amount", "count"]
},
"used": {
"type": "number"
},
"remaining": {
"type": "number"
},
"limit": {
"type": "number"
},
"period": {
"$ref": "#/components/schemas/ProviderUsagePeriod"
},
"durationMs": {
"type": "number"
},
"resetAt": {
"type": "string"
},
"state": {
"type": "string",
"enum": ["active", "exhausted", "unlimited", "not_in_plan", "unknown"]
}
},
"required": ["id", "resource", "unit", "orientation", "state"],
"additionalProperties": false
},
"ProviderUsageError": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"retryable": {
"type": "boolean"
}
},
"required": ["code", "message", "retryable"],
"additionalProperties": false
},
"ProviderUsageSnapshot": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"providerID": {
"type": "string"
},
"sourceKind": {
"type": "string",
"enum": ["kilo_managed", "direct"]
},
"providerLabel": {
"type": "string"
},
"planLabel": {
"type": "string"
},
"sourceLabel": {
"type": "string"
},
"fetchState": {
"type": "string",
"enum": ["ready", "stale", "unavailable", "error"]
},
"planState": {
"type": "string",
"enum": ["active", "past_due", "canceling", "unknown"]
},
"routingState": {
"type": "string",
"enum": ["active", "disabled", "missing", "replaced", "not_applicable", "unknown"]
},
"fetchedAt": {
"type": "string"
},
"managementUrl": {
"type": "string"
},
"windows": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ProviderUsageWindow"
}
},
"error": {
"$ref": "#/components/schemas/ProviderUsageError"
}
},
"required": [
"id",
"providerID",
"sourceKind",
"providerLabel",
"planLabel",
"sourceLabel",
"fetchState",
"planState",
"routingState",
"windows"
],
"additionalProperties": false
},
"ProviderUsage": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ProviderUsageSnapshot"
}
},
"generatedAt": {
"type": "string"
}
},
"required": ["items", "generatedAt"],
"additionalProperties": false
},
"NotebookOutput": {
"type": "object",
"properties": {