pi-commandcode-provider 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/CONTRIBUTING.md +10 -0
- package/README.md +41 -13
- package/index.ts +106 -38
- package/package.json +13 -4
- package/scripts/live-e2e-profile.mjs +87 -0
- package/scripts/pi-authenticated.mjs +1 -0
- package/scripts/pi-isolated.mjs +1 -0
- package/src/api-key.ts +69 -0
- package/src/auth-server.ts +7 -0
- package/src/commandcode-catalog-overrides.ts +23 -0
- package/src/commandcode-catalog.ts +152 -0
- package/src/converters.ts +78 -17
- package/src/core.ts +117 -23
- package/src/models.ts +68 -97
- package/src/oauth.ts +84 -22
- package/src/pricing.ts +58 -53
- package/src/quota-command.ts +66 -0
- package/src/quota-format.ts +143 -0
- package/src/quota-types.ts +47 -0
- package/src/quota.ts +342 -0
- package/src/runtime.ts +49 -8
- package/src/transport.ts +140 -0
- package/src/types.ts +9 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CommandCodeCredits,
|
|
3
|
+
CommandCodeQuota,
|
|
4
|
+
CommandCodeSubscription,
|
|
5
|
+
CommandCodeWindowLimit,
|
|
6
|
+
} from "./quota-types.ts"
|
|
7
|
+
|
|
8
|
+
export function formatWindowLimits(
|
|
9
|
+
limits: readonly CommandCodeWindowLimit[],
|
|
10
|
+
now: () => number = Date.now,
|
|
11
|
+
): string[] {
|
|
12
|
+
const labels: Record<CommandCodeWindowLimit["window"], string> = {
|
|
13
|
+
fiveHour: "5-hour",
|
|
14
|
+
weekly: "Weekly",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return limits.map((limit) => {
|
|
18
|
+
const used = limit.used.toFixed(2)
|
|
19
|
+
const cap = limit.cap.toFixed(2)
|
|
20
|
+
const percent = limit.cap > 0 ? Math.round((limit.used / limit.cap) * 100) : 0
|
|
21
|
+
const reset = limit.resetAt === null ? "" : ` (resets ${formatResetClock(limit.resetAt, now)})`
|
|
22
|
+
return `${labels[limit.window]}: ${used} / ${cap} credits (${percent}% used)${reset}`
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatResetClock(resetAtSeconds: number, now: () => number): string {
|
|
27
|
+
const date = new Date(resetAtSeconds * 1000)
|
|
28
|
+
if (Number.isNaN(date.getTime())) return "unknown"
|
|
29
|
+
const diffMs = date.getTime() - now()
|
|
30
|
+
if (diffMs <= 0) return "soon"
|
|
31
|
+
const minutes = Math.ceil(diffMs / 60_000)
|
|
32
|
+
if (minutes < 60) return `in ${minutes}m`
|
|
33
|
+
const hours = Math.floor(minutes / 60)
|
|
34
|
+
const remainingMinutes = minutes % 60
|
|
35
|
+
if (hours < 24) {
|
|
36
|
+
return remainingMinutes > 0 ? `in ${hours}h ${remainingMinutes}m` : `in ${hours}h`
|
|
37
|
+
}
|
|
38
|
+
const days = Math.floor(hours / 24)
|
|
39
|
+
return days === 1 ? "in 1 day" : `in ${days} days`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function creditsDetail(credits: CommandCodeCredits | null): string | undefined {
|
|
43
|
+
if (!credits) return undefined
|
|
44
|
+
const parts = [
|
|
45
|
+
`monthly $${credits.monthlyCredits.toFixed(2)}`,
|
|
46
|
+
`purchased $${credits.purchasedCredits.toFixed(2)}`,
|
|
47
|
+
]
|
|
48
|
+
if (credits.freeCredits > 0) parts.push(`free $${credits.freeCredits.toFixed(2)}`)
|
|
49
|
+
return `Sources: ${parts.join(" / ")}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parsePeriodEnd(value: string): Date | null {
|
|
53
|
+
const trimmed = value.trim()
|
|
54
|
+
const timestamp = /^\d+$/.test(trimmed) ? Number(trimmed) : Date.parse(trimmed)
|
|
55
|
+
if (!Number.isFinite(timestamp) || timestamp < 0) return null
|
|
56
|
+
const milliseconds = timestamp >= 1e12 ? timestamp : timestamp * 1000
|
|
57
|
+
const date = new Date(milliseconds)
|
|
58
|
+
return Number.isNaN(date.getTime()) ? null : date
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function subscriptionLine(
|
|
62
|
+
subscription: CommandCodeSubscription,
|
|
63
|
+
now: () => number = Date.now,
|
|
64
|
+
): string {
|
|
65
|
+
const plan = (subscription.planId ?? "Unknown").replace(/[_-]+/g, " ").trim()
|
|
66
|
+
const status = subscription.status ? ` (${subscription.status})` : ""
|
|
67
|
+
let renewal = ""
|
|
68
|
+
if (subscription.currentPeriodEnd) {
|
|
69
|
+
const end = parsePeriodEnd(subscription.currentPeriodEnd)
|
|
70
|
+
if (end) {
|
|
71
|
+
const diffMs = end.getTime() - now()
|
|
72
|
+
const days = Math.ceil(diffMs / 86_400_000)
|
|
73
|
+
const dateStr = end.toLocaleDateString("en-US", {
|
|
74
|
+
month: "short",
|
|
75
|
+
day: "numeric",
|
|
76
|
+
timeZone: "UTC",
|
|
77
|
+
})
|
|
78
|
+
if (days > 0) {
|
|
79
|
+
renewal = ` · renews ${dateStr} (${days}d)`
|
|
80
|
+
} else if (days === 0) {
|
|
81
|
+
renewal = ` · renews ${dateStr} (today)`
|
|
82
|
+
} else {
|
|
83
|
+
renewal = ` · renewed ${dateStr}`
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return `Plan: ${plan}${status}${renewal}`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatTokens(tokens: number): string {
|
|
91
|
+
if (tokens >= 1_000_000_000) return `${(tokens / 1_000_000_000).toFixed(1)}B`
|
|
92
|
+
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`
|
|
93
|
+
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`
|
|
94
|
+
return String(tokens)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function formatQuota(quota: CommandCodeQuota, now: () => number = Date.now): string {
|
|
98
|
+
const lines: string[] = []
|
|
99
|
+
const remaining = quota.credits?.remainingCredits ?? 0
|
|
100
|
+
const spent = quota.summary?.totalCost ?? 0
|
|
101
|
+
const pool = remaining + spent
|
|
102
|
+
|
|
103
|
+
if (quota.credits || quota.summary) {
|
|
104
|
+
lines.push("Credits")
|
|
105
|
+
lines.push(` Remaining: $${remaining.toFixed(2)} of $${pool.toFixed(2)}`)
|
|
106
|
+
lines.push(` Used: $${spent.toFixed(2)}`)
|
|
107
|
+
lines.push(` ${pool > 0 ? Math.round((spent / pool) * 100) : 0}% used`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const detail = creditsDetail(quota.credits)
|
|
111
|
+
if (detail) lines.push(detail)
|
|
112
|
+
if (quota.subscription) lines.push(subscriptionLine(quota.subscription, now))
|
|
113
|
+
|
|
114
|
+
if (quota.summary) {
|
|
115
|
+
lines.push("")
|
|
116
|
+
lines.push(quota.subscription?.currentPeriodStart ? "Usage (billing period)" : "Usage")
|
|
117
|
+
lines.push(` Cost: $${quota.summary.totalCost.toFixed(2)}`)
|
|
118
|
+
lines.push(` Requests: ${quota.summary.totalCount.toLocaleString("en-US")}`)
|
|
119
|
+
if (quota.summary.totalTokens !== undefined) {
|
|
120
|
+
lines.push(` Tokens: ${formatTokens(quota.summary.totalTokens)}`)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
lines.push("")
|
|
125
|
+
lines.push("Account")
|
|
126
|
+
lines.push(` ${quota.account.keyName ?? quota.account.login}`)
|
|
127
|
+
|
|
128
|
+
const limits = quota.credits?.windowLimits ?? []
|
|
129
|
+
if (limits.length > 0) {
|
|
130
|
+
lines.push("")
|
|
131
|
+
lines.push("Usage windows:")
|
|
132
|
+
lines.push(...formatWindowLimits(limits, now).map((line) => ` ${line}`))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if ((quota.unavailable?.length ?? 0) > 0) {
|
|
136
|
+
lines.push("")
|
|
137
|
+
lines.push(`Unavailable: ${quota.unavailable?.join(", ")}`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
lines.push("")
|
|
141
|
+
lines.push("Full detail: https://commandcode.ai/usage")
|
|
142
|
+
return lines.join("\n")
|
|
143
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface CommandCodeWindowLimit {
|
|
2
|
+
window: "fiveHour" | "weekly"
|
|
3
|
+
used: number
|
|
4
|
+
cap: number
|
|
5
|
+
resetAt: number | null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CommandCodeCredits {
|
|
9
|
+
monthlyCredits: number
|
|
10
|
+
purchasedCredits: number
|
|
11
|
+
freeCredits: number
|
|
12
|
+
remainingCredits: number
|
|
13
|
+
windowLimits: CommandCodeWindowLimit[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CommandCodeSubscription {
|
|
17
|
+
planId: string | null
|
|
18
|
+
status: string | null
|
|
19
|
+
currentPeriodStart: string | null
|
|
20
|
+
currentPeriodEnd: string | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CommandCodeUsageSummary {
|
|
24
|
+
totalCost: number
|
|
25
|
+
totalCount: number
|
|
26
|
+
totalTokens?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type CommandCodeQuotaSection = "credits" | "subscription" | "usage"
|
|
30
|
+
|
|
31
|
+
export interface CommandCodeQuota {
|
|
32
|
+
account: {
|
|
33
|
+
login: string
|
|
34
|
+
orgId: string | null
|
|
35
|
+
keyName?: string
|
|
36
|
+
}
|
|
37
|
+
credits: CommandCodeCredits | null
|
|
38
|
+
subscription: CommandCodeSubscription | null
|
|
39
|
+
summary: CommandCodeUsageSummary | null
|
|
40
|
+
unavailable?: readonly CommandCodeQuotaSection[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type CommandCodeQuotaErrorKind = "config" | "http" | "network" | "timeout"
|
|
44
|
+
|
|
45
|
+
export type CommandCodeQuotaResult =
|
|
46
|
+
| { ok: true; quota: CommandCodeQuota }
|
|
47
|
+
| { ok: false; error: { message: string; kind: CommandCodeQuotaErrorKind } }
|
package/src/quota.ts
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { redactCommandCodeErrorText } from "./overflow.ts"
|
|
2
|
+
import type {
|
|
3
|
+
CommandCodeCredits,
|
|
4
|
+
CommandCodeQuotaResult,
|
|
5
|
+
CommandCodeQuotaSection,
|
|
6
|
+
CommandCodeSubscription,
|
|
7
|
+
CommandCodeUsageSummary,
|
|
8
|
+
CommandCodeWindowLimit,
|
|
9
|
+
} from "./quota-types.ts"
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
|
12
|
+
export const QUOTA_TIMEOUT_MS = 15_000
|
|
13
|
+
|
|
14
|
+
interface FetchOptions {
|
|
15
|
+
apiKey: string
|
|
16
|
+
baseUrl?: string
|
|
17
|
+
fetchImpl?: typeof fetch
|
|
18
|
+
timeoutMs?: number
|
|
19
|
+
extraHeaders?: Record<string, string>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface HttpErrorShape {
|
|
23
|
+
__httpError: true
|
|
24
|
+
message: string
|
|
25
|
+
status: number
|
|
26
|
+
body: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface QuotaErrorShape {
|
|
30
|
+
__quotaError: true
|
|
31
|
+
kind: "timeout" | "network"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
35
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function numberValue(value: unknown): number | undefined {
|
|
39
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stringValue(value: unknown): string | undefined {
|
|
43
|
+
return typeof value === "string" && value.length > 0 ? value : undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function timestampValue(value: unknown): string | undefined {
|
|
47
|
+
const text = stringValue(value)
|
|
48
|
+
if (text) return text
|
|
49
|
+
const number = numberValue(value)
|
|
50
|
+
return number === undefined ? undefined : String(number)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function errorMessage(error: unknown): string {
|
|
54
|
+
return error instanceof Error ? error.message : String(error)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeResetAt(value: unknown): number | null {
|
|
58
|
+
let timestamp: number | undefined
|
|
59
|
+
if (typeof value === "number" && Number.isFinite(value)) timestamp = value
|
|
60
|
+
if (typeof value === "string" && value.length > 0) {
|
|
61
|
+
const trimmed = value.trim()
|
|
62
|
+
timestamp = /^\d+$/.test(trimmed) ? Number(trimmed) : Date.parse(trimmed)
|
|
63
|
+
}
|
|
64
|
+
if (timestamp === undefined || !Number.isFinite(timestamp) || timestamp < 0) return null
|
|
65
|
+
return timestamp >= 1e12 ? Math.round(timestamp / 1000) : timestamp
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function windowLimitsFromCredits(value: unknown): CommandCodeWindowLimit[] {
|
|
69
|
+
if (!isRecord(value)) return []
|
|
70
|
+
const limits: CommandCodeWindowLimit[] = []
|
|
71
|
+
for (const [window, entry] of [
|
|
72
|
+
["fiveHour", value.fiveHour],
|
|
73
|
+
["weekly", value.weekly],
|
|
74
|
+
] as const) {
|
|
75
|
+
if (!isRecord(entry)) continue
|
|
76
|
+
const used = numberValue(entry.used)
|
|
77
|
+
const cap = numberValue(entry.cap)
|
|
78
|
+
if (used === undefined || cap === undefined || (used === 0 && cap === 0)) continue
|
|
79
|
+
limits.push({ window, used, cap, resetAt: normalizeResetAt(entry.resetAt) })
|
|
80
|
+
}
|
|
81
|
+
return limits
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseCredits(value: unknown): CommandCodeCredits | null {
|
|
85
|
+
if (!isRecord(value) || !isRecord(value.credits)) return null
|
|
86
|
+
const credits = value.credits
|
|
87
|
+
const monthlyCredits = numberValue(credits.monthlyCredits)
|
|
88
|
+
const purchasedCredits = numberValue(credits.purchasedCredits)
|
|
89
|
+
const freeCredits = numberValue(credits.freeCredits)
|
|
90
|
+
if (monthlyCredits === undefined && purchasedCredits === undefined && freeCredits === undefined) {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
const monthly = monthlyCredits ?? 0
|
|
94
|
+
const purchased = purchasedCredits ?? 0
|
|
95
|
+
const free = freeCredits ?? 0
|
|
96
|
+
return {
|
|
97
|
+
monthlyCredits: monthly,
|
|
98
|
+
purchasedCredits: purchased,
|
|
99
|
+
freeCredits: free,
|
|
100
|
+
remainingCredits: monthly + purchased + free,
|
|
101
|
+
windowLimits: windowLimitsFromCredits(value.windowLimits),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseSubscription(value: unknown): CommandCodeSubscription | null {
|
|
106
|
+
if (!isRecord(value) || !isRecord(value.data)) return null
|
|
107
|
+
const data = value.data
|
|
108
|
+
const planId = stringValue(data.planId)
|
|
109
|
+
const status = stringValue(data.status)
|
|
110
|
+
const currentPeriodStart = timestampValue(data.currentPeriodStart)
|
|
111
|
+
const currentPeriodEnd = timestampValue(data.currentPeriodEnd)
|
|
112
|
+
if (!planId && !status && !currentPeriodStart && !currentPeriodEnd) return null
|
|
113
|
+
return {
|
|
114
|
+
planId: planId ?? null,
|
|
115
|
+
status: status ?? null,
|
|
116
|
+
currentPeriodStart: currentPeriodStart ?? null,
|
|
117
|
+
currentPeriodEnd: currentPeriodEnd ?? null,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseSummary(value: unknown): CommandCodeUsageSummary | null {
|
|
122
|
+
if (!isRecord(value)) return null
|
|
123
|
+
const totalCost = numberValue(value.totalCost)
|
|
124
|
+
const totalCount = numberValue(value.totalCount)
|
|
125
|
+
if (totalCost === undefined || totalCount === undefined) return null
|
|
126
|
+
const totalTokens = numberValue(value.totalTokens) ?? numberValue(value.tokens)
|
|
127
|
+
return { totalCost, totalCount, ...(totalTokens === undefined ? {} : { totalTokens }) }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseWhoami(value: unknown): {
|
|
131
|
+
login: string
|
|
132
|
+
orgId: string | null
|
|
133
|
+
keyName?: string
|
|
134
|
+
} | null {
|
|
135
|
+
if (!isRecord(value)) return null
|
|
136
|
+
const org = isRecord(value.org) ? value.org : undefined
|
|
137
|
+
const user = isRecord(value.user) ? value.user : undefined
|
|
138
|
+
const login =
|
|
139
|
+
(org ? stringValue(org.login) : undefined) ??
|
|
140
|
+
(user ? (stringValue(user.userName) ?? stringValue(user.name)) : undefined)
|
|
141
|
+
if (!login) return null
|
|
142
|
+
const orgId = org ? stringValue(org.id) : undefined
|
|
143
|
+
const keyName = user ? (stringValue(user.keyName) ?? stringValue(user.displayName)) : undefined
|
|
144
|
+
return { login, orgId: orgId ?? null, ...(keyName ? { keyName } : {}) }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildUrl(path: string, params: Record<string, string | undefined>): string {
|
|
148
|
+
const search = new URLSearchParams()
|
|
149
|
+
for (const [key, value] of Object.entries(params)) {
|
|
150
|
+
if (value) search.set(key, value)
|
|
151
|
+
}
|
|
152
|
+
const query = search.toString()
|
|
153
|
+
return `${path}${query ? `?${query}` : ""}`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isHttpError(value: unknown): value is HttpErrorShape {
|
|
157
|
+
return (
|
|
158
|
+
isRecord(value) &&
|
|
159
|
+
value.__httpError === true &&
|
|
160
|
+
typeof value.message === "string" &&
|
|
161
|
+
typeof value.status === "number" &&
|
|
162
|
+
typeof value.body === "string"
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isQuotaError(value: unknown): value is QuotaErrorShape {
|
|
167
|
+
return (
|
|
168
|
+
isRecord(value) &&
|
|
169
|
+
value.__quotaError === true &&
|
|
170
|
+
(value.kind === "timeout" || value.kind === "network")
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isBlockingHttpError(error: HttpErrorShape): boolean {
|
|
175
|
+
return error.status === 401 || error.status === 403
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function httpFailure(error: HttpErrorShape, context: string): CommandCodeQuotaResult {
|
|
179
|
+
const detail = error.body.trim().slice(0, 200)
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
error: {
|
|
183
|
+
kind: "http",
|
|
184
|
+
message: redactValue(
|
|
185
|
+
`${context} request failed (${error.status}): ${detail || error.message}`,
|
|
186
|
+
),
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
class QuotaTimeoutError extends Error {}
|
|
192
|
+
|
|
193
|
+
export async function fetchCommandCodeQuota(
|
|
194
|
+
options: FetchOptions,
|
|
195
|
+
): Promise<CommandCodeQuotaResult> {
|
|
196
|
+
if (!options.apiKey) {
|
|
197
|
+
return { ok: false, error: { message: "No Command Code API key found", kind: "config" } }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const baseUrl = options.baseUrl ?? DEFAULT_API_BASE
|
|
201
|
+
const fetchImpl = options.fetchImpl ?? fetch
|
|
202
|
+
const timeoutMs = options.timeoutMs ?? QUOTA_TIMEOUT_MS
|
|
203
|
+
const overallController = new AbortController()
|
|
204
|
+
const overallTimer = setTimeout(() => overallController.abort(), timeoutMs)
|
|
205
|
+
const headers = {
|
|
206
|
+
accept: "application/json",
|
|
207
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
208
|
+
...options.extraHeaders,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const request = async (path: string): Promise<unknown> => {
|
|
212
|
+
if (overallController.signal.aborted) throw new QuotaTimeoutError()
|
|
213
|
+
try {
|
|
214
|
+
const response = await fetchImpl(`${baseUrl}${path}`, {
|
|
215
|
+
method: "GET",
|
|
216
|
+
headers,
|
|
217
|
+
signal: overallController.signal,
|
|
218
|
+
})
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
return {
|
|
221
|
+
__httpError: true,
|
|
222
|
+
message:
|
|
223
|
+
response.status === 401 || response.status === 403
|
|
224
|
+
? "Command Code rejected the API key"
|
|
225
|
+
: response.statusText,
|
|
226
|
+
status: response.status,
|
|
227
|
+
body: await response.text().catch(() => ""),
|
|
228
|
+
} satisfies HttpErrorShape
|
|
229
|
+
}
|
|
230
|
+
return await response.json()
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (overallController.signal.aborted) throw new QuotaTimeoutError()
|
|
233
|
+
throw error
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const safeRequest = async (path: string): Promise<unknown> => {
|
|
238
|
+
try {
|
|
239
|
+
return await request(path)
|
|
240
|
+
} catch (error) {
|
|
241
|
+
return {
|
|
242
|
+
__quotaError: true,
|
|
243
|
+
kind: error instanceof QuotaTimeoutError ? "timeout" : "network",
|
|
244
|
+
} satisfies QuotaErrorShape
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const whoamiRaw = await request("/alpha/whoami")
|
|
250
|
+
if (isHttpError(whoamiRaw)) return httpFailure(whoamiRaw, "whoami")
|
|
251
|
+
const account = parseWhoami(whoamiRaw)
|
|
252
|
+
if (!account) {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
error: { kind: "http", message: "Command Code returned an unrecognized account response" },
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const orgId = account.orgId ?? undefined
|
|
260
|
+
const [creditsRaw, subscriptionRaw] = await Promise.all([
|
|
261
|
+
safeRequest(buildUrl("/alpha/billing/credits", { orgId })),
|
|
262
|
+
safeRequest(buildUrl("/alpha/billing/subscriptions", { orgId })),
|
|
263
|
+
])
|
|
264
|
+
if (isHttpError(creditsRaw) && isBlockingHttpError(creditsRaw)) {
|
|
265
|
+
return httpFailure(creditsRaw, "credits")
|
|
266
|
+
}
|
|
267
|
+
if (isHttpError(subscriptionRaw) && isBlockingHttpError(subscriptionRaw)) {
|
|
268
|
+
return httpFailure(subscriptionRaw, "subscription")
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const unavailable: CommandCodeQuotaSection[] = []
|
|
272
|
+
const credits =
|
|
273
|
+
isHttpError(creditsRaw) || isQuotaError(creditsRaw) ? null : parseCredits(creditsRaw)
|
|
274
|
+
if (!credits) unavailable.push("credits")
|
|
275
|
+
const subscription =
|
|
276
|
+
isHttpError(subscriptionRaw) || isQuotaError(subscriptionRaw)
|
|
277
|
+
? null
|
|
278
|
+
: parseSubscription(subscriptionRaw)
|
|
279
|
+
if (!subscription) unavailable.push("subscription")
|
|
280
|
+
|
|
281
|
+
const summaryRaw = await safeRequest(
|
|
282
|
+
buildUrl("/alpha/usage/summary", {
|
|
283
|
+
orgId,
|
|
284
|
+
since: subscription?.currentPeriodStart ?? undefined,
|
|
285
|
+
}),
|
|
286
|
+
)
|
|
287
|
+
if (isHttpError(summaryRaw) && isBlockingHttpError(summaryRaw)) {
|
|
288
|
+
return httpFailure(summaryRaw, "summary")
|
|
289
|
+
}
|
|
290
|
+
const summary =
|
|
291
|
+
isHttpError(summaryRaw) || isQuotaError(summaryRaw) ? null : parseSummary(summaryRaw)
|
|
292
|
+
if (!summary) unavailable.push("usage")
|
|
293
|
+
|
|
294
|
+
if (!credits && !subscription && !summary) {
|
|
295
|
+
return {
|
|
296
|
+
ok: false,
|
|
297
|
+
error: {
|
|
298
|
+
kind: overallController.signal.aborted ? "timeout" : "http",
|
|
299
|
+
message: overallController.signal.aborted
|
|
300
|
+
? "Command Code quota request timed out"
|
|
301
|
+
: "Command Code returned no recognized usage data for the account",
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
ok: true,
|
|
308
|
+
quota: {
|
|
309
|
+
account,
|
|
310
|
+
credits,
|
|
311
|
+
subscription,
|
|
312
|
+
summary,
|
|
313
|
+
...(unavailable.length > 0 ? { unavailable } : {}),
|
|
314
|
+
},
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (error instanceof QuotaTimeoutError || overallController.signal.aborted) {
|
|
318
|
+
return {
|
|
319
|
+
ok: false,
|
|
320
|
+
error: { message: "Command Code quota request timed out", kind: "timeout" },
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
ok: false,
|
|
325
|
+
error: {
|
|
326
|
+
message: redactValue(`Failed to fetch Command Code quota: ${errorMessage(error)}`),
|
|
327
|
+
kind: "network",
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
} finally {
|
|
331
|
+
clearTimeout(overallTimer)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function redactValue(value: string): string {
|
|
336
|
+
return redactCommandCodeErrorText(value)
|
|
337
|
+
.replace(
|
|
338
|
+
/("\s*(?:api[-_ ]?key|apikey|access[-_ ]?token|refresh[-_ ]?token|token|secret|password|authorization)\s*"\s*:\s*")([^"]{8,})/gi,
|
|
339
|
+
"$1[redacted]",
|
|
340
|
+
)
|
|
341
|
+
.trim()
|
|
342
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -26,13 +26,17 @@ export interface CommandCodeRuntimeApi<
|
|
|
26
26
|
export interface CommandCodeRuntimeOptions<TProviderConfig> {
|
|
27
27
|
endpoint: string
|
|
28
28
|
cachePath: string
|
|
29
|
-
loadModels: () => Promise<LoadCommandCodeModelsResult>
|
|
29
|
+
loadModels: (signal: AbortSignal) => Promise<LoadCommandCodeModelsResult>
|
|
30
|
+
/** Cached catalog only; resolves to an empty list when no valid cache exists. */
|
|
31
|
+
loadCachedModels: () => Promise<readonly CommandCodeModel[]>
|
|
30
32
|
createProviderConfig: (models: readonly CommandCodeModel[]) => TProviderConfig
|
|
33
|
+
getTransport?: () => "unknown" | "provider" | "generate"
|
|
31
34
|
now?: () => number
|
|
32
35
|
logWarning?: (message: string) => void
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export interface CommandCodeRuntimeStatus {
|
|
39
|
+
transport: "unknown" | "provider" | "generate"
|
|
36
40
|
source: LoadCommandCodeModelsResult["source"]
|
|
37
41
|
modelCount: number
|
|
38
42
|
lastSuccess?: number
|
|
@@ -86,6 +90,7 @@ function formatTimestamp(timestamp: number | undefined): string {
|
|
|
86
90
|
|
|
87
91
|
export function formatCommandCodeStatus(status: CommandCodeRuntimeStatus): string {
|
|
88
92
|
const lines = [
|
|
93
|
+
`transport: ${status.transport}`,
|
|
89
94
|
`source: ${status.source}`,
|
|
90
95
|
`model count: ${status.modelCount}`,
|
|
91
96
|
`last success: ${formatTimestamp(status.lastSuccess)}`,
|
|
@@ -105,6 +110,7 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
|
|
|
105
110
|
private status: CommandCodeRuntimeStatus
|
|
106
111
|
private providerRegistered = false
|
|
107
112
|
private refreshPromise: Promise<CommandCodeRefreshResult> | undefined
|
|
113
|
+
private readonly shutdown = new AbortController()
|
|
108
114
|
|
|
109
115
|
constructor(
|
|
110
116
|
private readonly pi: CommandCodeRuntimeApi<TProviderConfig, TContext>,
|
|
@@ -113,6 +119,7 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
|
|
|
113
119
|
this.now = options.now ?? Date.now
|
|
114
120
|
this.logWarning = options.logWarning ?? ((message) => console.warn(`[commandcode] ${message}`))
|
|
115
121
|
const initialStatus: CommandCodeRuntimeStatus = {
|
|
122
|
+
transport: "unknown",
|
|
116
123
|
source: "empty",
|
|
117
124
|
modelCount: 0,
|
|
118
125
|
cachePath: options.cachePath,
|
|
@@ -123,12 +130,40 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
|
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
getStatus(): CommandCodeRuntimeStatus {
|
|
126
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
...this.status,
|
|
135
|
+
transport: this.options.getTransport?.() ?? "unknown",
|
|
136
|
+
}
|
|
127
137
|
}
|
|
128
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Registers the cached catalog immediately and refreshes it in the
|
|
141
|
+
* background so host startup does not wait for the network. Without a
|
|
142
|
+
* valid cache the live refresh is awaited so models are available at once.
|
|
143
|
+
*/
|
|
129
144
|
async initialize(): Promise<void> {
|
|
130
145
|
this.registerCommands()
|
|
131
|
-
|
|
146
|
+
|
|
147
|
+
const cached = await this.options.loadCachedModels()
|
|
148
|
+
if (cached.length === 0) {
|
|
149
|
+
await this.refresh()
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.pi.registerProvider("commandcode", this.options.createProviderConfig(cached))
|
|
154
|
+
this.providerRegistered = true
|
|
155
|
+
this.status = {
|
|
156
|
+
...this.status,
|
|
157
|
+
source: "cache",
|
|
158
|
+
modelCount: cached.length,
|
|
159
|
+
lastSuccess: this.now(),
|
|
160
|
+
}
|
|
161
|
+
void this.refresh()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Aborts any background refresh so a stopping host does not wait for the network. */
|
|
165
|
+
dispose(): void {
|
|
166
|
+
this.shutdown.abort(new Error("Command Code provider shut down"))
|
|
132
167
|
}
|
|
133
168
|
|
|
134
169
|
refresh(): Promise<CommandCodeRefreshResult> {
|
|
@@ -149,7 +184,7 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
|
|
|
149
184
|
}
|
|
150
185
|
|
|
151
186
|
try {
|
|
152
|
-
const loaded = await this.options.loadModels()
|
|
187
|
+
const loaded = await this.options.loadModels(this.shutdown.signal)
|
|
153
188
|
const warning = loaded.warning ? redactDiagnosticText(loaded.warning) : undefined
|
|
154
189
|
|
|
155
190
|
const shouldRegister =
|
|
@@ -210,6 +245,14 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
|
|
|
210
245
|
warning: preservedWarning,
|
|
211
246
|
}
|
|
212
247
|
} catch (error) {
|
|
248
|
+
if (this.shutdown.signal.aborted) {
|
|
249
|
+
this.status = { ...this.status, refreshing: false }
|
|
250
|
+
return {
|
|
251
|
+
refreshed: false,
|
|
252
|
+
source: this.status.source,
|
|
253
|
+
modelCount: this.status.modelCount,
|
|
254
|
+
}
|
|
255
|
+
}
|
|
213
256
|
const warning = redactDiagnosticText(
|
|
214
257
|
`Could not refresh the Command Code model catalog: ${errorMessage(error)}`,
|
|
215
258
|
)
|
|
@@ -259,10 +302,8 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
|
|
|
259
302
|
this.pi.registerCommand("commandcode-status", {
|
|
260
303
|
description: "Show redacted Command Code provider diagnostics",
|
|
261
304
|
handler: async (_args, ctx) => {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
this.status.warning ? "warning" : "info",
|
|
265
|
-
)
|
|
305
|
+
const status = this.getStatus()
|
|
306
|
+
ctx.ui.notify(formatCommandCodeStatus(status), status.warning ? "warning" : "info")
|
|
266
307
|
},
|
|
267
308
|
})
|
|
268
309
|
}
|