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.
@@ -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 ModelCost {
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
  /**