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/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
  /**