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.
- package/CHANGELOG.md +39 -0
- package/CONTRIBUTING.md +10 -0
- package/README.md +38 -12
- package/index.ts +73 -41
- 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.ts +141 -0
- package/src/converters.ts +114 -23
- package/src/core.ts +121 -31
- package/src/models.ts +60 -50
- package/src/oauth.ts +84 -22
- package/src/pricing.ts +53 -45
- package/src/quota-command.ts +66 -0
- package/src/quota-format.ts +111 -0
- package/src/quota-types.ts +47 -0
- package/src/quota.ts +335 -0
- package/src/runtime.ts +10 -5
- package/src/transport.ts +140 -0
- package/src/types.ts +9 -0
|
@@ -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 {
|
|
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
|
-
|
|
263
|
-
|
|
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
|
}
|
package/src/transport.ts
ADDED
|
@@ -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
|