feat(gateway): expose kilo pass profile contract (#11721)

* feat(gateway): expose kilo pass profile contract

* chore(changeset): include sdk contract bump
This commit is contained in:
Johnny Eric Amancio
2026-06-26 13:30:56 +02:00
committed by GitHub
parent e0c1e729d3
commit be1f77d432
13 changed files with 200 additions and 7 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"@kilocode/cli": patch
"@kilocode/kilo-gateway": patch
"@kilocode/sdk": patch
---
Expose Kilo Pass state on the Kilo profile API contract.
@@ -0,0 +1,44 @@
import { buildKiloHeaders } from "../headers.js"
import type { KiloPassState } from "../types.js"
import { KILO_API_BASE } from "./constants.js"
function record(value: unknown) {
return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : undefined
}
function num(value: unknown) {
return typeof value === "number" && Number.isFinite(value) ? value : 0
}
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
const next = sub.nextBillingAt ?? sub.nextRenewalAt
return {
currentPeriodBaseCreditsUsd: num(sub.currentPeriodBaseCreditsUsd),
currentPeriodUsageUsd: num(sub.currentPeriodUsageUsd),
currentPeriodBonusCreditsUsd: num(sub.currentPeriodBonusCreditsUsd),
nextBillingAt: typeof next === "string" ? next : null,
}
}
export async function fetchKiloPassState(token: string): Promise<KiloPassState | null> {
try {
const params = new URLSearchParams({ batch: "1", input: JSON.stringify({ "0": null }) })
const response = await fetch(`${KILO_API_BASE}/api/trpc/kiloPass.getState?${params}`, {
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...buildKiloHeaders() },
})
if (!response.ok) {
console.warn(`Failed to fetch Kilo Pass: ${response.status}`)
return null
}
return parseKiloPassState(await response.json())
} catch (err) {
console.warn("Error fetching Kilo Pass:", err)
return null
}
}
+2
View File
@@ -33,6 +33,7 @@ export {
getKiloDefaultModel,
promptOrganizationSelection,
} from "./api/profile.js"
export { fetchKiloPassState } from "./api/kilo-pass.js"
export { fetchKiloModels, type KiloModelsResult } from "./api/models.js"
export {
EMPTY_KILO_EMBEDDING_MODEL_CATALOG,
@@ -94,6 +95,7 @@ export type {
Organization,
KilocodeProfile,
KilocodeBalance,
KiloPassState,
PollOptions,
PollResult,
// Provider types
+6 -3
View File
@@ -1,8 +1,9 @@
import { fetchBalance, fetchProfile } from "../api/profile.js"
import { fetchKiloPassState } from "../api/kilo-pass.js"
import { fetchKilocodeNotifications } from "../api/notifications.js"
import { clearModesCache } from "../api/modes.js"
import { HEADER_ORGANIZATIONID, KILO_API_BASE, KILO_CHAT_URL, KILO_EVENT_SERVICE_URL } from "../api/constants.js"
import type { KilocodeBalance, KilocodeProfile } from "../types.js"
import type { KilocodeBalance, KilocodeProfile, KiloPassState } from "../types.js"
import { buildKiloHeaders } from "../headers.js"
export type KiloAuth =
@@ -13,6 +14,7 @@ export type KiloAuth =
export interface KiloProfileResult {
profile: KilocodeProfile
balance: KilocodeBalance | null
kiloPass: KiloPassState | null
currentOrgId: string | null
}
@@ -67,11 +69,12 @@ export async function getProfile(auth: AuthStore): Promise<KiloProfileResult> {
if (!info || info.type !== "oauth") throw new UnauthorizedError("Not authenticated with Kilo Gateway")
const currentOrgId = info.accountId ?? null
const [profile, balance] = await Promise.all([
const [profile, balance, kiloPass] = await Promise.all([
fetchProfile(info.access),
fetchBalance(info.access, currentOrgId ?? undefined),
fetchKiloPassState(info.access),
])
return { profile, balance, currentOrgId }
return { profile, balance, kiloPass, currentOrgId }
}
export async function getNotifications(auth: AuthStore) {
@@ -107,9 +107,17 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
balance: z.number(),
})
const KiloPassState = z.object({
currentPeriodBaseCreditsUsd: z.number(),
currentPeriodUsageUsd: z.number(),
currentPeriodBonusCreditsUsd: z.number(),
nextBillingAt: z.string().nullable().optional(),
})
const ProfileWithBalance = z.object({
profile: Profile,
balance: Balance.nullable(),
kiloPass: KiloPassState.nullable(),
currentOrgId: z.string().nullable(),
})
+7
View File
@@ -33,6 +33,13 @@ export interface KilocodeBalance {
balance: number
}
export interface KiloPassState {
currentPeriodBaseCreditsUsd: number
currentPeriodUsageUsd: number
currentPeriodBonusCreditsUsd: number
nextBillingAt?: string | null
}
export interface PollOptions<T> {
interval: number
maxAttempts: number
@@ -0,0 +1,63 @@
import { describe, expect, test } from "bun:test"
import { parseKiloPassState } from "../../src/api/kilo-pass"
describe("parseKiloPassState", () => {
test("parses batched tRPC subscription data", () => {
const state = parseKiloPassState([
{
result: {
data: {
json: {
subscription: {
tier: "tier_199",
currentPeriodBaseCreditsUsd: 199,
currentPeriodUsageUsd: 73.27,
currentPeriodBonusCreditsUsd: 99.5,
nextBillingAt: "2026-07-01T00:00:00.000Z",
},
},
},
},
},
])
expect(state).toEqual({
currentPeriodBaseCreditsUsd: 199,
currentPeriodUsageUsd: 73.27,
currentPeriodBonusCreditsUsd: 99.5,
nextBillingAt: "2026-07-01T00:00:00.000Z",
})
})
test("parses plain subscription payload", () => {
const state = parseKiloPassState([
{
result: {
data: {
subscription: {
tier: "tier_199",
status: "active",
currentPeriodBaseCreditsUsd: 199,
currentPeriodUsageUsd: 0.01,
currentPeriodBonusCreditsUsd: 29.85,
isBonusUnlocked: false,
nextBillingAt: "2026-07-20T09:30:20.806Z",
},
isEligibleForFirstMonthPromo: false,
},
},
},
])
expect(state).toEqual({
currentPeriodBaseCreditsUsd: 199,
currentPeriodUsageUsd: 0.01,
currentPeriodBonusCreditsUsd: 29.85,
nextBillingAt: "2026-07-20T09:30:20.806Z",
})
})
test("returns null without period amounts", () => {
expect(parseKiloPassState({ status: "none" })).toBeNull()
})
})
@@ -27,9 +27,17 @@ export const Balance = Schema.Struct({
balance: Schema.Finite,
})
export const KiloPassState = Schema.Struct({
currentPeriodBaseCreditsUsd: Schema.Finite,
currentPeriodUsageUsd: Schema.Finite,
currentPeriodBonusCreditsUsd: Schema.Finite,
nextBillingAt: Schema.optional(Schema.NullOr(Schema.String)),
})
export const ProfileWithBalance = Schema.Struct({
profile: Profile,
balance: Schema.NullOr(Balance),
kiloPass: Schema.NullOr(KiloPassState),
currentOrgId: Schema.NullOr(Schema.String),
})
@@ -16,6 +16,7 @@ import {
clearModesCache,
fetchBalance,
fetchKilocodeNotifications,
fetchKiloPassState,
fetchOrganizationModes,
fetchProfile,
} from "@kilocode/kilo-gateway"
@@ -66,11 +67,16 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo",
if (!info || info.type !== "oauth") return yield* Effect.fail(new HttpApiError.Unauthorized({}))
const currentOrgId = info.accountId ?? null
const [profile, balance] = yield* Effect.tryPromise({
try: () => Promise.all([fetchProfile(info.access), fetchBalance(info.access, currentOrgId ?? undefined)]),
const [profile, balance, kiloPass] = yield* Effect.tryPromise({
try: () =>
Promise.all([
fetchProfile(info.access),
fetchBalance(info.access, currentOrgId ?? undefined),
fetchKiloPassState(info.access),
]),
catch: () => new HttpApiError.BadRequest({}),
})
return { profile, balance, currentOrgId }
return { profile, balance, kiloPass, currentOrgId }
})
const authStatus = Effect.fn("KiloGatewayHttpApi.authStatus")(function* () {
@@ -52,6 +52,7 @@ export function matchLegacyKiloOpenApi(input: Record<string, unknown>) {
const json = (path: string) => spec.paths?.[path]?.get?.responses?.["200"]?.content?.["application/json"]
const profile = json("/kilo/profile")?.schema?.properties
if (profile?.balance) profile.balance = nullable(profile.balance)
if (profile?.kiloPass) profile.kiloPass = nullable(profile.kiloPass)
if (profile?.currentOrgId) profile.currentOrgId = nullable(profile.currentOrgId)
const sessions = json("/kilo/cloud-sessions")?.schema?.properties
@@ -164,6 +164,7 @@ describe("Kilo PublicApi OpenAPI contract", () => {
const profile = response(KiloGatewayPaths.profile)?.properties
expect(profile?.balance).toEqual({ anyOf: [expect.objectContaining({ type: "object" }), { type: "null" }] })
expect(profile?.kiloPass).toEqual({ anyOf: [expect.objectContaining({ type: "object" }), { type: "null" }] })
expect(profile?.currentOrgId).toEqual({ anyOf: [{ type: "string" }, { type: "null" }] })
const auth = response(KiloGatewayPaths.authStatus)?.properties
+6
View File
@@ -10114,6 +10114,12 @@ export type KiloProfileResponses = {
balance: {
balance: number
} | null
kiloPass: {
currentPeriodBaseCreditsUsd: number
currentPeriodUsageUsd: number
currentPeriodBonusCreditsUsd: number
nextBillingAt?: string | null
} | null
currentOrgId: string | null
}
}
+38 -1
View File
@@ -13064,6 +13064,43 @@
}
]
},
"kiloPass": {
"anyOf": [
{
"type": "object",
"properties": {
"currentPeriodBaseCreditsUsd": {
"type": "number"
},
"currentPeriodUsageUsd": {
"type": "number"
},
"currentPeriodBonusCreditsUsd": {
"type": "number"
},
"nextBillingAt": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"currentPeriodBaseCreditsUsd",
"currentPeriodUsageUsd",
"currentPeriodBonusCreditsUsd"
],
"additionalProperties": false
},
{
"type": "null"
}
]
},
"currentOrgId": {
"anyOf": [
{
@@ -13075,7 +13112,7 @@
]
}
},
"required": ["profile", "balance", "currentOrgId"],
"required": ["profile", "balance", "kiloPass", "currentOrgId"],
"additionalProperties": false,
"description": "Profile data"
}