pi-commandcode-provider 0.4.3 → 0.5.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 +19 -0
- package/CONTRIBUTING.md +18 -0
- package/README.md +100 -72
- package/index.ts +89 -92
- package/package.json +15 -6
- package/scripts/pi-authenticated.mjs +49 -0
- package/scripts/pi-isolated.mjs +78 -0
- package/src/converters.ts +27 -83
- package/src/core.ts +109 -29
- package/src/cost.ts +16 -4
- package/src/json-schema.ts +382 -0
- package/src/models.ts +194 -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,26 @@ function apiKeyFromCredentialRecord(value: unknown): string | undefined {
|
|
|
56
55
|
return stringValue(value.key) ?? stringValue(value.access)
|
|
57
56
|
}
|
|
58
57
|
|
|
58
|
+
function hasImageContent(value: unknown): boolean {
|
|
59
|
+
if (isRecord(value)) return value.type === "image"
|
|
60
|
+
return recordArray(value).some((part) => part.type === "image")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function imageContentError(role: string): Error {
|
|
64
|
+
return new Error(
|
|
65
|
+
`Command Code does not support image content in ${role}; refusing to send it to avoid lossy handling`,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function assertTextOnlyMessages(messages?: readonly MessageLike[]): void {
|
|
70
|
+
for (const message of messages ?? []) {
|
|
71
|
+
if (hasImageContent(message.content)) {
|
|
72
|
+
const role = message.role === "toolResult" ? "tool results" : `${message.role} messages`
|
|
73
|
+
throw imageContentError(role)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
export function getApiKey(
|
|
60
79
|
options: {
|
|
61
80
|
env?: NodeJS.ProcessEnv
|
|
@@ -96,6 +115,8 @@ export function getApiKey(
|
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
export function textContent(message: { content?: unknown }): string {
|
|
118
|
+
if (hasImageContent(message.content)) throw imageContentError("tool results")
|
|
119
|
+
|
|
99
120
|
return recordArray(message.content)
|
|
100
121
|
.filter((part) => part.type === "text")
|
|
101
122
|
.map((part) => stringValue(part.text) ?? "")
|
|
@@ -106,80 +127,6 @@ export function getEnvironmentInfo(): string {
|
|
|
106
127
|
return `${process.platform}-${process.arch}, Node.js ${process.version}`
|
|
107
128
|
}
|
|
108
129
|
|
|
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
130
|
export function toolsToJson(tools?: readonly ToolLike[]): unknown[] {
|
|
184
131
|
if (!tools) return []
|
|
185
132
|
return tools.map((tool) => ({
|
|
@@ -211,6 +158,8 @@ function completeToolCallIds(messages?: readonly MessageLike[]): Set<string> {
|
|
|
211
158
|
}
|
|
212
159
|
|
|
213
160
|
export function messagesToCC(messages?: readonly MessageLike[]): unknown[] {
|
|
161
|
+
assertTextOnlyMessages(messages)
|
|
162
|
+
|
|
214
163
|
const out: unknown[] = []
|
|
215
164
|
const pairedToolCallIds = completeToolCallIds(messages)
|
|
216
165
|
|
|
@@ -225,11 +174,6 @@ export function messagesToCC(messages?: readonly MessageLike[]): unknown[] {
|
|
|
225
174
|
for (const content of recordArray(message.content)) {
|
|
226
175
|
if (content.type === "text") {
|
|
227
176
|
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
177
|
} else if (content.type === "toolCall") {
|
|
234
178
|
const toolCallId = stringValue(content.id) ?? ""
|
|
235
179
|
if (!pairedToolCallIds.has(toolCallId)) continue
|
package/src/core.ts
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
import { randomUUID } from "node:crypto"
|
|
9
9
|
|
|
10
|
+
import { commandCodeErrorMessage, redactCommandCodeErrorText } from "./overflow.ts"
|
|
10
11
|
import {
|
|
11
12
|
getApiKey,
|
|
12
13
|
getEnvironmentInfo,
|
|
13
14
|
isRecord,
|
|
15
|
+
assertTextOnlyMessages,
|
|
14
16
|
mapFinishReason,
|
|
15
17
|
messagesToCC,
|
|
16
18
|
numberValue,
|
|
@@ -36,10 +38,17 @@ import type {
|
|
|
36
38
|
} from "./types.ts"
|
|
37
39
|
|
|
38
40
|
export * from "./converters.ts"
|
|
41
|
+
export * from "./overflow.ts"
|
|
39
42
|
export * from "./types.ts"
|
|
40
43
|
|
|
41
44
|
export const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
|
42
45
|
export const COMMAND_CODE_CLI_VERSION = "0.29.0"
|
|
46
|
+
/**
|
|
47
|
+
* The legacy /alpha/generate request path used by this provider has no
|
|
48
|
+
* documented image-part contract. Keep the advertised capability text-only
|
|
49
|
+
* until Command Code documents and tests image handling for this endpoint.
|
|
50
|
+
*/
|
|
51
|
+
export const COMMAND_CODE_INPUT_TYPES = ["text"] as const
|
|
43
52
|
|
|
44
53
|
const DEFAULT_GENERATE_MAX_TOKENS = 64_000
|
|
45
54
|
const DEFAULT_MAX_RETRIES = 0
|
|
@@ -134,6 +143,15 @@ function generateMaxTokens(model: ModelLike, options?: StreamOptions): number {
|
|
|
134
143
|
)
|
|
135
144
|
}
|
|
136
145
|
|
|
146
|
+
function mappedReasoningEffort(model: ModelLike, options?: StreamOptions): string | undefined {
|
|
147
|
+
const level = options?.reasoning
|
|
148
|
+
if (!level || level === "off" || !model.reasoning) return undefined
|
|
149
|
+
|
|
150
|
+
const effortMap = model.thinking?.effortMap ?? model.thinkingLevelMap
|
|
151
|
+
const mapped = effortMap?.[level]
|
|
152
|
+
return typeof mapped === "string" && mapped !== "off" ? mapped : undefined
|
|
153
|
+
}
|
|
154
|
+
|
|
137
155
|
export function projectSlugFromPath(pathName: string): string {
|
|
138
156
|
const slug = pathName
|
|
139
157
|
.toLowerCase()
|
|
@@ -185,6 +203,31 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
185
203
|
})
|
|
186
204
|
}
|
|
187
205
|
|
|
206
|
+
function raceAbortWithTimeout<T>(
|
|
207
|
+
promise: Promise<T>,
|
|
208
|
+
controller: AbortController,
|
|
209
|
+
timeoutMs: number | undefined,
|
|
210
|
+
): Promise<T> {
|
|
211
|
+
if (timeoutMs === undefined) return raceAbort(promise, controller.signal)
|
|
212
|
+
|
|
213
|
+
return new Promise<T>((resolve, reject) => {
|
|
214
|
+
const timer = setTimeout(() => {
|
|
215
|
+
controller.abort()
|
|
216
|
+
reject(timeoutError(timeoutMs))
|
|
217
|
+
}, timeoutMs)
|
|
218
|
+
raceAbort(promise, controller.signal).then(
|
|
219
|
+
(value) => {
|
|
220
|
+
clearTimeout(timer)
|
|
221
|
+
resolve(value)
|
|
222
|
+
},
|
|
223
|
+
(error: unknown) => {
|
|
224
|
+
clearTimeout(timer)
|
|
225
|
+
reject(error)
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
188
231
|
return function streamCommandCode(
|
|
189
232
|
model: ModelLike,
|
|
190
233
|
context: ContextLike,
|
|
@@ -388,10 +431,14 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
388
431
|
const usage = commandCodeUsage(event)
|
|
389
432
|
if (usage) {
|
|
390
433
|
const details = commandCodeInputTokenDetails(usage)
|
|
391
|
-
|
|
434
|
+
const totalInput = numberValue(usage.inputTokens) ?? 0
|
|
435
|
+
const input = numberValue(details?.noCacheTokens)
|
|
436
|
+
const cacheRead = numberValue(details?.cacheReadTokens) ?? 0
|
|
437
|
+
const cacheWrite = numberValue(details?.cacheWriteTokens) ?? 0
|
|
438
|
+
output.usage.input = input ?? Math.max(0, totalInput - cacheRead - cacheWrite)
|
|
392
439
|
output.usage.output = numberValue(usage.outputTokens) ?? 0
|
|
393
|
-
output.usage.cacheRead =
|
|
394
|
-
output.usage.cacheWrite =
|
|
440
|
+
output.usage.cacheRead = cacheRead
|
|
441
|
+
output.usage.cacheWrite = cacheWrite
|
|
395
442
|
output.usage.totalTokens =
|
|
396
443
|
output.usage.input +
|
|
397
444
|
output.usage.output +
|
|
@@ -405,9 +452,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
405
452
|
}
|
|
406
453
|
|
|
407
454
|
case "error": {
|
|
408
|
-
const errorRecord = isRecord(event.error) ? event.error : undefined
|
|
409
455
|
const message =
|
|
410
|
-
|
|
456
|
+
commandCodeErrorMessage(event.error) ??
|
|
457
|
+
commandCodeErrorMessage(event.message) ??
|
|
458
|
+
"Stream error"
|
|
411
459
|
output.stopReason = "error"
|
|
412
460
|
output.errorMessage = message
|
|
413
461
|
throw new Error(message)
|
|
@@ -417,9 +465,14 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
417
465
|
|
|
418
466
|
try {
|
|
419
467
|
stream.push({ type: "start", partial: output })
|
|
468
|
+
if (controller.signal.aborted) throw abortError("Aborted")
|
|
420
469
|
|
|
421
470
|
const workingDir = cwd()
|
|
422
471
|
const threadId = uuid()
|
|
472
|
+
const reasoningEffort = mappedReasoningEffort(model, options)
|
|
473
|
+
const timeoutMs = options?.timeoutMs
|
|
474
|
+
|
|
475
|
+
assertTextOnlyMessages(context.messages)
|
|
423
476
|
|
|
424
477
|
let body: unknown = {
|
|
425
478
|
config: {
|
|
@@ -444,19 +497,28 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
444
497
|
max_tokens: generateMaxTokens(model, options),
|
|
445
498
|
temperature: 0.3,
|
|
446
499
|
stream: true,
|
|
500
|
+
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
|
|
447
501
|
},
|
|
448
502
|
threadId,
|
|
449
503
|
}
|
|
450
504
|
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
505
|
+
const payloadController = new AbortController()
|
|
506
|
+
const onPayloadAbort = () => payloadController.abort()
|
|
507
|
+
controller.signal.addEventListener("abort", onPayloadAbort, { once: true })
|
|
508
|
+
let nextBody: unknown
|
|
509
|
+
try {
|
|
510
|
+
nextBody = await raceAbortWithTimeout(
|
|
511
|
+
Promise.resolve(options?.onPayload?.(body, model)),
|
|
512
|
+
payloadController,
|
|
513
|
+
timeoutMs,
|
|
514
|
+
)
|
|
515
|
+
} finally {
|
|
516
|
+
controller.signal.removeEventListener("abort", onPayloadAbort)
|
|
517
|
+
}
|
|
455
518
|
if (nextBody !== undefined) body = nextBody
|
|
456
519
|
|
|
457
520
|
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES
|
|
458
521
|
const maxRetryDelayMs = effectiveMaxRetryDelayMs(options?.maxRetryDelayMs)
|
|
459
|
-
const timeoutMs = options?.timeoutMs
|
|
460
522
|
const requestHeaders = {
|
|
461
523
|
"Content-Type": "application/json",
|
|
462
524
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -490,6 +552,11 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
490
552
|
}
|
|
491
553
|
const onOuterAbort = () => attemptController.abort()
|
|
492
554
|
controller.signal.addEventListener("abort", onOuterAbort, { once: true })
|
|
555
|
+
const raceAttempt = <T>(promise: Promise<T>): Promise<T> =>
|
|
556
|
+
raceAbort(promise, attemptController.signal).catch((error: unknown) => {
|
|
557
|
+
if (attemptTimedOut) throw timeoutError(timeoutMs)
|
|
558
|
+
throw error
|
|
559
|
+
})
|
|
493
560
|
|
|
494
561
|
try {
|
|
495
562
|
try {
|
|
@@ -525,25 +592,38 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
525
592
|
}
|
|
526
593
|
}
|
|
527
594
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
595
|
+
try {
|
|
596
|
+
await raceAttempt(
|
|
597
|
+
Promise.resolve(
|
|
598
|
+
options?.onResponse?.(
|
|
599
|
+
{
|
|
600
|
+
status: response.status,
|
|
601
|
+
headers: headersToRecord(response.headers),
|
|
602
|
+
},
|
|
603
|
+
model,
|
|
604
|
+
),
|
|
536
605
|
),
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
|
|
606
|
+
)
|
|
607
|
+
} catch (error: unknown) {
|
|
608
|
+
if (attemptTimedOut && attempt < maxRetries) continue retryLoop
|
|
609
|
+
throw error
|
|
610
|
+
}
|
|
540
611
|
|
|
541
612
|
if (!response.ok) {
|
|
542
|
-
const errBody = await
|
|
543
|
-
|
|
544
|
-
|
|
613
|
+
const errBody = await raceAttempt(response.text().catch(() => ""))
|
|
614
|
+
let errorDetail: string | undefined
|
|
615
|
+
try {
|
|
616
|
+
const parsedBody: unknown = JSON.parse(errBody)
|
|
617
|
+
errorDetail = commandCodeErrorMessage(parsedBody)
|
|
618
|
+
} catch {
|
|
619
|
+
// Preserve useful plain-text provider errors only after secret
|
|
620
|
+
// redaction; upstream/proxy bodies may echo credentials.
|
|
621
|
+
}
|
|
622
|
+
const safeBody = redactCommandCodeErrorText(errBody).slice(0, 500)
|
|
623
|
+
const detail = redactCommandCodeErrorText(
|
|
624
|
+
errorDetail ?? (safeBody || "Provider returned an error"),
|
|
545
625
|
)
|
|
546
|
-
throw new Error(`Command Code API error ${response.status}: ${
|
|
626
|
+
throw new Error(`Command Code API error ${response.status}: ${detail}`)
|
|
547
627
|
}
|
|
548
628
|
|
|
549
629
|
// --- Read response stream ---
|
|
@@ -624,9 +704,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
624
704
|
output.errorMessage =
|
|
625
705
|
reason === "aborted"
|
|
626
706
|
? "Request aborted"
|
|
627
|
-
: error instanceof Error
|
|
628
|
-
? error.message
|
|
629
|
-
: String(error)
|
|
707
|
+
: redactCommandCodeErrorText(error instanceof Error ? error.message : String(error))
|
|
630
708
|
stream.push({ type: "error", reason, error: output })
|
|
631
709
|
stream.end()
|
|
632
710
|
} finally {
|
|
@@ -653,7 +731,9 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
653
731
|
model: model.id,
|
|
654
732
|
usage: defaultUsage(),
|
|
655
733
|
stopReason: "error",
|
|
656
|
-
errorMessage:
|
|
734
|
+
errorMessage: redactCommandCodeErrorText(
|
|
735
|
+
error instanceof Error ? error.message : String(error),
|
|
736
|
+
),
|
|
657
737
|
timestamp: now(),
|
|
658
738
|
}
|
|
659
739
|
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
|
}
|