pi-commandcode-provider 0.5.0 → 0.6.0

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.
@@ -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,335 @@
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 errorMessage(error: unknown): string {
47
+ return error instanceof Error ? error.message : String(error)
48
+ }
49
+
50
+ function normalizeResetAt(value: unknown): number | null {
51
+ let timestamp: number | undefined
52
+ if (typeof value === "number" && Number.isFinite(value)) timestamp = value
53
+ if (typeof value === "string" && value.length > 0) {
54
+ const trimmed = value.trim()
55
+ timestamp = /^\d+$/.test(trimmed) ? Number(trimmed) : Date.parse(trimmed)
56
+ }
57
+ if (timestamp === undefined || !Number.isFinite(timestamp) || timestamp < 0) return null
58
+ return timestamp >= 1e12 ? Math.round(timestamp / 1000) : timestamp
59
+ }
60
+
61
+ export function windowLimitsFromCredits(value: unknown): CommandCodeWindowLimit[] {
62
+ if (!isRecord(value)) return []
63
+ const limits: CommandCodeWindowLimit[] = []
64
+ for (const [window, entry] of [
65
+ ["fiveHour", value.fiveHour],
66
+ ["weekly", value.weekly],
67
+ ] as const) {
68
+ if (!isRecord(entry)) continue
69
+ const used = numberValue(entry.used)
70
+ const cap = numberValue(entry.cap)
71
+ if (used === undefined || cap === undefined || (used === 0 && cap === 0)) continue
72
+ limits.push({ window, used, cap, resetAt: normalizeResetAt(entry.resetAt) })
73
+ }
74
+ return limits
75
+ }
76
+
77
+ function parseCredits(value: unknown): CommandCodeCredits | null {
78
+ if (!isRecord(value) || !isRecord(value.credits)) return null
79
+ const credits = value.credits
80
+ const monthlyCredits = numberValue(credits.monthlyCredits)
81
+ const purchasedCredits = numberValue(credits.purchasedCredits)
82
+ const freeCredits = numberValue(credits.freeCredits)
83
+ if (monthlyCredits === undefined && purchasedCredits === undefined && freeCredits === undefined) {
84
+ return null
85
+ }
86
+ const monthly = monthlyCredits ?? 0
87
+ const purchased = purchasedCredits ?? 0
88
+ const free = freeCredits ?? 0
89
+ return {
90
+ monthlyCredits: monthly,
91
+ purchasedCredits: purchased,
92
+ freeCredits: free,
93
+ remainingCredits: monthly + purchased + free,
94
+ windowLimits: windowLimitsFromCredits(value.windowLimits),
95
+ }
96
+ }
97
+
98
+ function parseSubscription(value: unknown): CommandCodeSubscription | null {
99
+ if (!isRecord(value) || !isRecord(value.data)) return null
100
+ const data = value.data
101
+ const planId = stringValue(data.planId)
102
+ const status = stringValue(data.status)
103
+ const currentPeriodStart = stringValue(data.currentPeriodStart)
104
+ const currentPeriodEnd = stringValue(data.currentPeriodEnd)
105
+ if (!planId && !status && !currentPeriodStart && !currentPeriodEnd) return null
106
+ return {
107
+ planId: planId ?? null,
108
+ status: status ?? null,
109
+ currentPeriodStart: currentPeriodStart ?? null,
110
+ currentPeriodEnd: currentPeriodEnd ?? null,
111
+ }
112
+ }
113
+
114
+ function parseSummary(value: unknown): CommandCodeUsageSummary | null {
115
+ if (!isRecord(value)) return null
116
+ const totalCost = numberValue(value.totalCost)
117
+ const totalCount = numberValue(value.totalCount)
118
+ if (totalCost === undefined || totalCount === undefined) return null
119
+ const totalTokens = numberValue(value.totalTokens) ?? numberValue(value.tokens)
120
+ return { totalCost, totalCount, ...(totalTokens === undefined ? {} : { totalTokens }) }
121
+ }
122
+
123
+ function parseWhoami(value: unknown): {
124
+ login: string
125
+ orgId: string | null
126
+ keyName?: string
127
+ } | null {
128
+ if (!isRecord(value)) return null
129
+ const org = isRecord(value.org) ? value.org : undefined
130
+ const user = isRecord(value.user) ? value.user : undefined
131
+ const login =
132
+ (org ? stringValue(org.login) : undefined) ??
133
+ (user ? (stringValue(user.userName) ?? stringValue(user.name)) : undefined)
134
+ if (!login) return null
135
+ const orgId = org ? stringValue(org.id) : undefined
136
+ const keyName = user ? (stringValue(user.keyName) ?? stringValue(user.displayName)) : undefined
137
+ return { login, orgId: orgId ?? null, ...(keyName ? { keyName } : {}) }
138
+ }
139
+
140
+ function buildUrl(path: string, params: Record<string, string | undefined>): string {
141
+ const search = new URLSearchParams()
142
+ for (const [key, value] of Object.entries(params)) {
143
+ if (value) search.set(key, value)
144
+ }
145
+ const query = search.toString()
146
+ return `${path}${query ? `?${query}` : ""}`
147
+ }
148
+
149
+ function isHttpError(value: unknown): value is HttpErrorShape {
150
+ return (
151
+ isRecord(value) &&
152
+ value.__httpError === true &&
153
+ typeof value.message === "string" &&
154
+ typeof value.status === "number" &&
155
+ typeof value.body === "string"
156
+ )
157
+ }
158
+
159
+ function isQuotaError(value: unknown): value is QuotaErrorShape {
160
+ return (
161
+ isRecord(value) &&
162
+ value.__quotaError === true &&
163
+ (value.kind === "timeout" || value.kind === "network")
164
+ )
165
+ }
166
+
167
+ function isBlockingHttpError(error: HttpErrorShape): boolean {
168
+ return error.status === 401 || error.status === 403
169
+ }
170
+
171
+ function httpFailure(error: HttpErrorShape, context: string): CommandCodeQuotaResult {
172
+ const detail = error.body.trim().slice(0, 200)
173
+ return {
174
+ ok: false,
175
+ error: {
176
+ kind: "http",
177
+ message: redactValue(
178
+ `${context} request failed (${error.status}): ${detail || error.message}`,
179
+ ),
180
+ },
181
+ }
182
+ }
183
+
184
+ class QuotaTimeoutError extends Error {}
185
+
186
+ export async function fetchCommandCodeQuota(
187
+ options: FetchOptions,
188
+ ): Promise<CommandCodeQuotaResult> {
189
+ if (!options.apiKey) {
190
+ return { ok: false, error: { message: "No Command Code API key found", kind: "config" } }
191
+ }
192
+
193
+ const baseUrl = options.baseUrl ?? DEFAULT_API_BASE
194
+ const fetchImpl = options.fetchImpl ?? fetch
195
+ const timeoutMs = options.timeoutMs ?? QUOTA_TIMEOUT_MS
196
+ const overallController = new AbortController()
197
+ const overallTimer = setTimeout(() => overallController.abort(), timeoutMs)
198
+ const headers = {
199
+ accept: "application/json",
200
+ Authorization: `Bearer ${options.apiKey}`,
201
+ ...options.extraHeaders,
202
+ }
203
+
204
+ const request = async (path: string): Promise<unknown> => {
205
+ if (overallController.signal.aborted) throw new QuotaTimeoutError()
206
+ try {
207
+ const response = await fetchImpl(`${baseUrl}${path}`, {
208
+ method: "GET",
209
+ headers,
210
+ signal: overallController.signal,
211
+ })
212
+ if (!response.ok) {
213
+ return {
214
+ __httpError: true,
215
+ message:
216
+ response.status === 401 || response.status === 403
217
+ ? "Command Code rejected the API key"
218
+ : response.statusText,
219
+ status: response.status,
220
+ body: await response.text().catch(() => ""),
221
+ } satisfies HttpErrorShape
222
+ }
223
+ return await response.json()
224
+ } catch (error) {
225
+ if (overallController.signal.aborted) throw new QuotaTimeoutError()
226
+ throw error
227
+ }
228
+ }
229
+
230
+ const safeRequest = async (path: string): Promise<unknown> => {
231
+ try {
232
+ return await request(path)
233
+ } catch (error) {
234
+ return {
235
+ __quotaError: true,
236
+ kind: error instanceof QuotaTimeoutError ? "timeout" : "network",
237
+ } satisfies QuotaErrorShape
238
+ }
239
+ }
240
+
241
+ try {
242
+ const whoamiRaw = await request("/alpha/whoami")
243
+ if (isHttpError(whoamiRaw)) return httpFailure(whoamiRaw, "whoami")
244
+ const account = parseWhoami(whoamiRaw)
245
+ if (!account) {
246
+ return {
247
+ ok: false,
248
+ error: { kind: "http", message: "Command Code returned an unrecognized account response" },
249
+ }
250
+ }
251
+
252
+ const orgId = account.orgId ?? undefined
253
+ const [creditsRaw, subscriptionRaw] = await Promise.all([
254
+ safeRequest(buildUrl("/alpha/billing/credits", { orgId })),
255
+ safeRequest(buildUrl("/alpha/billing/subscriptions", { orgId })),
256
+ ])
257
+ if (isHttpError(creditsRaw) && isBlockingHttpError(creditsRaw)) {
258
+ return httpFailure(creditsRaw, "credits")
259
+ }
260
+ if (isHttpError(subscriptionRaw) && isBlockingHttpError(subscriptionRaw)) {
261
+ return httpFailure(subscriptionRaw, "subscription")
262
+ }
263
+
264
+ const unavailable: CommandCodeQuotaSection[] = []
265
+ const credits =
266
+ isHttpError(creditsRaw) || isQuotaError(creditsRaw) ? null : parseCredits(creditsRaw)
267
+ if (!credits) unavailable.push("credits")
268
+ const subscription =
269
+ isHttpError(subscriptionRaw) || isQuotaError(subscriptionRaw)
270
+ ? null
271
+ : parseSubscription(subscriptionRaw)
272
+ if (!subscription) unavailable.push("subscription")
273
+
274
+ const summaryRaw = await safeRequest(
275
+ buildUrl("/alpha/usage/summary", {
276
+ orgId,
277
+ since: subscription?.currentPeriodStart ?? undefined,
278
+ }),
279
+ )
280
+ if (isHttpError(summaryRaw) && isBlockingHttpError(summaryRaw)) {
281
+ return httpFailure(summaryRaw, "summary")
282
+ }
283
+ const summary =
284
+ isHttpError(summaryRaw) || isQuotaError(summaryRaw) ? null : parseSummary(summaryRaw)
285
+ if (!summary) unavailable.push("usage")
286
+
287
+ if (!credits && !subscription && !summary) {
288
+ return {
289
+ ok: false,
290
+ error: {
291
+ kind: overallController.signal.aborted ? "timeout" : "http",
292
+ message: overallController.signal.aborted
293
+ ? "Command Code quota request timed out"
294
+ : "Command Code returned no recognized usage data for the account",
295
+ },
296
+ }
297
+ }
298
+
299
+ return {
300
+ ok: true,
301
+ quota: {
302
+ account,
303
+ credits,
304
+ subscription,
305
+ summary,
306
+ ...(unavailable.length > 0 ? { unavailable } : {}),
307
+ },
308
+ }
309
+ } catch (error) {
310
+ if (error instanceof QuotaTimeoutError || overallController.signal.aborted) {
311
+ return {
312
+ ok: false,
313
+ error: { message: "Command Code quota request timed out", kind: "timeout" },
314
+ }
315
+ }
316
+ return {
317
+ ok: false,
318
+ error: {
319
+ message: redactValue(`Failed to fetch Command Code quota: ${errorMessage(error)}`),
320
+ kind: "network",
321
+ },
322
+ }
323
+ } finally {
324
+ clearTimeout(overallTimer)
325
+ }
326
+ }
327
+
328
+ export function redactValue(value: string): string {
329
+ return redactCommandCodeErrorText(value)
330
+ .replace(
331
+ /("\s*(?:api[-_ ]?key|apikey|access[-_ ]?token|refresh[-_ ]?token|token|secret|password|authorization)\s*"\s*:\s*")([^"]{8,})/gi,
332
+ "$1[redacted]",
333
+ )
334
+ .trim()
335
+ }
package/src/runtime.ts CHANGED
@@ -28,11 +28,13 @@ export interface CommandCodeRuntimeOptions<TProviderConfig> {
28
28
  cachePath: string
29
29
  loadModels: () => Promise<LoadCommandCodeModelsResult>
30
30
  createProviderConfig: (models: readonly CommandCodeModel[]) => TProviderConfig
31
+ getTransport?: () => "unknown" | "provider" | "generate"
31
32
  now?: () => number
32
33
  logWarning?: (message: string) => void
33
34
  }
34
35
 
35
36
  export interface CommandCodeRuntimeStatus {
37
+ transport: "unknown" | "provider" | "generate"
36
38
  source: LoadCommandCodeModelsResult["source"]
37
39
  modelCount: number
38
40
  lastSuccess?: number
@@ -86,6 +88,7 @@ function formatTimestamp(timestamp: number | undefined): string {
86
88
 
87
89
  export function formatCommandCodeStatus(status: CommandCodeRuntimeStatus): string {
88
90
  const lines = [
91
+ `transport: ${status.transport}`,
89
92
  `source: ${status.source}`,
90
93
  `model count: ${status.modelCount}`,
91
94
  `last success: ${formatTimestamp(status.lastSuccess)}`,
@@ -113,6 +116,7 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
113
116
  this.now = options.now ?? Date.now
114
117
  this.logWarning = options.logWarning ?? ((message) => console.warn(`[commandcode] ${message}`))
115
118
  const initialStatus: CommandCodeRuntimeStatus = {
119
+ transport: "unknown",
116
120
  source: "empty",
117
121
  modelCount: 0,
118
122
  cachePath: options.cachePath,
@@ -123,7 +127,10 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
123
127
  }
124
128
 
125
129
  getStatus(): CommandCodeRuntimeStatus {
126
- return { ...this.status }
130
+ return {
131
+ ...this.status,
132
+ transport: this.options.getTransport?.() ?? "unknown",
133
+ }
127
134
  }
128
135
 
129
136
  async initialize(): Promise<void> {
@@ -259,10 +266,8 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
259
266
  this.pi.registerCommand("commandcode-status", {
260
267
  description: "Show redacted Command Code provider diagnostics",
261
268
  handler: async (_args, ctx) => {
262
- ctx.ui.notify(
263
- formatCommandCodeStatus(this.status),
264
- this.status.warning ? "warning" : "info",
265
- )
269
+ const status = this.getStatus()
270
+ ctx.ui.notify(formatCommandCodeStatus(status), status.warning ? "warning" : "info")
266
271
  },
267
272
  })
268
273
  }
@@ -0,0 +1,140 @@
1
+ import type {
2
+ AssistantMessageEvent,
3
+ AssistantMessageEventStreamLike,
4
+ ContextLike,
5
+ ModelLike,
6
+ StreamOptions,
7
+ } from "./types.ts"
8
+
9
+ export type CommandCodeTransport = "unknown" | "provider" | "generate"
10
+
11
+ interface TransportDependencies {
12
+ createStream: () => AssistantMessageEventStreamLike
13
+ streamProvider: (
14
+ model: ModelLike,
15
+ context: ContextLike,
16
+ options?: StreamOptions,
17
+ ) => AssistantMessageEventStreamLike
18
+ streamGenerate: (
19
+ model: ModelLike,
20
+ context: ContextLike,
21
+ options?: StreamOptions,
22
+ ) => AssistantMessageEventStreamLike
23
+ }
24
+
25
+ function isRecord(value: unknown): value is Record<string, unknown> {
26
+ return typeof value === "object" && value !== null && !Array.isArray(value)
27
+ }
28
+
29
+ async function isUpgradeRequired(response: Response): Promise<boolean> {
30
+ if (response.status !== 403) return false
31
+
32
+ try {
33
+ const body: unknown = await response.clone().json()
34
+ if (!isRecord(body)) return false
35
+ const error = isRecord(body.error) ? body.error : body
36
+ return error.code === "upgrade_required"
37
+ } catch {
38
+ return false
39
+ }
40
+ }
41
+
42
+ export function createCommandCodeTransportRouter(deps: TransportDependencies) {
43
+ let transport: CommandCodeTransport = "unknown"
44
+ let apiKey: string | undefined
45
+
46
+ function pipe(
47
+ source: AssistantMessageEventStreamLike,
48
+ target: AssistantMessageEventStreamLike,
49
+ ): Promise<void> {
50
+ return (async () => {
51
+ for await (const event of source) target.push(event)
52
+ })()
53
+ }
54
+
55
+ return {
56
+ getTransport(): CommandCodeTransport {
57
+ return transport
58
+ },
59
+
60
+ reset(): void {
61
+ transport = "unknown"
62
+ apiKey = undefined
63
+ },
64
+
65
+ stream(
66
+ model: ModelLike,
67
+ context: ContextLike,
68
+ options?: StreamOptions,
69
+ ): AssistantMessageEventStreamLike {
70
+ if (options?.apiKey !== apiKey) {
71
+ apiKey = options?.apiKey
72
+ transport = "unknown"
73
+ }
74
+ const requestApiKey = options?.apiKey
75
+ if (transport === "generate") return deps.streamGenerate(model, context, options)
76
+
77
+ const output = deps.createStream()
78
+ let upgradeRequired = false
79
+ const fetchImpl = options?.fetch ?? fetch
80
+ const providerOptions: StreamOptions = {
81
+ ...options,
82
+ fetch: async (input, init) => {
83
+ const response = await fetchImpl(input, init)
84
+ if (await isUpgradeRequired(response)) upgradeRequired = true
85
+ return response
86
+ },
87
+ onResponse: async (response, responseModel) => {
88
+ if (upgradeRequired) return
89
+ await options?.onResponse?.(response, responseModel)
90
+ },
91
+ }
92
+
93
+ const run = async () => {
94
+ const providerStream = deps.streamProvider(model, context, providerOptions)
95
+
96
+ for await (const event of providerStream) {
97
+ if (!upgradeRequired) {
98
+ if (apiKey === requestApiKey) transport = "provider"
99
+ output.push(event)
100
+ }
101
+ }
102
+
103
+ if (upgradeRequired) {
104
+ if (apiKey === requestApiKey) transport = "generate"
105
+ await pipe(deps.streamGenerate(model, context, options), output)
106
+ }
107
+ output.end()
108
+ }
109
+
110
+ run().catch((error: unknown) => {
111
+ const message = error instanceof Error ? error.message : String(error)
112
+ output.push({
113
+ type: "error",
114
+ reason: "error",
115
+ error: {
116
+ role: "assistant",
117
+ content: [],
118
+ api: model.api,
119
+ provider: model.provider,
120
+ model: model.id,
121
+ usage: {
122
+ input: 0,
123
+ output: 0,
124
+ cacheRead: 0,
125
+ cacheWrite: 0,
126
+ totalTokens: 0,
127
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
128
+ },
129
+ stopReason: "error",
130
+ errorMessage: message,
131
+ timestamp: Date.now(),
132
+ },
133
+ })
134
+ output.end()
135
+ })
136
+
137
+ return output
138
+ },
139
+ }
140
+ }
package/src/types.ts CHANGED
@@ -110,7 +110,10 @@ export interface StreamOptions {
110
110
  apiKey?: string
111
111
  signal?: AbortSignal
112
112
  headers?: Record<string, string>
113
+ fetch?: typeof fetch
113
114
  maxTokens?: number
115
+ temperature?: number
116
+ sessionId?: string
114
117
  /** Resolved pi thinking level; forwarded only through the model's map. */
115
118
  reasoning?: string
116
119
  onPayload?: (payload: unknown, model: ModelLike) => unknown | Promise<unknown>
@@ -171,6 +174,12 @@ export type AssistantMessageEvent =
171
174
  contentIndex: number
172
175
  partial: AssistantMessageLike
173
176
  }
177
+ | {
178
+ type: "toolcall_delta"
179
+ contentIndex: number
180
+ delta: string
181
+ partial: AssistantMessageLike
182
+ }
174
183
  | {
175
184
  type: "toolcall_end"
176
185
  contentIndex: number