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
package/src/overflow.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const COMMAND_CODE_PROVIDER = "commandcode"
|
|
2
|
+
const CONTEXT_OVERFLOW_PREFIX = "context_length_exceeded:"
|
|
3
|
+
|
|
4
|
+
const COMMAND_CODE_OVERFLOW_PATTERNS = [
|
|
5
|
+
/\b(?:context[_\s-]*(?:length|window)|model[_\s-]*context[_\s-]*window)[_\s-]*(?:exceeded|overflow(?:ed)?|too[_\s-]*(?:large|long))\b/i,
|
|
6
|
+
/\b(?:context|prompt|input)[_\s-]*(?:length|window|size|tokens?|limit|maximum)\b[\s\S]{0,120}\b(?:exceed(?:ed|s)?|overflow(?:ed|s)?|too\s+(?:large|long)|(?:maximum|limit)\s+(?:reached|exceeded|hit))\b/i,
|
|
7
|
+
/\b(?:exceed(?:ed|s)?|overflow(?:ed|s)?|too\s+(?:large|long))\b[\s\S]{0,120}\b(?:context|prompt|input)[_\s-]*(?:length|window|size|tokens?|limit|maximum)\b/i,
|
|
8
|
+
/\b(?:prompt|input|context)\b[\s\S]{0,32}\btoo\s+(?:large|long)\b/i,
|
|
9
|
+
/\b(?:prompt|input)[_\s-]*too[_\s-]*(?:large|long)\b/i,
|
|
10
|
+
/\b(?:prompt|input)[_\s-]*tokens?[_\s-]*(?:limit|maximum|max)[_\s-]*(?:exceeded|reached)\b/i,
|
|
11
|
+
/\b(?:prompt|input)[_\s-]*(?:tokens?|length|size)\b[\s\S]{0,120}\b(?:limit|maximum)\b[\s\S]{0,40}\b(?:exceed(?:ed|s)?|reached|hit)\b/i,
|
|
12
|
+
/\b(?:maximum|limit)[_\s-]+(?:allowed[_\s-]+)?(?:context|prompt|input)[_\s-]*(?:length|window|size|tokens?)\b/i,
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
const NON_OVERFLOW_PATTERNS = [
|
|
16
|
+
/\brate[_\s-]*limit\b/i,
|
|
17
|
+
/\btoo\s+many\s+requests\b/i,
|
|
18
|
+
/\b(?:capacity|quota|throttl(?:e|ed|ing)?|concurren(?:cy|t)|overloaded)\b/i,
|
|
19
|
+
/\b(?:service|temporarily)\s+unavailable\b/i,
|
|
20
|
+
/\bstatus(?:[_\s-]*code)?\s*[:=]\s*429\b/i,
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const CONTEXT_OVERFLOW_PREFIX_PATTERN = /context_length_exceeded/i
|
|
24
|
+
|
|
25
|
+
const HTTP_RATE_LIMIT_STATUS_PATTERNS = [
|
|
26
|
+
/\b(?:api\s+error|http|status(?:[_\s-]*code)?|status[_\s-]*code)\s*[:(]?\s*429\b/i,
|
|
27
|
+
/["']?(?:status|status[_\s-]*code)["']?\s*:\s*429\b/i,
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi
|
|
31
|
+
const CREDENTIAL_PATTERN =
|
|
32
|
+
/\b(?:api[-_ ]?key|apikey|access[-_ ]?token|refresh[-_ ]?token|token|secret|password|authorization)\s*[=:]\s*[^\s,;)]+/gi
|
|
33
|
+
const USER_TOKEN_PATTERN = /\b(?:user|cc)_[A-Za-z0-9_-]{8,}\b/gi
|
|
34
|
+
const QUERY_SECRET_PATTERN =
|
|
35
|
+
/([?&](?:api[-_ ]?key|apikey|access_token|refresh_token|token|secret|password)=)[^&#\s]+/gi
|
|
36
|
+
const STANDALONE_SECRET_PATTERN =
|
|
37
|
+
/\b(?:sk|rk|ghp|github_pat|xox[baprs])[-_A-Za-z0-9]{16,}\b|\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g
|
|
38
|
+
|
|
39
|
+
export function redactCommandCodeErrorText(value: string): string {
|
|
40
|
+
return value
|
|
41
|
+
.replace(BEARER_PATTERN, "Bearer [redacted]")
|
|
42
|
+
.replace(CREDENTIAL_PATTERN, (match) => {
|
|
43
|
+
const separatorIndex = match.search(/[=:]/)
|
|
44
|
+
return separatorIndex < 0 ? "[redacted]" : `${match.slice(0, separatorIndex + 1)}[redacted]`
|
|
45
|
+
})
|
|
46
|
+
.replace(USER_TOKEN_PATTERN, "[redacted]")
|
|
47
|
+
.replace(QUERY_SECRET_PATTERN, "$1[redacted]")
|
|
48
|
+
.replace(STANDALONE_SECRET_PATTERN, "[redacted]")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
52
|
+
return typeof value === "object" && value !== null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CommandCodeMessageLike {
|
|
56
|
+
role: string
|
|
57
|
+
provider: string
|
|
58
|
+
stopReason: string
|
|
59
|
+
errorMessage?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function commandCodeErrorMessage(value: unknown): string | undefined {
|
|
63
|
+
if (typeof value === "string") return value
|
|
64
|
+
if (!isRecord(value)) return undefined
|
|
65
|
+
|
|
66
|
+
const record = value
|
|
67
|
+
const parts: string[] = []
|
|
68
|
+
for (const key of [
|
|
69
|
+
"message",
|
|
70
|
+
"errorMessage",
|
|
71
|
+
"error",
|
|
72
|
+
"detail",
|
|
73
|
+
"details",
|
|
74
|
+
"code",
|
|
75
|
+
"type",
|
|
76
|
+
"reason",
|
|
77
|
+
]) {
|
|
78
|
+
const part = commandCodeErrorMessage(record[key])
|
|
79
|
+
if (part && !parts.includes(part)) parts.push(part)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const key of ["status", "statusCode", "httpStatus"]) {
|
|
83
|
+
const status = record[key]
|
|
84
|
+
if (typeof status === "string" || typeof status === "number") {
|
|
85
|
+
const statusPart = `status: ${status}`
|
|
86
|
+
if (!parts.includes(statusPart)) parts.push(statusPart)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return parts.length > 0 ? redactCommandCodeErrorText(parts.join(": ")) : undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizeCommandCodeErrorMessage(
|
|
94
|
+
errorMessage: string | undefined,
|
|
95
|
+
): string | undefined {
|
|
96
|
+
if (!errorMessage) return undefined
|
|
97
|
+
if (CONTEXT_OVERFLOW_PREFIX_PATTERN.test(errorMessage)) return undefined
|
|
98
|
+
if (NON_OVERFLOW_PATTERNS.some((pattern) => pattern.test(errorMessage))) return undefined
|
|
99
|
+
if (HTTP_RATE_LIMIT_STATUS_PATTERNS.some((pattern) => pattern.test(errorMessage)))
|
|
100
|
+
return undefined
|
|
101
|
+
if (!COMMAND_CODE_OVERFLOW_PATTERNS.some((pattern) => pattern.test(errorMessage)))
|
|
102
|
+
return undefined
|
|
103
|
+
|
|
104
|
+
return `${CONTEXT_OVERFLOW_PREFIX} ${errorMessage}`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function normalizeCommandCodeMessage<T extends CommandCodeMessageLike>(
|
|
108
|
+
message: T,
|
|
109
|
+
modelProvider?: string,
|
|
110
|
+
): { message: T & { errorMessage: string } } | undefined {
|
|
111
|
+
if (message.role !== "assistant" || message.stopReason !== "error") return undefined
|
|
112
|
+
if (message.provider !== COMMAND_CODE_PROVIDER && modelProvider !== COMMAND_CODE_PROVIDER) {
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const errorMessage = normalizeCommandCodeErrorMessage(message.errorMessage)
|
|
117
|
+
if (!errorMessage) return undefined
|
|
118
|
+
|
|
119
|
+
return { message: { ...message, errorMessage } }
|
|
120
|
+
}
|
package/src/pricing.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
export interface CommandCodeModelCostRates {
|
|
2
|
+
input: number
|
|
3
|
+
output: number
|
|
4
|
+
cacheRead: number
|
|
5
|
+
cacheWrite: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CommandCodeModelCostTier extends CommandCodeModelCostRates {
|
|
9
|
+
inputTokensAbove: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CommandCodeModelCost extends CommandCodeModelCostRates {
|
|
13
|
+
tiers?: readonly CommandCodeModelCostTier[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TemporaryPricing {
|
|
17
|
+
models: readonly string[]
|
|
18
|
+
expiresOn: string
|
|
19
|
+
description: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const PRICING_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits"
|
|
23
|
+
export const PRICING_LAST_VERIFIED = "2026-08-04"
|
|
24
|
+
|
|
25
|
+
export const ZERO_MODEL_COST: CommandCodeModelCost = {
|
|
26
|
+
input: 0,
|
|
27
|
+
output: 0,
|
|
28
|
+
cacheRead: 0,
|
|
29
|
+
cacheWrite: 0,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Display prices in USD per million tokens.
|
|
34
|
+
*
|
|
35
|
+
* Context-dependent rates use pi's request-wide input pricing tiers. The
|
|
36
|
+
* highest threshold exceeded by input + cache reads + cache writes applies to
|
|
37
|
+
* the full request. The Command Code usage page remains authoritative for the
|
|
38
|
+
* amount billed for an individual request.
|
|
39
|
+
*/
|
|
40
|
+
export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
|
|
41
|
+
// Free models
|
|
42
|
+
"poolside/laguna-s-2.1-free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
43
|
+
"inclusionai/ling-3.0-flash-free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
44
|
+
|
|
45
|
+
// Open and open-weight models
|
|
46
|
+
"tencent/hy3-paid": { input: 0.14, output: 0.58, cacheRead: 0.035, cacheWrite: 0 },
|
|
47
|
+
"moonshotai/Kimi-K3": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
|
|
48
|
+
"moonshotai/Kimi-K2.7-Code": { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
|
|
49
|
+
"moonshotai/Kimi-K2.7-Code-Highspeed": {
|
|
50
|
+
input: 1.9,
|
|
51
|
+
output: 8,
|
|
52
|
+
cacheRead: 0.38,
|
|
53
|
+
cacheWrite: 0,
|
|
54
|
+
},
|
|
55
|
+
"moonshotai/Kimi-K2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 },
|
|
56
|
+
"moonshotai/Kimi-K2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0 },
|
|
57
|
+
"zai-org/GLM-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
|
|
58
|
+
"zai-org/GLM-5.2-Fast": { input: 3, output: 10.25, cacheRead: 0.5, cacheWrite: 0 },
|
|
59
|
+
"zai-org/GLM-5.1": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
|
|
60
|
+
"zai-org/GLM-5": { input: 1, output: 3.2, cacheRead: 0.2, cacheWrite: 0 },
|
|
61
|
+
"MiniMaxAI/MiniMax-M3": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
|
|
62
|
+
"MiniMaxAI/MiniMax-M2.7": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
|
|
63
|
+
"MiniMaxAI/MiniMax-M2.5": { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0 },
|
|
64
|
+
// Permanent 75% discount.
|
|
65
|
+
"deepseek/deepseek-v4-pro": {
|
|
66
|
+
input: 0.435,
|
|
67
|
+
output: 0.87,
|
|
68
|
+
cacheRead: 0.003625,
|
|
69
|
+
cacheWrite: 0,
|
|
70
|
+
},
|
|
71
|
+
"deepseek/deepseek-v4-flash": {
|
|
72
|
+
input: 0.14,
|
|
73
|
+
output: 0.28,
|
|
74
|
+
cacheRead: 0.0028,
|
|
75
|
+
cacheWrite: 0,
|
|
76
|
+
},
|
|
77
|
+
"Qwen/Qwen3.8-Max": { input: 2, output: 6, cacheRead: 0.25, cacheWrite: 2.5 },
|
|
78
|
+
"Qwen/Qwen3.7-Max": { input: 2.5, output: 7.5, cacheRead: 0.5, cacheWrite: 3.13 },
|
|
79
|
+
"Qwen/Qwen3.7-Plus": {
|
|
80
|
+
input: 0.4,
|
|
81
|
+
output: 1.6,
|
|
82
|
+
cacheRead: 0.08,
|
|
83
|
+
cacheWrite: 0.5,
|
|
84
|
+
tiers: [
|
|
85
|
+
{
|
|
86
|
+
inputTokensAbove: 256_000,
|
|
87
|
+
input: 1.2,
|
|
88
|
+
output: 4.8,
|
|
89
|
+
cacheRead: 0.24,
|
|
90
|
+
cacheWrite: 1.5,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
"Qwen/Qwen3.7-Flash": {
|
|
95
|
+
input: 0.03,
|
|
96
|
+
output: 0.13,
|
|
97
|
+
cacheRead: 0.006,
|
|
98
|
+
cacheWrite: 0.038,
|
|
99
|
+
tiers: [
|
|
100
|
+
{
|
|
101
|
+
inputTokensAbove: 32_000,
|
|
102
|
+
input: 0.1,
|
|
103
|
+
output: 0.4,
|
|
104
|
+
cacheRead: 0.02,
|
|
105
|
+
cacheWrite: 0.125,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
inputTokensAbove: 256_000,
|
|
109
|
+
input: 0.2,
|
|
110
|
+
output: 0.8,
|
|
111
|
+
cacheRead: 0.04,
|
|
112
|
+
cacheWrite: 0.25,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
"Qwen/Qwen3.6-Max-Preview": {
|
|
117
|
+
input: 1.3,
|
|
118
|
+
output: 7.8,
|
|
119
|
+
cacheRead: 0.26,
|
|
120
|
+
cacheWrite: 1.63,
|
|
121
|
+
},
|
|
122
|
+
"Qwen/Qwen3.6-Plus": { input: 0.5, output: 3, cacheRead: 0.1, cacheWrite: 0 },
|
|
123
|
+
"stepfun/Step-3.7-Flash": { input: 0.2, output: 1.15, cacheRead: 0.04, cacheWrite: 0 },
|
|
124
|
+
"stepfun/Step-3.5-Flash": { input: 0.1, output: 0.3, cacheRead: 0.02, cacheWrite: 0 },
|
|
125
|
+
// Permanent discounted rates.
|
|
126
|
+
"xiaomi/mimo-v2.5-pro": { input: 0.435, output: 0.87, cacheRead: 0.0036, cacheWrite: 0 },
|
|
127
|
+
"xiaomi/mimo-v2.5": { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 },
|
|
128
|
+
"nvidia/nemotron-3-ultra-550b-a55b": {
|
|
129
|
+
input: 0.6,
|
|
130
|
+
output: 2.4,
|
|
131
|
+
cacheRead: 0.12,
|
|
132
|
+
cacheWrite: 0,
|
|
133
|
+
},
|
|
134
|
+
"sakana/fugu-ultra": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
|
|
135
|
+
"thinkingmachines/inkling": { input: 1, output: 4.05, cacheRead: 0.17, cacheWrite: 0 },
|
|
136
|
+
"thinkingmachines/inkling-small": {
|
|
137
|
+
input: 0.5,
|
|
138
|
+
output: 1.2,
|
|
139
|
+
cacheRead: 0.1,
|
|
140
|
+
cacheWrite: 0,
|
|
141
|
+
},
|
|
142
|
+
"meta/muse-spark-1.1": { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
|
|
143
|
+
|
|
144
|
+
// Anthropic
|
|
145
|
+
// Introductory pricing through 2026-08-31.
|
|
146
|
+
"claude-sonnet-5": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
|
|
147
|
+
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
148
|
+
"claude-fable-5": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
|
|
149
|
+
"claude-opus-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
150
|
+
"claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
151
|
+
"claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
152
|
+
"claude-haiku-4-5-20251001": {
|
|
153
|
+
input: 1,
|
|
154
|
+
output: 5,
|
|
155
|
+
cacheRead: 0.1,
|
|
156
|
+
cacheWrite: 1.25,
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// OpenAI
|
|
160
|
+
"gpt-5.6-sol": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
161
|
+
// Discounted rates through 2026-08-14.
|
|
162
|
+
"gpt-5.6-terra": {
|
|
163
|
+
input: 1,
|
|
164
|
+
output: 6,
|
|
165
|
+
cacheRead: 0.1,
|
|
166
|
+
cacheWrite: 1.25,
|
|
167
|
+
tiers: [
|
|
168
|
+
{
|
|
169
|
+
inputTokensAbove: 272_000,
|
|
170
|
+
input: 2,
|
|
171
|
+
output: 9,
|
|
172
|
+
cacheRead: 0.2,
|
|
173
|
+
cacheWrite: 2.5,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
"gpt-5.6-luna": {
|
|
178
|
+
input: 0.1,
|
|
179
|
+
output: 0.6,
|
|
180
|
+
cacheRead: 0.01,
|
|
181
|
+
cacheWrite: 0.125,
|
|
182
|
+
tiers: [
|
|
183
|
+
{
|
|
184
|
+
inputTokensAbove: 272_000,
|
|
185
|
+
input: 0.2,
|
|
186
|
+
output: 0.9,
|
|
187
|
+
cacheRead: 0.02,
|
|
188
|
+
cacheWrite: 0.25,
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
"gpt-5.5": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
|
|
193
|
+
"gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
|
|
194
|
+
"gpt-5.3-codex": { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 0 },
|
|
195
|
+
"gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
|
|
196
|
+
|
|
197
|
+
// Google and xAI
|
|
198
|
+
"google/gemini-3.6-flash": { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 },
|
|
199
|
+
"google/gemini-3.5-flash": { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 },
|
|
200
|
+
"google/gemini-3.5-flash-lite": {
|
|
201
|
+
input: 0.3,
|
|
202
|
+
output: 2.5,
|
|
203
|
+
cacheRead: 0.03,
|
|
204
|
+
cacheWrite: 0,
|
|
205
|
+
},
|
|
206
|
+
"google/gemini-3.1-flash-lite": {
|
|
207
|
+
input: 0.25,
|
|
208
|
+
output: 1.5,
|
|
209
|
+
cacheRead: 0.03,
|
|
210
|
+
cacheWrite: 0,
|
|
211
|
+
},
|
|
212
|
+
"xai/grok-4.5": { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const TEMPORARY_PRICING: readonly TemporaryPricing[] = [
|
|
216
|
+
{
|
|
217
|
+
models: ["gpt-5.6-terra", "gpt-5.6-luna"],
|
|
218
|
+
expiresOn: "2026-08-14",
|
|
219
|
+
description: "50% promotional rates",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
models: ["claude-sonnet-5"],
|
|
223
|
+
expiresOn: "2026-08-31",
|
|
224
|
+
description: "introductory pricing",
|
|
225
|
+
},
|
|
226
|
+
]
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type { CommandCodeModel, LoadCommandCodeModelsResult } from "./models.ts"
|
|
2
|
+
|
|
3
|
+
export interface CommandCodeUi {
|
|
4
|
+
notify(message: string, type?: "info" | "warning" | "error"): void
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface CommandCodeCommandContext {
|
|
8
|
+
ui: CommandCodeUi
|
|
9
|
+
waitForIdle?: () => Promise<void>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CommandCodeRuntimeApi<
|
|
13
|
+
TProviderConfig,
|
|
14
|
+
TContext extends CommandCodeCommandContext,
|
|
15
|
+
> {
|
|
16
|
+
registerProvider(name: string, config: TProviderConfig): void
|
|
17
|
+
registerCommand(
|
|
18
|
+
name: string,
|
|
19
|
+
options: {
|
|
20
|
+
description: string
|
|
21
|
+
handler: (args: string, ctx: TContext) => Promise<void>
|
|
22
|
+
},
|
|
23
|
+
): void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CommandCodeRuntimeOptions<TProviderConfig> {
|
|
27
|
+
endpoint: string
|
|
28
|
+
cachePath: string
|
|
29
|
+
loadModels: () => Promise<LoadCommandCodeModelsResult>
|
|
30
|
+
createProviderConfig: (models: readonly CommandCodeModel[]) => TProviderConfig
|
|
31
|
+
now?: () => number
|
|
32
|
+
logWarning?: (message: string) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CommandCodeRuntimeStatus {
|
|
36
|
+
source: LoadCommandCodeModelsResult["source"]
|
|
37
|
+
modelCount: number
|
|
38
|
+
lastSuccess?: number
|
|
39
|
+
lastAttempt?: number
|
|
40
|
+
cachePath: string
|
|
41
|
+
endpoint: string
|
|
42
|
+
warning?: string
|
|
43
|
+
refreshing: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CommandCodeRefreshResult {
|
|
47
|
+
refreshed: boolean
|
|
48
|
+
source: CommandCodeRuntimeStatus["source"]
|
|
49
|
+
modelCount: number
|
|
50
|
+
warning?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const REDACTED = "[redacted]"
|
|
54
|
+
|
|
55
|
+
function errorMessage(error: unknown): string {
|
|
56
|
+
return error instanceof Error ? error.message : String(error)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function redactUrl(value: string): string {
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(value)
|
|
62
|
+
return `${url.protocol}//${url.host}${url.pathname}`
|
|
63
|
+
} catch {
|
|
64
|
+
return REDACTED
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function redactDiagnosticText(value: string): string {
|
|
69
|
+
const redactedUrls = value.replace(/https?:\/\/[^\s)]+/gi, (match) => redactUrl(match))
|
|
70
|
+
return redactedUrls
|
|
71
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${REDACTED}`)
|
|
72
|
+
.replace(/\b(?:user|cc)_[A-Za-z0-9_-]{8,}\b/gi, REDACTED)
|
|
73
|
+
.replace(/\b(?:api[-_ ]?key|token|secret|password)\s*[=:]\s*[^\s,;)]+/gi, (match) => {
|
|
74
|
+
const separator = match.match(/\s*[=:]\s*/)?.[0] ?? "="
|
|
75
|
+
return `${match.slice(0, match.indexOf(separator))}${separator}${REDACTED}`
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function redactEndpoint(value: string): string {
|
|
80
|
+
return redactUrl(value)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatTimestamp(timestamp: number | undefined): string {
|
|
84
|
+
return timestamp === undefined ? "never" : new Date(timestamp).toISOString()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatCommandCodeStatus(status: CommandCodeRuntimeStatus): string {
|
|
88
|
+
const lines = [
|
|
89
|
+
`source: ${status.source}`,
|
|
90
|
+
`model count: ${status.modelCount}`,
|
|
91
|
+
`last success: ${formatTimestamp(status.lastSuccess)}`,
|
|
92
|
+
`last attempt: ${formatTimestamp(status.lastAttempt)}`,
|
|
93
|
+
`cache path: ${status.cachePath}`,
|
|
94
|
+
`endpoint: ${redactEndpoint(status.endpoint)}`,
|
|
95
|
+
`refresh: ${status.refreshing ? "in progress" : "idle"}`,
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
lines.push(`warning: ${status.warning ? redactDiagnosticText(status.warning) : "none"}`)
|
|
99
|
+
return lines.join("\n")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCommandContext> {
|
|
103
|
+
private readonly now: () => number
|
|
104
|
+
private readonly logWarning: (message: string) => void
|
|
105
|
+
private status: CommandCodeRuntimeStatus
|
|
106
|
+
private providerRegistered = false
|
|
107
|
+
private refreshPromise: Promise<CommandCodeRefreshResult> | undefined
|
|
108
|
+
|
|
109
|
+
constructor(
|
|
110
|
+
private readonly pi: CommandCodeRuntimeApi<TProviderConfig, TContext>,
|
|
111
|
+
private readonly options: CommandCodeRuntimeOptions<TProviderConfig>,
|
|
112
|
+
) {
|
|
113
|
+
this.now = options.now ?? Date.now
|
|
114
|
+
this.logWarning = options.logWarning ?? ((message) => console.warn(`[commandcode] ${message}`))
|
|
115
|
+
const initialStatus: CommandCodeRuntimeStatus = {
|
|
116
|
+
source: "empty",
|
|
117
|
+
modelCount: 0,
|
|
118
|
+
cachePath: options.cachePath,
|
|
119
|
+
endpoint: options.endpoint,
|
|
120
|
+
refreshing: false,
|
|
121
|
+
}
|
|
122
|
+
this.status = { ...initialStatus }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getStatus(): CommandCodeRuntimeStatus {
|
|
126
|
+
return { ...this.status }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async initialize(): Promise<void> {
|
|
130
|
+
this.registerCommands()
|
|
131
|
+
await this.refresh()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
refresh(): Promise<CommandCodeRefreshResult> {
|
|
135
|
+
if (this.refreshPromise) return this.refreshPromise
|
|
136
|
+
|
|
137
|
+
const refreshPromise = this.refreshCatalog().finally(() => {
|
|
138
|
+
if (this.refreshPromise === refreshPromise) this.refreshPromise = undefined
|
|
139
|
+
})
|
|
140
|
+
this.refreshPromise = refreshPromise
|
|
141
|
+
return refreshPromise
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async refreshCatalog(): Promise<CommandCodeRefreshResult> {
|
|
145
|
+
this.status = {
|
|
146
|
+
...this.status,
|
|
147
|
+
lastAttempt: this.now(),
|
|
148
|
+
refreshing: true,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const loaded = await this.options.loadModels()
|
|
153
|
+
const warning = loaded.warning ? redactDiagnosticText(loaded.warning) : undefined
|
|
154
|
+
|
|
155
|
+
const shouldRegister =
|
|
156
|
+
!this.providerRegistered ||
|
|
157
|
+
loaded.source === "live" ||
|
|
158
|
+
(this.status.modelCount === 0 && loaded.models.length > 0)
|
|
159
|
+
|
|
160
|
+
if (shouldRegister) {
|
|
161
|
+
this.pi.registerProvider("commandcode", this.options.createProviderConfig(loaded.models))
|
|
162
|
+
this.providerRegistered = true
|
|
163
|
+
|
|
164
|
+
if (loaded.models.length === 0) {
|
|
165
|
+
const preservedWarning = warning ?? "Model catalog refresh returned no models"
|
|
166
|
+
this.status = {
|
|
167
|
+
...this.status,
|
|
168
|
+
source: loaded.source,
|
|
169
|
+
modelCount: 0,
|
|
170
|
+
warning: preservedWarning,
|
|
171
|
+
refreshing: false,
|
|
172
|
+
}
|
|
173
|
+
this.warn(preservedWarning)
|
|
174
|
+
return {
|
|
175
|
+
refreshed: false,
|
|
176
|
+
source: loaded.source,
|
|
177
|
+
modelCount: 0,
|
|
178
|
+
warning: preservedWarning,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.status = {
|
|
183
|
+
...this.status,
|
|
184
|
+
source: loaded.source,
|
|
185
|
+
modelCount: loaded.models.length,
|
|
186
|
+
lastSuccess: this.now(),
|
|
187
|
+
warning,
|
|
188
|
+
refreshing: false,
|
|
189
|
+
}
|
|
190
|
+
if (warning) this.warn(warning)
|
|
191
|
+
return {
|
|
192
|
+
refreshed: true,
|
|
193
|
+
source: loaded.source,
|
|
194
|
+
modelCount: loaded.models.length,
|
|
195
|
+
warning,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const preservedWarning = warning ?? "Model catalog refresh returned no models"
|
|
200
|
+
this.status = {
|
|
201
|
+
...this.status,
|
|
202
|
+
warning: preservedWarning,
|
|
203
|
+
refreshing: false,
|
|
204
|
+
}
|
|
205
|
+
this.warn(preservedWarning)
|
|
206
|
+
return {
|
|
207
|
+
refreshed: false,
|
|
208
|
+
source: this.status.source,
|
|
209
|
+
modelCount: this.status.modelCount,
|
|
210
|
+
warning: preservedWarning,
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
const warning = redactDiagnosticText(
|
|
214
|
+
`Could not refresh the Command Code model catalog: ${errorMessage(error)}`,
|
|
215
|
+
)
|
|
216
|
+
this.status = {
|
|
217
|
+
...this.status,
|
|
218
|
+
warning,
|
|
219
|
+
refreshing: false,
|
|
220
|
+
}
|
|
221
|
+
this.warn(warning)
|
|
222
|
+
return {
|
|
223
|
+
refreshed: false,
|
|
224
|
+
source: this.status.source,
|
|
225
|
+
modelCount: this.status.modelCount,
|
|
226
|
+
warning,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private warn(message: string): void {
|
|
232
|
+
try {
|
|
233
|
+
this.logWarning(redactDiagnosticText(message))
|
|
234
|
+
} catch {
|
|
235
|
+
// Diagnostics must never make a catalog refresh fail.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private registerCommands(): void {
|
|
240
|
+
this.pi.registerCommand("commandcode-refresh", {
|
|
241
|
+
description: "Refresh the Command Code model catalog",
|
|
242
|
+
handler: async (_args, ctx) => {
|
|
243
|
+
await ctx.waitForIdle?.()
|
|
244
|
+
const result = await this.refresh()
|
|
245
|
+
if (result.refreshed) {
|
|
246
|
+
ctx.ui.notify(
|
|
247
|
+
`Command Code model catalog refreshed (${result.modelCount} models from ${result.source}).`,
|
|
248
|
+
"info",
|
|
249
|
+
)
|
|
250
|
+
} else {
|
|
251
|
+
ctx.ui.notify(
|
|
252
|
+
`Command Code model catalog unchanged (${result.modelCount} models remain available).${result.warning ? ` ${result.warning}` : ""}`,
|
|
253
|
+
"warning",
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
this.pi.registerCommand("commandcode-status", {
|
|
260
|
+
description: "Show redacted Command Code provider diagnostics",
|
|
261
|
+
handler: async (_args, ctx) => {
|
|
262
|
+
ctx.ui.notify(
|
|
263
|
+
formatCommandCodeStatus(this.status),
|
|
264
|
+
this.status.warning ? "warning" : "info",
|
|
265
|
+
)
|
|
266
|
+
},
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function createCommandCodeRuntime<
|
|
272
|
+
TProviderConfig,
|
|
273
|
+
TContext extends CommandCodeCommandContext,
|
|
274
|
+
>(
|
|
275
|
+
pi: CommandCodeRuntimeApi<TProviderConfig, TContext>,
|
|
276
|
+
options: CommandCodeRuntimeOptions<TProviderConfig>,
|
|
277
|
+
): CommandCodeRuntime<TProviderConfig, TContext> {
|
|
278
|
+
return new CommandCodeRuntime(pi, options)
|
|
279
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface Usage {
|
|
|
15
15
|
output: number
|
|
16
16
|
cacheRead: number
|
|
17
17
|
cacheWrite: number
|
|
18
|
+
cacheWrite1h?: number
|
|
18
19
|
totalTokens: number
|
|
19
20
|
cost: UsageCost
|
|
20
21
|
}
|
|
@@ -50,19 +51,34 @@ export interface AssistantMessageLike {
|
|
|
50
51
|
timestamp: number
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
export interface
|
|
54
|
+
export interface ModelCostRates {
|
|
54
55
|
input: number
|
|
55
56
|
output: number
|
|
56
57
|
cacheRead: number
|
|
57
58
|
cacheWrite: number
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
export interface ModelCostTier extends ModelCostRates {
|
|
62
|
+
inputTokensAbove: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ModelCost extends ModelCostRates {
|
|
66
|
+
tiers?: readonly ModelCostTier[]
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
export interface ModelLike {
|
|
61
70
|
id: string
|
|
62
71
|
api: unknown
|
|
63
72
|
provider: string
|
|
64
73
|
maxTokens: number
|
|
65
74
|
cost: ModelCost
|
|
75
|
+
reasoning?: boolean
|
|
76
|
+
thinkingLevelMap?: Partial<Record<string, string | null>>
|
|
77
|
+
thinking?: {
|
|
78
|
+
mode?: "effort"
|
|
79
|
+
effortMap?: Partial<Record<string, string>>
|
|
80
|
+
efforts?: readonly string[]
|
|
81
|
+
}
|
|
66
82
|
}
|
|
67
83
|
|
|
68
84
|
export interface MessageLike {
|
|
@@ -95,6 +111,8 @@ export interface StreamOptions {
|
|
|
95
111
|
signal?: AbortSignal
|
|
96
112
|
headers?: Record<string, string>
|
|
97
113
|
maxTokens?: number
|
|
114
|
+
/** Resolved pi thinking level; forwarded only through the model's map. */
|
|
115
|
+
reasoning?: string
|
|
98
116
|
onPayload?: (payload: unknown, model: ModelLike) => unknown | Promise<unknown>
|
|
99
117
|
onResponse?: (response: ProviderResponseInfo, model: ModelLike) => void | Promise<void>
|
|
100
118
|
/**
|