pi-commandcode-provider 0.4.3 → 0.5.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 +28 -0
- package/CONTRIBUTING.md +18 -0
- package/README.md +100 -72
- package/index.ts +84 -91
- package/package.json +15 -6
- package/scripts/pi-authenticated.mjs +49 -0
- package/scripts/pi-isolated.mjs +78 -0
- package/src/converters.ts +64 -85
- package/src/core.ts +107 -31
- package/src/cost.ts +16 -4
- package/src/json-schema.ts +382 -0
- package/src/models.ts +251 -15
- package/src/overflow.ts +120 -0
- package/src/pricing.ts +226 -0
- package/src/runtime.ts +279 -0
- package/src/types.ts +19 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process"
|
|
4
|
+
import { mkdir, mkdtemp, rm } from "node:fs/promises"
|
|
5
|
+
import { tmpdir } from "node:os"
|
|
6
|
+
import { dirname, join, resolve } from "node:path"
|
|
7
|
+
import { fileURLToPath } from "node:url"
|
|
8
|
+
|
|
9
|
+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
|
10
|
+
const testRoot = await mkdtemp(join(tmpdir(), "pi-commandcode-isolated-"))
|
|
11
|
+
const agentDir = join(testRoot, "agent")
|
|
12
|
+
const sessionDir = join(testRoot, "sessions")
|
|
13
|
+
|
|
14
|
+
await mkdir(agentDir, { mode: 0o700 })
|
|
15
|
+
await mkdir(sessionDir, { mode: 0o700 })
|
|
16
|
+
|
|
17
|
+
const env = {
|
|
18
|
+
...process.env,
|
|
19
|
+
HOME: testRoot,
|
|
20
|
+
USERPROFILE: testRoot,
|
|
21
|
+
PI_CODING_AGENT_DIR: agentDir,
|
|
22
|
+
PI_CODING_AGENT_SESSION_DIR: sessionDir,
|
|
23
|
+
PI_SKIP_VERSION_CHECK: "1",
|
|
24
|
+
}
|
|
25
|
+
delete env.COMMANDCODE_API_KEY
|
|
26
|
+
|
|
27
|
+
let activeChild
|
|
28
|
+
let receivedSignal
|
|
29
|
+
|
|
30
|
+
function forwardSignal(signal) {
|
|
31
|
+
receivedSignal = signal
|
|
32
|
+
activeChild?.kill(signal)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const forwardSigint = () => forwardSignal("SIGINT")
|
|
36
|
+
const forwardSigterm = () => forwardSignal("SIGTERM")
|
|
37
|
+
process.on("SIGINT", forwardSigint)
|
|
38
|
+
process.on("SIGTERM", forwardSigterm)
|
|
39
|
+
|
|
40
|
+
function runPi(args) {
|
|
41
|
+
return new Promise((resolveRun, rejectRun) => {
|
|
42
|
+
const child = spawn("pi", args, { cwd: repoRoot, env, stdio: "inherit" })
|
|
43
|
+
activeChild = child
|
|
44
|
+
child.once("error", rejectRun)
|
|
45
|
+
child.once("exit", (status, signal) => {
|
|
46
|
+
activeChild = undefined
|
|
47
|
+
resolveRun({ status, signal })
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let result
|
|
53
|
+
try {
|
|
54
|
+
console.error("Installing the current checkout into an isolated pi environment...")
|
|
55
|
+
const install = await runPi(["install", repoRoot, "--no-approve"])
|
|
56
|
+
if (install.status !== 0 || install.signal) {
|
|
57
|
+
result = install
|
|
58
|
+
} else {
|
|
59
|
+
console.error("Starting pi. Temporary auth and sessions will be removed on exit.")
|
|
60
|
+
result = await runPi([
|
|
61
|
+
"--no-approve",
|
|
62
|
+
"--provider",
|
|
63
|
+
"commandcode",
|
|
64
|
+
"--model",
|
|
65
|
+
"gpt-5.6-luna",
|
|
66
|
+
...process.argv.slice(2),
|
|
67
|
+
])
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
process.removeListener("SIGINT", forwardSigint)
|
|
71
|
+
process.removeListener("SIGTERM", forwardSigterm)
|
|
72
|
+
await rm(testRoot, { recursive: true, force: true })
|
|
73
|
+
console.error("Removed the isolated pi environment.")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const signal = receivedSignal ?? result?.signal
|
|
77
|
+
if (signal) process.kill(process.pid, signal)
|
|
78
|
+
process.exitCode = result?.status ?? 1
|
package/src/converters.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { homedir } from "node:os"
|
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
|
|
5
5
|
import type { MessageLike, StopReason, ToolLike } from "./types.ts"
|
|
6
|
+
import { toJsonSchema } from "./json-schema.ts"
|
|
7
|
+
|
|
8
|
+
export { toJsonSchema } from "./json-schema.ts"
|
|
6
9
|
|
|
7
10
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
8
11
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
@@ -12,10 +15,6 @@ export function stringValue(value: unknown): string | undefined {
|
|
|
12
15
|
return typeof value === "string" ? value : undefined
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
function booleanValue(value: unknown): boolean | undefined {
|
|
16
|
-
return typeof value === "boolean" ? value : undefined
|
|
17
|
-
}
|
|
18
|
-
|
|
19
18
|
export function recordArray(value: unknown): readonly Record<string, unknown>[] {
|
|
20
19
|
if (!Array.isArray(value)) return []
|
|
21
20
|
return value.filter(isRecord)
|
|
@@ -56,6 +55,50 @@ function apiKeyFromCredentialRecord(value: unknown): string | undefined {
|
|
|
56
55
|
return stringValue(value.key) ?? stringValue(value.access)
|
|
57
56
|
}
|
|
58
57
|
|
|
58
|
+
function imageParts(value: unknown): readonly Record<string, unknown>[] {
|
|
59
|
+
if (isRecord(value)) return value.type === "image" ? [value] : []
|
|
60
|
+
return recordArray(value).filter((part) => part.type === "image")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function imageContentError(role: string): Error {
|
|
64
|
+
return new Error(`Selected Command Code model does not support image content in ${role}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function assertTextOnlyMessages(messages?: readonly MessageLike[]): void {
|
|
68
|
+
for (const message of messages ?? []) {
|
|
69
|
+
if (imageParts(message.content).length > 0) {
|
|
70
|
+
const role = message.role === "toolResult" ? "tool results" : `${message.role} messages`
|
|
71
|
+
throw imageContentError(role)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function imageToCommandCode(part: Record<string, unknown>): Record<string, string> {
|
|
77
|
+
const data = stringValue(part.data)
|
|
78
|
+
const mimeType = stringValue(part.mimeType)
|
|
79
|
+
if (!data || !mimeType)
|
|
80
|
+
throw new Error("Invalid image content: expected base64 data and mimeType")
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
type: "image",
|
|
84
|
+
image: `data:${mimeType};base64,${data}`,
|
|
85
|
+
mimeType,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function userContentToCommandCode(content: unknown, allowImages: boolean): unknown {
|
|
90
|
+
if (typeof content === "string") return content
|
|
91
|
+
|
|
92
|
+
return recordArray(content).flatMap((part) => {
|
|
93
|
+
if (part.type === "text") return [{ type: "text", text: stringValue(part.text) ?? "" }]
|
|
94
|
+
if (part.type === "image") {
|
|
95
|
+
if (!allowImages) throw imageContentError("user messages")
|
|
96
|
+
return [imageToCommandCode(part)]
|
|
97
|
+
}
|
|
98
|
+
return []
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
59
102
|
export function getApiKey(
|
|
60
103
|
options: {
|
|
61
104
|
env?: NodeJS.ProcessEnv
|
|
@@ -106,80 +149,6 @@ export function getEnvironmentInfo(): string {
|
|
|
106
149
|
return `${process.platform}-${process.arch}, Node.js ${process.version}`
|
|
107
150
|
}
|
|
108
151
|
|
|
109
|
-
export function toJsonSchema(schema: unknown): unknown {
|
|
110
|
-
if (!isRecord(schema)) return {}
|
|
111
|
-
|
|
112
|
-
const kind = stringValue(schema.kind) ?? stringValue(schema.type)
|
|
113
|
-
const enumValues = Array.isArray(schema.enum) ? schema.enum : undefined
|
|
114
|
-
if (enumValues) {
|
|
115
|
-
return { type: typeof enumValues[0], enum: enumValues }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
switch (kind) {
|
|
119
|
-
case "string":
|
|
120
|
-
case "String":
|
|
121
|
-
return { type: "string" }
|
|
122
|
-
case "number":
|
|
123
|
-
case "Number":
|
|
124
|
-
return { type: "number" }
|
|
125
|
-
case "boolean":
|
|
126
|
-
case "Boolean":
|
|
127
|
-
return { type: "boolean" }
|
|
128
|
-
case "object":
|
|
129
|
-
case "Object": {
|
|
130
|
-
const properties: Record<string, unknown> = {}
|
|
131
|
-
const inferredRequired: string[] = []
|
|
132
|
-
const sourceProperties = isRecord(schema.properties) ? schema.properties : undefined
|
|
133
|
-
const optional = Array.isArray(schema.optional)
|
|
134
|
-
? schema.optional.filter((item): item is string => typeof item === "string")
|
|
135
|
-
: []
|
|
136
|
-
|
|
137
|
-
if (sourceProperties) {
|
|
138
|
-
for (const [key, value] of Object.entries(sourceProperties)) {
|
|
139
|
-
properties[key] = toJsonSchema(value)
|
|
140
|
-
const valueRecord = isRecord(value) ? value : undefined
|
|
141
|
-
if (booleanValue(valueRecord?.optional) !== true && !optional.includes(key)) {
|
|
142
|
-
inferredRequired.push(key)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const explicitRequired = Array.isArray(schema.required)
|
|
148
|
-
? schema.required.filter((item): item is string => typeof item === "string")
|
|
149
|
-
: undefined
|
|
150
|
-
const required = explicitRequired ?? inferredRequired
|
|
151
|
-
const out: Record<string, unknown> = { type: "object" }
|
|
152
|
-
if (Object.keys(properties).length > 0) out.properties = properties
|
|
153
|
-
if (required.length > 0) out.required = required
|
|
154
|
-
return out
|
|
155
|
-
}
|
|
156
|
-
case "array":
|
|
157
|
-
case "Array":
|
|
158
|
-
return {
|
|
159
|
-
type: "array",
|
|
160
|
-
items: toJsonSchema(schema.items ?? schema.element),
|
|
161
|
-
}
|
|
162
|
-
case "union":
|
|
163
|
-
case "Union": {
|
|
164
|
-
const variants = Array.isArray(schema.variants)
|
|
165
|
-
? schema.variants
|
|
166
|
-
: Array.isArray(schema.anyOf)
|
|
167
|
-
? schema.anyOf
|
|
168
|
-
: []
|
|
169
|
-
for (const variant of variants) {
|
|
170
|
-
const converted = toJsonSchema(variant)
|
|
171
|
-
if (isRecord(converted) && Object.keys(converted).length > 0) return converted
|
|
172
|
-
}
|
|
173
|
-
return {}
|
|
174
|
-
}
|
|
175
|
-
case "optional":
|
|
176
|
-
case "Optional":
|
|
177
|
-
return toJsonSchema(schema.wrapped ?? schema.inner)
|
|
178
|
-
default:
|
|
179
|
-
return {}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
152
|
export function toolsToJson(tools?: readonly ToolLike[]): unknown[] {
|
|
184
153
|
if (!tools) return []
|
|
185
154
|
return tools.map((tool) => ({
|
|
@@ -210,7 +179,13 @@ function completeToolCallIds(messages?: readonly MessageLike[]): Set<string> {
|
|
|
210
179
|
return new Set([...callIds].filter((id) => resultIds.has(id)))
|
|
211
180
|
}
|
|
212
181
|
|
|
213
|
-
export function messagesToCC(
|
|
182
|
+
export function messagesToCC(
|
|
183
|
+
messages?: readonly MessageLike[],
|
|
184
|
+
options: { allowImages?: boolean } = {},
|
|
185
|
+
): unknown[] {
|
|
186
|
+
const allowImages = options.allowImages ?? false
|
|
187
|
+
if (!allowImages) assertTextOnlyMessages(messages)
|
|
188
|
+
|
|
214
189
|
const out: unknown[] = []
|
|
215
190
|
const pairedToolCallIds = completeToolCallIds(messages)
|
|
216
191
|
|
|
@@ -218,18 +193,13 @@ export function messagesToCC(messages?: readonly MessageLike[]): unknown[] {
|
|
|
218
193
|
if (message.role === "user") {
|
|
219
194
|
out.push({
|
|
220
195
|
role: "user",
|
|
221
|
-
content:
|
|
196
|
+
content: userContentToCommandCode(message.content, allowImages),
|
|
222
197
|
})
|
|
223
198
|
} else if (message.role === "assistant") {
|
|
224
199
|
const parts: unknown[] = []
|
|
225
200
|
for (const content of recordArray(message.content)) {
|
|
226
201
|
if (content.type === "text") {
|
|
227
202
|
parts.push({ type: "text", text: stringValue(content.text) ?? "" })
|
|
228
|
-
} else if (content.type === "thinking") {
|
|
229
|
-
parts.push({
|
|
230
|
-
type: "reasoning",
|
|
231
|
-
text: stringValue(content.thinking) ?? "",
|
|
232
|
-
})
|
|
233
203
|
} else if (content.type === "toolCall") {
|
|
234
204
|
const toolCallId = stringValue(content.id) ?? ""
|
|
235
205
|
if (!pairedToolCallIds.has(toolCallId)) continue
|
|
@@ -257,6 +227,15 @@ export function messagesToCC(messages?: readonly MessageLike[]): unknown[] {
|
|
|
257
227
|
},
|
|
258
228
|
],
|
|
259
229
|
})
|
|
230
|
+
|
|
231
|
+
const images = imageParts(message.content)
|
|
232
|
+
if (images.length > 0) {
|
|
233
|
+
if (!allowImages) throw imageContentError("tool results")
|
|
234
|
+
out.push({
|
|
235
|
+
role: "user",
|
|
236
|
+
content: images.map(imageToCommandCode),
|
|
237
|
+
})
|
|
238
|
+
}
|
|
260
239
|
}
|
|
261
240
|
}
|
|
262
241
|
return out
|
package/src/core.ts
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
import { randomUUID } from "node:crypto"
|
|
9
9
|
|
|
10
|
+
import { commandCodeErrorMessage, redactCommandCodeErrorText } from "./overflow.ts"
|
|
11
|
+
import { modelSupportsImageInput } from "./models.ts"
|
|
10
12
|
import {
|
|
11
13
|
getApiKey,
|
|
12
14
|
getEnvironmentInfo,
|
|
13
15
|
isRecord,
|
|
16
|
+
assertTextOnlyMessages,
|
|
14
17
|
mapFinishReason,
|
|
15
18
|
messagesToCC,
|
|
16
19
|
numberValue,
|
|
@@ -36,10 +39,11 @@ import type {
|
|
|
36
39
|
} from "./types.ts"
|
|
37
40
|
|
|
38
41
|
export * from "./converters.ts"
|
|
42
|
+
export * from "./overflow.ts"
|
|
39
43
|
export * from "./types.ts"
|
|
40
44
|
|
|
41
45
|
export const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
|
42
|
-
export const COMMAND_CODE_CLI_VERSION = "
|
|
46
|
+
export const COMMAND_CODE_CLI_VERSION = "1.15.1"
|
|
43
47
|
|
|
44
48
|
const DEFAULT_GENERATE_MAX_TOKENS = 64_000
|
|
45
49
|
const DEFAULT_MAX_RETRIES = 0
|
|
@@ -134,6 +138,15 @@ function generateMaxTokens(model: ModelLike, options?: StreamOptions): number {
|
|
|
134
138
|
)
|
|
135
139
|
}
|
|
136
140
|
|
|
141
|
+
function mappedReasoningEffort(model: ModelLike, options?: StreamOptions): string | undefined {
|
|
142
|
+
const level = options?.reasoning
|
|
143
|
+
if (!level || level === "off" || !model.reasoning) return undefined
|
|
144
|
+
|
|
145
|
+
const effortMap = model.thinking?.effortMap ?? model.thinkingLevelMap
|
|
146
|
+
const mapped = effortMap?.[level]
|
|
147
|
+
return typeof mapped === "string" && mapped !== "off" ? mapped : undefined
|
|
148
|
+
}
|
|
149
|
+
|
|
137
150
|
export function projectSlugFromPath(pathName: string): string {
|
|
138
151
|
const slug = pathName
|
|
139
152
|
.toLowerCase()
|
|
@@ -185,6 +198,31 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
185
198
|
})
|
|
186
199
|
}
|
|
187
200
|
|
|
201
|
+
function raceAbortWithTimeout<T>(
|
|
202
|
+
promise: Promise<T>,
|
|
203
|
+
controller: AbortController,
|
|
204
|
+
timeoutMs: number | undefined,
|
|
205
|
+
): Promise<T> {
|
|
206
|
+
if (timeoutMs === undefined) return raceAbort(promise, controller.signal)
|
|
207
|
+
|
|
208
|
+
return new Promise<T>((resolve, reject) => {
|
|
209
|
+
const timer = setTimeout(() => {
|
|
210
|
+
controller.abort()
|
|
211
|
+
reject(timeoutError(timeoutMs))
|
|
212
|
+
}, timeoutMs)
|
|
213
|
+
raceAbort(promise, controller.signal).then(
|
|
214
|
+
(value) => {
|
|
215
|
+
clearTimeout(timer)
|
|
216
|
+
resolve(value)
|
|
217
|
+
},
|
|
218
|
+
(error: unknown) => {
|
|
219
|
+
clearTimeout(timer)
|
|
220
|
+
reject(error)
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
188
226
|
return function streamCommandCode(
|
|
189
227
|
model: ModelLike,
|
|
190
228
|
context: ContextLike,
|
|
@@ -388,10 +426,14 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
388
426
|
const usage = commandCodeUsage(event)
|
|
389
427
|
if (usage) {
|
|
390
428
|
const details = commandCodeInputTokenDetails(usage)
|
|
391
|
-
|
|
429
|
+
const totalInput = numberValue(usage.inputTokens) ?? 0
|
|
430
|
+
const input = numberValue(details?.noCacheTokens)
|
|
431
|
+
const cacheRead = numberValue(details?.cacheReadTokens) ?? 0
|
|
432
|
+
const cacheWrite = numberValue(details?.cacheWriteTokens) ?? 0
|
|
433
|
+
output.usage.input = input ?? Math.max(0, totalInput - cacheRead - cacheWrite)
|
|
392
434
|
output.usage.output = numberValue(usage.outputTokens) ?? 0
|
|
393
|
-
output.usage.cacheRead =
|
|
394
|
-
output.usage.cacheWrite =
|
|
435
|
+
output.usage.cacheRead = cacheRead
|
|
436
|
+
output.usage.cacheWrite = cacheWrite
|
|
395
437
|
output.usage.totalTokens =
|
|
396
438
|
output.usage.input +
|
|
397
439
|
output.usage.output +
|
|
@@ -405,9 +447,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
405
447
|
}
|
|
406
448
|
|
|
407
449
|
case "error": {
|
|
408
|
-
const errorRecord = isRecord(event.error) ? event.error : undefined
|
|
409
450
|
const message =
|
|
410
|
-
|
|
451
|
+
commandCodeErrorMessage(event.error) ??
|
|
452
|
+
commandCodeErrorMessage(event.message) ??
|
|
453
|
+
"Stream error"
|
|
411
454
|
output.stopReason = "error"
|
|
412
455
|
output.errorMessage = message
|
|
413
456
|
throw new Error(message)
|
|
@@ -417,9 +460,15 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
417
460
|
|
|
418
461
|
try {
|
|
419
462
|
stream.push({ type: "start", partial: output })
|
|
463
|
+
if (controller.signal.aborted) throw abortError("Aborted")
|
|
420
464
|
|
|
421
465
|
const workingDir = cwd()
|
|
422
466
|
const threadId = uuid()
|
|
467
|
+
const reasoningEffort = mappedReasoningEffort(model, options)
|
|
468
|
+
const timeoutMs = options?.timeoutMs
|
|
469
|
+
|
|
470
|
+
const allowImages = modelSupportsImageInput(model.id)
|
|
471
|
+
if (!allowImages) assertTextOnlyMessages(context.messages)
|
|
423
472
|
|
|
424
473
|
let body: unknown = {
|
|
425
474
|
config: {
|
|
@@ -438,25 +487,34 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
438
487
|
skills: null,
|
|
439
488
|
params: {
|
|
440
489
|
model: model.id,
|
|
441
|
-
messages: messagesToCC(context.messages),
|
|
490
|
+
messages: messagesToCC(context.messages, { allowImages }),
|
|
442
491
|
tools: toolsToJson(context.tools),
|
|
443
492
|
system: systemPromptToText(context.systemPrompt),
|
|
444
493
|
max_tokens: generateMaxTokens(model, options),
|
|
445
494
|
temperature: 0.3,
|
|
446
495
|
stream: true,
|
|
496
|
+
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
|
|
447
497
|
},
|
|
448
498
|
threadId,
|
|
449
499
|
}
|
|
450
500
|
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
501
|
+
const payloadController = new AbortController()
|
|
502
|
+
const onPayloadAbort = () => payloadController.abort()
|
|
503
|
+
controller.signal.addEventListener("abort", onPayloadAbort, { once: true })
|
|
504
|
+
let nextBody: unknown
|
|
505
|
+
try {
|
|
506
|
+
nextBody = await raceAbortWithTimeout(
|
|
507
|
+
Promise.resolve(options?.onPayload?.(body, model)),
|
|
508
|
+
payloadController,
|
|
509
|
+
timeoutMs,
|
|
510
|
+
)
|
|
511
|
+
} finally {
|
|
512
|
+
controller.signal.removeEventListener("abort", onPayloadAbort)
|
|
513
|
+
}
|
|
455
514
|
if (nextBody !== undefined) body = nextBody
|
|
456
515
|
|
|
457
516
|
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES
|
|
458
517
|
const maxRetryDelayMs = effectiveMaxRetryDelayMs(options?.maxRetryDelayMs)
|
|
459
|
-
const timeoutMs = options?.timeoutMs
|
|
460
518
|
const requestHeaders = {
|
|
461
519
|
"Content-Type": "application/json",
|
|
462
520
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -490,6 +548,11 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
490
548
|
}
|
|
491
549
|
const onOuterAbort = () => attemptController.abort()
|
|
492
550
|
controller.signal.addEventListener("abort", onOuterAbort, { once: true })
|
|
551
|
+
const raceAttempt = <T>(promise: Promise<T>): Promise<T> =>
|
|
552
|
+
raceAbort(promise, attemptController.signal).catch((error: unknown) => {
|
|
553
|
+
if (attemptTimedOut) throw timeoutError(timeoutMs)
|
|
554
|
+
throw error
|
|
555
|
+
})
|
|
493
556
|
|
|
494
557
|
try {
|
|
495
558
|
try {
|
|
@@ -525,25 +588,38 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
525
588
|
}
|
|
526
589
|
}
|
|
527
590
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
591
|
+
try {
|
|
592
|
+
await raceAttempt(
|
|
593
|
+
Promise.resolve(
|
|
594
|
+
options?.onResponse?.(
|
|
595
|
+
{
|
|
596
|
+
status: response.status,
|
|
597
|
+
headers: headersToRecord(response.headers),
|
|
598
|
+
},
|
|
599
|
+
model,
|
|
600
|
+
),
|
|
536
601
|
),
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
|
|
602
|
+
)
|
|
603
|
+
} catch (error: unknown) {
|
|
604
|
+
if (attemptTimedOut && attempt < maxRetries) continue retryLoop
|
|
605
|
+
throw error
|
|
606
|
+
}
|
|
540
607
|
|
|
541
608
|
if (!response.ok) {
|
|
542
|
-
const errBody = await
|
|
543
|
-
|
|
544
|
-
|
|
609
|
+
const errBody = await raceAttempt(response.text().catch(() => ""))
|
|
610
|
+
let errorDetail: string | undefined
|
|
611
|
+
try {
|
|
612
|
+
const parsedBody: unknown = JSON.parse(errBody)
|
|
613
|
+
errorDetail = commandCodeErrorMessage(parsedBody)
|
|
614
|
+
} catch {
|
|
615
|
+
// Preserve useful plain-text provider errors only after secret
|
|
616
|
+
// redaction; upstream/proxy bodies may echo credentials.
|
|
617
|
+
}
|
|
618
|
+
const safeBody = redactCommandCodeErrorText(errBody).slice(0, 500)
|
|
619
|
+
const detail = redactCommandCodeErrorText(
|
|
620
|
+
errorDetail ?? (safeBody || "Provider returned an error"),
|
|
545
621
|
)
|
|
546
|
-
throw new Error(`Command Code API error ${response.status}: ${
|
|
622
|
+
throw new Error(`Command Code API error ${response.status}: ${detail}`)
|
|
547
623
|
}
|
|
548
624
|
|
|
549
625
|
// --- Read response stream ---
|
|
@@ -624,9 +700,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
624
700
|
output.errorMessage =
|
|
625
701
|
reason === "aborted"
|
|
626
702
|
? "Request aborted"
|
|
627
|
-
: error instanceof Error
|
|
628
|
-
? error.message
|
|
629
|
-
: String(error)
|
|
703
|
+
: redactCommandCodeErrorText(error instanceof Error ? error.message : String(error))
|
|
630
704
|
stream.push({ type: "error", reason, error: output })
|
|
631
705
|
stream.end()
|
|
632
706
|
} finally {
|
|
@@ -653,7 +727,9 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
653
727
|
model: model.id,
|
|
654
728
|
usage: defaultUsage(),
|
|
655
729
|
stopReason: "error",
|
|
656
|
-
errorMessage:
|
|
730
|
+
errorMessage: redactCommandCodeErrorText(
|
|
731
|
+
error instanceof Error ? error.message : String(error),
|
|
732
|
+
),
|
|
657
733
|
timestamp: now(),
|
|
658
734
|
}
|
|
659
735
|
stream.push({ type: "error", reason: "error", error: msg })
|
package/src/cost.ts
CHANGED
|
@@ -10,10 +10,22 @@
|
|
|
10
10
|
import type { ModelLike, Usage } from "./types.ts"
|
|
11
11
|
|
|
12
12
|
export function calculateCommandCodeCost(model: ModelLike, usage: Usage): void {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const inputTokens = usage.input + usage.cacheRead + usage.cacheWrite
|
|
14
|
+
let rates = model.cost
|
|
15
|
+
let matchedThreshold = -1
|
|
16
|
+
for (const tier of model.cost.tiers ?? []) {
|
|
17
|
+
if (inputTokens > tier.inputTokensAbove && tier.inputTokensAbove > matchedThreshold) {
|
|
18
|
+
rates = tier
|
|
19
|
+
matchedThreshold = tier.inputTokensAbove
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const longWrite = usage.cacheWrite1h ?? 0
|
|
24
|
+
const shortWrite = usage.cacheWrite - longWrite
|
|
25
|
+
usage.cost.input = (rates.input / 1_000_000) * usage.input
|
|
26
|
+
usage.cost.output = (rates.output / 1_000_000) * usage.output
|
|
27
|
+
usage.cost.cacheRead = (rates.cacheRead / 1_000_000) * usage.cacheRead
|
|
28
|
+
usage.cost.cacheWrite = (rates.cacheWrite * shortWrite + rates.input * 2 * longWrite) / 1_000_000
|
|
17
29
|
usage.cost.total =
|
|
18
30
|
usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite
|
|
19
31
|
}
|