feat(models): add model-aware runtime metadata
This commit is contained in:
@@ -13,50 +13,42 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
|
import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
|
||||||
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
import {
|
||||||
|
getAgentDir,
|
||||||
|
type ExtensionAPI,
|
||||||
|
type ExtensionCommandContext,
|
||||||
|
type ProviderConfig,
|
||||||
|
} from "@earendil-works/pi-coding-agent"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
|
|
||||||
import { getApiKey as getStoredApiKey } from "./src/converters.ts"
|
import { getApiKey as getStoredApiKey } from "./src/converters.ts"
|
||||||
import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
|
import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
|
||||||
import { calculateCommandCodeCost } from "./src/cost.ts"
|
import { calculateCommandCodeCost } from "./src/cost.ts"
|
||||||
import { DEFAULT_MODELS_URL, loadCommandCodeModels } from "./src/models.ts"
|
import {
|
||||||
|
DEFAULT_MODELS_URL,
|
||||||
|
getModelsTimeoutMs,
|
||||||
|
loadCommandCodeModels,
|
||||||
|
thinkingMetadataForModel,
|
||||||
|
type CommandCodeModel,
|
||||||
|
} from "./src/models.ts"
|
||||||
import { getApiKey as getOAuthApiKey, login, refreshToken } from "./src/oauth.ts"
|
import { getApiKey as getOAuthApiKey, login, refreshToken } from "./src/oauth.ts"
|
||||||
import { MODEL_COSTS, ZERO_MODEL_COST } from "./src/pricing.ts"
|
import { MODEL_COSTS, ZERO_MODEL_COST } from "./src/pricing.ts"
|
||||||
|
import { createCommandCodeRuntime } from "./src/runtime.ts"
|
||||||
|
|
||||||
const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE
|
function createProviderConfig(
|
||||||
const MODELS_URL = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
|
models: readonly CommandCodeModel[],
|
||||||
const MODELS_CACHE_PATH =
|
apiBase: string,
|
||||||
process.env.COMMANDCODE_MODELS_CACHE ?? join(getAgentDir(), "commandcode-models.json")
|
streamCommandCode: ProviderConfig["streamSimple"],
|
||||||
|
): ProviderConfig {
|
||||||
const streamCommandCode = createStreamCommandCode({
|
return {
|
||||||
createStream: () => new AssistantMessageEventStream(),
|
|
||||||
calculateCost: calculateCommandCodeCost,
|
|
||||||
apiBase: API_BASE,
|
|
||||||
})
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Extension entry point
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export default async function (pi: ExtensionAPI) {
|
|
||||||
const storedApiKey = getStoredApiKey()
|
|
||||||
const { models, warning } = await loadCommandCodeModels({
|
|
||||||
url: MODELS_URL,
|
|
||||||
cachePath: MODELS_CACHE_PATH,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (warning) console.warn(`[commandcode] ${warning}`)
|
|
||||||
|
|
||||||
pi.registerProvider("commandcode", {
|
|
||||||
name: "Command Code",
|
name: "Command Code",
|
||||||
baseUrl: API_BASE,
|
baseUrl: apiBase,
|
||||||
apiKey: storedApiKey,
|
apiKey: "$COMMANDCODE_API_KEY",
|
||||||
authHeader: true,
|
authHeader: true,
|
||||||
api: "commandcode-custom",
|
api: "commandcode-custom",
|
||||||
streamSimple: streamCommandCode,
|
streamSimple: streamCommandCode,
|
||||||
headers: {
|
headers: {
|
||||||
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
"x-commandcode-version": COMMAND_CODE_CLI_VERSION,
|
||||||
"x-cli-environment": "production",
|
|
||||||
},
|
},
|
||||||
oauth: {
|
oauth: {
|
||||||
name: "Command Code",
|
name: "Command Code",
|
||||||
@@ -68,10 +60,38 @@ export default async function (pi: ExtensionAPI) {
|
|||||||
id: model.id,
|
id: model.id,
|
||||||
name: model.name,
|
name: model.name,
|
||||||
reasoning: model.reasoning,
|
reasoning: model.reasoning,
|
||||||
|
...(thinkingMetadataForModel(model.id) ?? {}),
|
||||||
input: ["text"] as const,
|
input: ["text"] as const,
|
||||||
cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST,
|
cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST,
|
||||||
contextWindow: model.contextWindow,
|
contextWindow: model.contextWindow,
|
||||||
maxTokens: model.maxTokens,
|
maxTokens: model.maxTokens,
|
||||||
})),
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function (pi: ExtensionAPI) {
|
||||||
|
const apiBase = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE
|
||||||
|
const modelsUrl = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
|
||||||
|
const modelsTimeoutMs = getModelsTimeoutMs()
|
||||||
|
const modelsCachePath =
|
||||||
|
process.env.COMMANDCODE_MODELS_CACHE ?? join(getAgentDir(), "commandcode-models.json")
|
||||||
|
const streamCommandCode = createStreamCommandCode({
|
||||||
|
createStream: () => new AssistantMessageEventStream(),
|
||||||
|
calculateCost: calculateCommandCodeCost,
|
||||||
|
apiBase,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const runtime = createCommandCodeRuntime<ProviderConfig, ExtensionCommandContext>(pi, {
|
||||||
|
endpoint: modelsUrl,
|
||||||
|
cachePath: modelsCachePath,
|
||||||
|
loadModels: () =>
|
||||||
|
loadCommandCodeModels({
|
||||||
|
url: modelsUrl,
|
||||||
|
cachePath: modelsCachePath,
|
||||||
|
timeoutMs: modelsTimeoutMs,
|
||||||
|
}),
|
||||||
|
createProviderConfig: (models) => createProviderConfig(models, apiBase, streamCommandCode),
|
||||||
|
})
|
||||||
|
|
||||||
|
await runtime.initialize()
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -134,6 +134,15 @@ function generateMaxTokens(model: ModelLike, options?: StreamOptions): number {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mappedReasoningEffort(model: ModelLike, options?: StreamOptions): string | undefined {
|
||||||
|
const level = options?.reasoning
|
||||||
|
if (!level || level === "off" || !model.reasoning) return undefined
|
||||||
|
|
||||||
|
const effortMap = model.thinking?.effortMap ?? model.thinkingLevelMap
|
||||||
|
const mapped = effortMap?.[level]
|
||||||
|
return typeof mapped === "string" && mapped !== "off" ? mapped : undefined
|
||||||
|
}
|
||||||
|
|
||||||
export function projectSlugFromPath(pathName: string): string {
|
export function projectSlugFromPath(pathName: string): string {
|
||||||
const slug = pathName
|
const slug = pathName
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -424,6 +433,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
|
|
||||||
const workingDir = cwd()
|
const workingDir = cwd()
|
||||||
const threadId = uuid()
|
const threadId = uuid()
|
||||||
|
const reasoningEffort = mappedReasoningEffort(model, options)
|
||||||
|
|
||||||
let body: unknown = {
|
let body: unknown = {
|
||||||
config: {
|
config: {
|
||||||
@@ -448,6 +458,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
max_tokens: generateMaxTokens(model, options),
|
max_tokens: generateMaxTokens(model, options),
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
stream: true,
|
stream: true,
|
||||||
|
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
|
||||||
},
|
},
|
||||||
threadId,
|
threadId,
|
||||||
}
|
}
|
||||||
|
|||||||
+187
-5
@@ -2,10 +2,100 @@ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
|
|||||||
import { dirname } from "node:path"
|
import { dirname } from "node:path"
|
||||||
|
|
||||||
export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models"
|
export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models"
|
||||||
|
export const DEFAULT_MODELS_TIMEOUT_MS = 10_000
|
||||||
|
|
||||||
const DEFAULT_MAX_OUTPUT_TOKENS = 65_536
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65_536
|
||||||
const MODEL_CACHE_VERSION = 1
|
const MODEL_CACHE_VERSION = 1
|
||||||
|
|
||||||
|
export type PiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
|
||||||
|
|
||||||
|
type CommandCodeReasoningEffort = Exclude<PiThinkingLevel, "off">
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-model reasoning efforts supported by Command Code's generate endpoint.
|
||||||
|
*
|
||||||
|
* The Provider API does not expose reasoning metadata. These entries are
|
||||||
|
* maintained from the official Command Code CLI model catalog, so a model is
|
||||||
|
* marked reasoning-capable only when its upstream effort support is known.
|
||||||
|
*/
|
||||||
|
export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasoningEffort[]>> = {
|
||||||
|
"claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
"claude-haiku-4-5-20251001": ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
"claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
"claude-opus-4-8": ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
"claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
"claude-sonnet-4-6": ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
"deepseek/deepseek-v4-flash": ["high", "max"],
|
||||||
|
"deepseek/deepseek-v4-pro": ["high", "max"],
|
||||||
|
"gpt-5.3-codex": ["low", "medium", "high", "xhigh"],
|
||||||
|
"gpt-5.4": ["low", "medium", "high", "xhigh"],
|
||||||
|
"gpt-5.4-mini": ["low", "medium", "high"],
|
||||||
|
"gpt-5.5": ["low", "medium", "high", "xhigh"],
|
||||||
|
"gpt-5.6-luna": ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
"gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
"gpt-5.6-terra": ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
"google/gemini-3.1-flash-lite": ["low", "medium", "high"],
|
||||||
|
"google/gemini-3.5-flash": ["low", "medium", "high"],
|
||||||
|
"google/gemini-3.5-flash-lite": ["low", "medium", "high"],
|
||||||
|
"google/gemini-3.6-flash": ["low", "medium", "high"],
|
||||||
|
"meta/muse-spark-1.1": ["low", "medium", "high"],
|
||||||
|
"moonshotai/Kimi-K2.5": ["high", "max"],
|
||||||
|
"moonshotai/Kimi-K2.6": ["high", "max"],
|
||||||
|
"sakana/fugu-ultra": ["high", "xhigh"],
|
||||||
|
"tencent/hy3-paid": ["low", "medium", "high"],
|
||||||
|
"xai/grok-4.5": ["low", "medium", "high"],
|
||||||
|
"zai-org/GLM-5.2": ["high", "max"],
|
||||||
|
}
|
||||||
|
|
||||||
|
const PI_THINKING_LEVELS: readonly PiThinkingLevel[] = [
|
||||||
|
"off",
|
||||||
|
"minimal",
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high",
|
||||||
|
"xhigh",
|
||||||
|
"max",
|
||||||
|
]
|
||||||
|
|
||||||
|
export function thinkingLevelMapForEfforts(
|
||||||
|
efforts: readonly string[],
|
||||||
|
): Partial<Record<PiThinkingLevel, string | null>> {
|
||||||
|
const map: Partial<Record<PiThinkingLevel, string | null>> = {}
|
||||||
|
for (const level of PI_THINKING_LEVELS) {
|
||||||
|
if (level === "off") continue
|
||||||
|
map[level] = efforts.includes(level) ? level : null
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThinkingMetadata {
|
||||||
|
thinkingLevelMap: Partial<Record<PiThinkingLevel, string | null>>
|
||||||
|
thinking: {
|
||||||
|
effortMap: Partial<Record<PiThinkingLevel, string | null>>
|
||||||
|
efforts: readonly CommandCodeReasoningEffort[]
|
||||||
|
defaultLevel: CommandCodeReasoningEffort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function thinkingMetadataForModel(modelId: string): ThinkingMetadata | undefined {
|
||||||
|
const efforts = MODEL_EFFORTS[modelId]
|
||||||
|
if (!efforts) return undefined
|
||||||
|
const effortMap = thinkingLevelMapForEfforts(efforts)
|
||||||
|
return {
|
||||||
|
thinkingLevelMap: effortMap,
|
||||||
|
thinking: {
|
||||||
|
effortMap,
|
||||||
|
efforts,
|
||||||
|
defaultLevel: efforts[efforts.length - 2] ?? efforts[0],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isReasoningModel(modelId: string): boolean {
|
||||||
|
return MODEL_EFFORTS[modelId] !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
interface ApiModel {
|
interface ApiModel {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -23,6 +113,8 @@ export interface CommandCodeModel {
|
|||||||
interface FetchCommandCodeModelsOptions {
|
interface FetchCommandCodeModelsOptions {
|
||||||
url?: string
|
url?: string
|
||||||
fetchImpl?: typeof fetch
|
fetchImpl?: typeof fetch
|
||||||
|
signal?: AbortSignal
|
||||||
|
timeoutMs?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadCommandCodeModelsOptions extends FetchCommandCodeModelsOptions {
|
interface LoadCommandCodeModelsOptions extends FetchCommandCodeModelsOptions {
|
||||||
@@ -74,10 +166,12 @@ function parseApiModel(value: unknown): ApiModel {
|
|||||||
function parseCachedModel(value: unknown): CommandCodeModel {
|
function parseCachedModel(value: unknown): CommandCodeModel {
|
||||||
if (!isRecord(value)) throw new Error("Expected cached model entry to be an object")
|
if (!isRecord(value)) throw new Error("Expected cached model entry to be an object")
|
||||||
|
|
||||||
|
const id = stringField(value, "id")
|
||||||
|
booleanField(value, "reasoning")
|
||||||
return {
|
return {
|
||||||
id: stringField(value, "id"),
|
id,
|
||||||
name: stringField(value, "name"),
|
name: stringField(value, "name"),
|
||||||
reasoning: booleanField(value, "reasoning"),
|
reasoning: isReasoningModel(id),
|
||||||
contextWindow: positiveNumberField(value, "contextWindow"),
|
contextWindow: positiveNumberField(value, "contextWindow"),
|
||||||
maxTokens: positiveNumberField(value, "maxTokens"),
|
maxTokens: positiveNumberField(value, "maxTokens"),
|
||||||
}
|
}
|
||||||
@@ -92,6 +186,85 @@ function errorMessage(error: unknown): string {
|
|||||||
return error instanceof Error ? error.message : String(error)
|
return error instanceof Error ? error.message : String(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function abortError(reason: unknown): Error {
|
||||||
|
if (reason instanceof Error) return reason
|
||||||
|
return new DOMException("The operation was aborted", "AbortError")
|
||||||
|
}
|
||||||
|
|
||||||
|
function configuredTimeoutMs(timeoutMs: number | undefined): number {
|
||||||
|
return timeoutMs !== undefined && Number.isFinite(timeoutMs) && timeoutMs > 0
|
||||||
|
? timeoutMs
|
||||||
|
: DEFAULT_MODELS_TIMEOUT_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getModelsTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
|
||||||
|
const raw = env.COMMANDCODE_MODELS_TIMEOUT_MS
|
||||||
|
if (!raw) return DEFAULT_MODELS_TIMEOUT_MS
|
||||||
|
|
||||||
|
const parsed = Number(raw)
|
||||||
|
return configuredTimeoutMs(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModelDiscoveryTimeoutError extends Error {
|
||||||
|
constructor(timeoutMs: number) {
|
||||||
|
super(`Command Code model discovery timed out after ${timeoutMs}ms`)
|
||||||
|
this.name = "ModelDiscoveryTimeoutError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runWithTimeout<T>(
|
||||||
|
operation: (signal: AbortSignal) => Promise<T>,
|
||||||
|
timeoutMs: number,
|
||||||
|
externalSignal: AbortSignal | undefined,
|
||||||
|
): Promise<T> {
|
||||||
|
const controller = new AbortController()
|
||||||
|
let timer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
let settled = false
|
||||||
|
let onExternalAbort: (() => void) | undefined
|
||||||
|
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
const cleanup = () => {
|
||||||
|
if (timer !== undefined) clearTimeout(timer)
|
||||||
|
if (onExternalAbort && externalSignal) {
|
||||||
|
externalSignal.removeEventListener("abort", onExternalAbort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveOnce = (value: T) => {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
cleanup()
|
||||||
|
resolve(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rejectOnce = (error: unknown) => {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
cleanup()
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const abort = (reason: unknown) => {
|
||||||
|
const error = abortError(reason)
|
||||||
|
controller.abort(error)
|
||||||
|
rejectOnce(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (externalSignal?.aborted) {
|
||||||
|
abort(externalSignal.reason)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onExternalAbort = () => abort(externalSignal?.reason)
|
||||||
|
externalSignal?.addEventListener("abort", onExternalAbort, { once: true })
|
||||||
|
timer = setTimeout(() => abort(new ModelDiscoveryTimeoutError(timeoutMs)), timeoutMs)
|
||||||
|
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => operation(controller.signal))
|
||||||
|
.then(resolveOnce, rejectOnce)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function commandCodeModelsFromApiResponse(value: unknown): readonly CommandCodeModel[] {
|
export function commandCodeModelsFromApiResponse(value: unknown): readonly CommandCodeModel[] {
|
||||||
if (!isRecord(value)) throw new Error("Expected models response to be an object")
|
if (!isRecord(value)) throw new Error("Expected models response to be an object")
|
||||||
if (value.object !== "list") throw new Error("Expected models response object to be 'list'")
|
if (value.object !== "list") throw new Error("Expected models response object to be 'list'")
|
||||||
@@ -102,7 +275,7 @@ export function commandCodeModelsFromApiResponse(value: unknown): readonly Comma
|
|||||||
return data.map(parseApiModel).map((model) => ({
|
return data.map(parseApiModel).map((model) => ({
|
||||||
id: model.id,
|
id: model.id,
|
||||||
name: `${model.name} (CC)`,
|
name: `${model.name} (CC)`,
|
||||||
reasoning: true,
|
reasoning: isReasoningModel(model.id),
|
||||||
contextWindow: model.contextLength,
|
contextWindow: model.contextLength,
|
||||||
maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
|
maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
|
||||||
}))
|
}))
|
||||||
@@ -123,10 +296,13 @@ export async function fetchCommandCodeModels(
|
|||||||
): Promise<readonly CommandCodeModel[]> {
|
): Promise<readonly CommandCodeModel[]> {
|
||||||
const url = options.url ?? DEFAULT_MODELS_URL
|
const url = options.url ?? DEFAULT_MODELS_URL
|
||||||
const fetchImpl = options.fetchImpl ?? fetch
|
const fetchImpl = options.fetchImpl ?? fetch
|
||||||
|
const body: unknown = await runWithTimeout(
|
||||||
|
async (signal) => {
|
||||||
const response = await fetchImpl(url, {
|
const response = await fetchImpl(url, {
|
||||||
headers: {
|
headers: {
|
||||||
accept: "application/json",
|
accept: "application/json",
|
||||||
},
|
},
|
||||||
|
signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -135,7 +311,11 @@ export async function fetchCommandCodeModels(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const body: unknown = await response.json()
|
return await response.json()
|
||||||
|
},
|
||||||
|
configuredTimeoutMs(options.timeoutMs),
|
||||||
|
options.signal,
|
||||||
|
)
|
||||||
return requireModels(commandCodeModelsFromApiResponse(body))
|
return requireModels(commandCodeModelsFromApiResponse(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +367,8 @@ export async function loadCommandCodeModels(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (liveError) {
|
} catch (liveError) {
|
||||||
|
if (options.signal?.aborted) throw abortError(options.signal.reason ?? liveError)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const models = await readCommandCodeModelsCache(cachePath)
|
const models = await readCommandCodeModelsCache(cachePath)
|
||||||
return {
|
return {
|
||||||
@@ -198,7 +380,7 @@ export async function loadCommandCodeModels(
|
|||||||
return {
|
return {
|
||||||
models: [],
|
models: [],
|
||||||
source: "empty",
|
source: "empty",
|
||||||
warning: `Could not refresh the Command Code model catalog (${errorMessage(liveError)}), and no valid cached catalog is available at ${cachePath} (${errorMessage(cacheError)}). Command Code models will remain unavailable until /reload succeeds.`,
|
warning: `Could not refresh the Command Code model catalog (${errorMessage(liveError)}), and no valid cached catalog is available at ${cachePath} (${errorMessage(cacheError)}). Command Code models will remain unavailable until /commandcode-refresh succeeds.`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+279
@@ -0,0 +1,279 @@
|
|||||||
|
import type { CommandCodeModel, LoadCommandCodeModelsResult } from "./models.ts"
|
||||||
|
|
||||||
|
export interface CommandCodeUi {
|
||||||
|
notify(message: string, type?: "info" | "warning" | "error"): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandCodeCommandContext {
|
||||||
|
ui: CommandCodeUi
|
||||||
|
waitForIdle?: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandCodeRuntimeApi<
|
||||||
|
TProviderConfig,
|
||||||
|
TContext extends CommandCodeCommandContext,
|
||||||
|
> {
|
||||||
|
registerProvider(name: string, config: TProviderConfig): void
|
||||||
|
registerCommand(
|
||||||
|
name: string,
|
||||||
|
options: {
|
||||||
|
description: string
|
||||||
|
handler: (args: string, ctx: TContext) => Promise<void>
|
||||||
|
},
|
||||||
|
): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandCodeRuntimeOptions<TProviderConfig> {
|
||||||
|
endpoint: string
|
||||||
|
cachePath: string
|
||||||
|
loadModels: () => Promise<LoadCommandCodeModelsResult>
|
||||||
|
createProviderConfig: (models: readonly CommandCodeModel[]) => TProviderConfig
|
||||||
|
now?: () => number
|
||||||
|
logWarning?: (message: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandCodeRuntimeStatus {
|
||||||
|
source: LoadCommandCodeModelsResult["source"]
|
||||||
|
modelCount: number
|
||||||
|
lastSuccess?: number
|
||||||
|
lastAttempt?: number
|
||||||
|
cachePath: string
|
||||||
|
endpoint: string
|
||||||
|
warning?: string
|
||||||
|
refreshing: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandCodeRefreshResult {
|
||||||
|
refreshed: boolean
|
||||||
|
source: CommandCodeRuntimeStatus["source"]
|
||||||
|
modelCount: number
|
||||||
|
warning?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const REDACTED = "[redacted]"
|
||||||
|
|
||||||
|
function errorMessage(error: unknown): string {
|
||||||
|
return error instanceof Error ? error.message : String(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
function redactUrl(value: string): string {
|
||||||
|
try {
|
||||||
|
const url = new URL(value)
|
||||||
|
return `${url.protocol}//${url.host}${url.pathname}`
|
||||||
|
} catch {
|
||||||
|
return REDACTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redactDiagnosticText(value: string): string {
|
||||||
|
const redactedUrls = value.replace(/https?:\/\/[^\s)]+/gi, (match) => redactUrl(match))
|
||||||
|
return redactedUrls
|
||||||
|
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${REDACTED}`)
|
||||||
|
.replace(/\b(?:user|cc)_[A-Za-z0-9_-]{8,}\b/gi, REDACTED)
|
||||||
|
.replace(/\b(?:api[-_ ]?key|token|secret|password)\s*[=:]\s*[^\s,;)]+/gi, (match) => {
|
||||||
|
const separator = match.match(/\s*[=:]\s*/)?.[0] ?? "="
|
||||||
|
return `${match.slice(0, match.indexOf(separator))}${separator}${REDACTED}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redactEndpoint(value: string): string {
|
||||||
|
return redactUrl(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(timestamp: number | undefined): string {
|
||||||
|
return timestamp === undefined ? "never" : new Date(timestamp).toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCommandCodeStatus(status: CommandCodeRuntimeStatus): string {
|
||||||
|
const lines = [
|
||||||
|
`source: ${status.source}`,
|
||||||
|
`model count: ${status.modelCount}`,
|
||||||
|
`last success: ${formatTimestamp(status.lastSuccess)}`,
|
||||||
|
`last attempt: ${formatTimestamp(status.lastAttempt)}`,
|
||||||
|
`cache path: ${status.cachePath}`,
|
||||||
|
`endpoint: ${redactEndpoint(status.endpoint)}`,
|
||||||
|
`refresh: ${status.refreshing ? "in progress" : "idle"}`,
|
||||||
|
]
|
||||||
|
|
||||||
|
lines.push(`warning: ${status.warning ? redactDiagnosticText(status.warning) : "none"}`)
|
||||||
|
return lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCommandContext> {
|
||||||
|
private readonly now: () => number
|
||||||
|
private readonly logWarning: (message: string) => void
|
||||||
|
private status: CommandCodeRuntimeStatus
|
||||||
|
private providerRegistered = false
|
||||||
|
private refreshPromise: Promise<CommandCodeRefreshResult> | undefined
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly pi: CommandCodeRuntimeApi<TProviderConfig, TContext>,
|
||||||
|
private readonly options: CommandCodeRuntimeOptions<TProviderConfig>,
|
||||||
|
) {
|
||||||
|
this.now = options.now ?? Date.now
|
||||||
|
this.logWarning = options.logWarning ?? ((message) => console.warn(`[commandcode] ${message}`))
|
||||||
|
const initialStatus: CommandCodeRuntimeStatus = {
|
||||||
|
source: "empty",
|
||||||
|
modelCount: 0,
|
||||||
|
cachePath: options.cachePath,
|
||||||
|
endpoint: options.endpoint,
|
||||||
|
refreshing: false,
|
||||||
|
}
|
||||||
|
this.status = { ...initialStatus }
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus(): CommandCodeRuntimeStatus {
|
||||||
|
return { ...this.status }
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
this.registerCommands()
|
||||||
|
await this.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh(): Promise<CommandCodeRefreshResult> {
|
||||||
|
if (this.refreshPromise) return this.refreshPromise
|
||||||
|
|
||||||
|
const refreshPromise = this.refreshCatalog().finally(() => {
|
||||||
|
if (this.refreshPromise === refreshPromise) this.refreshPromise = undefined
|
||||||
|
})
|
||||||
|
this.refreshPromise = refreshPromise
|
||||||
|
return refreshPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshCatalog(): Promise<CommandCodeRefreshResult> {
|
||||||
|
this.status = {
|
||||||
|
...this.status,
|
||||||
|
lastAttempt: this.now(),
|
||||||
|
refreshing: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const loaded = await this.options.loadModels()
|
||||||
|
const warning = loaded.warning ? redactDiagnosticText(loaded.warning) : undefined
|
||||||
|
|
||||||
|
const shouldRegister =
|
||||||
|
!this.providerRegistered ||
|
||||||
|
loaded.source === "live" ||
|
||||||
|
(this.status.modelCount === 0 && loaded.models.length > 0)
|
||||||
|
|
||||||
|
if (shouldRegister) {
|
||||||
|
this.pi.registerProvider("commandcode", this.options.createProviderConfig(loaded.models))
|
||||||
|
this.providerRegistered = true
|
||||||
|
|
||||||
|
if (loaded.models.length === 0) {
|
||||||
|
const preservedWarning = warning ?? "Model catalog refresh returned no models"
|
||||||
|
this.status = {
|
||||||
|
...this.status,
|
||||||
|
source: loaded.source,
|
||||||
|
modelCount: 0,
|
||||||
|
warning: preservedWarning,
|
||||||
|
refreshing: false,
|
||||||
|
}
|
||||||
|
this.warn(preservedWarning)
|
||||||
|
return {
|
||||||
|
refreshed: false,
|
||||||
|
source: loaded.source,
|
||||||
|
modelCount: 0,
|
||||||
|
warning: preservedWarning,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.status = {
|
||||||
|
...this.status,
|
||||||
|
source: loaded.source,
|
||||||
|
modelCount: loaded.models.length,
|
||||||
|
lastSuccess: this.now(),
|
||||||
|
warning,
|
||||||
|
refreshing: false,
|
||||||
|
}
|
||||||
|
if (warning) this.warn(warning)
|
||||||
|
return {
|
||||||
|
refreshed: true,
|
||||||
|
source: loaded.source,
|
||||||
|
modelCount: loaded.models.length,
|
||||||
|
warning,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const preservedWarning = warning ?? "Model catalog refresh returned no models"
|
||||||
|
this.status = {
|
||||||
|
...this.status,
|
||||||
|
warning: preservedWarning,
|
||||||
|
refreshing: false,
|
||||||
|
}
|
||||||
|
this.warn(preservedWarning)
|
||||||
|
return {
|
||||||
|
refreshed: false,
|
||||||
|
source: this.status.source,
|
||||||
|
modelCount: this.status.modelCount,
|
||||||
|
warning: preservedWarning,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const warning = redactDiagnosticText(
|
||||||
|
`Could not refresh the Command Code model catalog: ${errorMessage(error)}`,
|
||||||
|
)
|
||||||
|
this.status = {
|
||||||
|
...this.status,
|
||||||
|
warning,
|
||||||
|
refreshing: false,
|
||||||
|
}
|
||||||
|
this.warn(warning)
|
||||||
|
return {
|
||||||
|
refreshed: false,
|
||||||
|
source: this.status.source,
|
||||||
|
modelCount: this.status.modelCount,
|
||||||
|
warning,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private warn(message: string): void {
|
||||||
|
try {
|
||||||
|
this.logWarning(redactDiagnosticText(message))
|
||||||
|
} catch {
|
||||||
|
// Diagnostics must never make a catalog refresh fail.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerCommands(): void {
|
||||||
|
this.pi.registerCommand("commandcode-refresh", {
|
||||||
|
description: "Refresh the Command Code model catalog",
|
||||||
|
handler: async (_args, ctx) => {
|
||||||
|
await ctx.waitForIdle?.()
|
||||||
|
const result = await this.refresh()
|
||||||
|
if (result.refreshed) {
|
||||||
|
ctx.ui.notify(
|
||||||
|
`Command Code model catalog refreshed (${result.modelCount} models from ${result.source}).`,
|
||||||
|
"info",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ctx.ui.notify(
|
||||||
|
`Command Code model catalog unchanged (${result.modelCount} models remain available).${result.warning ? ` ${result.warning}` : ""}`,
|
||||||
|
"warning",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
this.pi.registerCommand("commandcode-status", {
|
||||||
|
description: "Show redacted Command Code provider diagnostics",
|
||||||
|
handler: async (_args, ctx) => {
|
||||||
|
ctx.ui.notify(
|
||||||
|
formatCommandCodeStatus(this.status),
|
||||||
|
this.status.warning ? "warning" : "info",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCommandCodeRuntime<
|
||||||
|
TProviderConfig,
|
||||||
|
TContext extends CommandCodeCommandContext,
|
||||||
|
>(
|
||||||
|
pi: CommandCodeRuntimeApi<TProviderConfig, TContext>,
|
||||||
|
options: CommandCodeRuntimeOptions<TProviderConfig>,
|
||||||
|
): CommandCodeRuntime<TProviderConfig, TContext> {
|
||||||
|
return new CommandCodeRuntime(pi, options)
|
||||||
|
}
|
||||||
@@ -72,6 +72,13 @@ export interface ModelLike {
|
|||||||
provider: string
|
provider: string
|
||||||
maxTokens: number
|
maxTokens: number
|
||||||
cost: ModelCost
|
cost: ModelCost
|
||||||
|
reasoning?: boolean
|
||||||
|
thinkingLevelMap?: Partial<Record<string, string | null>>
|
||||||
|
thinking?: {
|
||||||
|
effortMap?: Partial<Record<string, string | null>>
|
||||||
|
efforts?: readonly string[]
|
||||||
|
defaultLevel?: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageLike {
|
export interface MessageLike {
|
||||||
@@ -104,6 +111,8 @@ export interface StreamOptions {
|
|||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
maxTokens?: number
|
maxTokens?: number
|
||||||
|
/** Resolved pi thinking level; forwarded only through the model's map. */
|
||||||
|
reasoning?: string
|
||||||
onPayload?: (payload: unknown, model: ModelLike) => unknown | Promise<unknown>
|
onPayload?: (payload: unknown, model: ModelLike) => unknown | Promise<unknown>
|
||||||
onResponse?: (response: ProviderResponseInfo, model: ModelLike) => void | Promise<void>
|
onResponse?: (response: ProviderResponseInfo, model: ModelLike) => void | Promise<void>
|
||||||
/**
|
/**
|
||||||
|
|||||||
+121
-2
@@ -7,7 +7,12 @@ import { describe, it } from "node:test"
|
|||||||
import {
|
import {
|
||||||
commandCodeModelsFromApiResponse,
|
commandCodeModelsFromApiResponse,
|
||||||
commandCodeModelsFromCache,
|
commandCodeModelsFromCache,
|
||||||
|
DEFAULT_MODELS_TIMEOUT_MS,
|
||||||
|
getModelsTimeoutMs,
|
||||||
loadCommandCodeModels,
|
loadCommandCodeModels,
|
||||||
|
MODEL_EFFORTS,
|
||||||
|
thinkingLevelMapForEfforts,
|
||||||
|
thinkingMetadataForModel,
|
||||||
type CommandCodeModel,
|
type CommandCodeModel,
|
||||||
} from "../src/models.ts"
|
} from "../src/models.ts"
|
||||||
|
|
||||||
@@ -29,7 +34,7 @@ const EXPECTED_MODELS: readonly CommandCodeModel[] = [
|
|||||||
{
|
{
|
||||||
id: "Qwen/Qwen3.7-Max",
|
id: "Qwen/Qwen3.7-Max",
|
||||||
name: "Qwen 3.7 Max (CC)",
|
name: "Qwen 3.7 Max (CC)",
|
||||||
reasoning: true,
|
reasoning: false,
|
||||||
contextWindow: 1_000_000,
|
contextWindow: 1_000_000,
|
||||||
maxTokens: 65_536,
|
maxTokens: 65_536,
|
||||||
},
|
},
|
||||||
@@ -49,6 +54,17 @@ function failingFetch(message = "offline"): typeof fetch {
|
|||||||
return () => Promise.reject(new TypeError(message))
|
return () => Promise.reject(new TypeError(message))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hangingFetch(): typeof fetch {
|
||||||
|
return (_input, init) =>
|
||||||
|
new Promise<Response>((_resolve, reject) => {
|
||||||
|
init?.signal?.addEventListener(
|
||||||
|
"abort",
|
||||||
|
() => reject(init.signal?.reason ?? new DOMException("Aborted", "AbortError")),
|
||||||
|
{ once: true },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function withTemporaryCache(
|
async function withTemporaryCache(
|
||||||
run: (paths: { directory: string; cachePath: string }) => Promise<void>,
|
run: (paths: { directory: string; cachePath: string }) => Promise<void>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -65,6 +81,44 @@ describe("commandCodeModelsFromApiResponse()", () => {
|
|||||||
assert.deepEqual(commandCodeModelsFromApiResponse(API_RESPONSE), EXPECTED_MODELS)
|
assert.deepEqual(commandCodeModelsFromApiResponse(API_RESPONSE), EXPECTED_MODELS)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("marks only known reasoning models as reasoning-capable", () => {
|
||||||
|
const models = commandCodeModelsFromApiResponse({
|
||||||
|
object: "list",
|
||||||
|
data: [
|
||||||
|
{ ...API_RESPONSE.data[0], id: "deepseek/deepseek-v4-flash" },
|
||||||
|
{ ...API_RESPONSE.data[0], id: "new-model-without-metadata" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(models[0]?.reasoning, true)
|
||||||
|
assert.equal(models[1]?.reasoning, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("builds explicit maps for every known effort set", () => {
|
||||||
|
for (const [modelId, efforts] of Object.entries(MODEL_EFFORTS)) {
|
||||||
|
const metadata = thinkingMetadataForModel(modelId)
|
||||||
|
assert.ok(metadata, `${modelId} should have reasoning metadata`)
|
||||||
|
for (const level of ["minimal", "low", "medium", "high", "xhigh", "max"] as const) {
|
||||||
|
const expected = efforts.includes(level)
|
||||||
|
assert.equal(
|
||||||
|
metadata.thinkingLevelMap[level],
|
||||||
|
expected ? level : null,
|
||||||
|
`${modelId} should map ${level} according to its catalog entry`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.deepEqual(thinkingLevelMapForEfforts(MODEL_EFFORTS["deepseek/deepseek-v4-flash"]), {
|
||||||
|
minimal: null,
|
||||||
|
low: null,
|
||||||
|
medium: null,
|
||||||
|
high: "high",
|
||||||
|
xhigh: null,
|
||||||
|
max: "max",
|
||||||
|
})
|
||||||
|
assert.deepEqual(thinkingMetadataForModel("new-model-without-metadata"), undefined)
|
||||||
|
})
|
||||||
|
|
||||||
it("rejects unexpected API shapes", () => {
|
it("rejects unexpected API shapes", () => {
|
||||||
assert.throws(() => commandCodeModelsFromApiResponse({ object: "list", data: [{}] }))
|
assert.throws(() => commandCodeModelsFromApiResponse({ object: "list", data: [{}] }))
|
||||||
})
|
})
|
||||||
@@ -78,6 +132,20 @@ describe("commandCodeModelsFromCache()", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("normalizes cached reasoning metadata from the model id", () => {
|
||||||
|
const cached = commandCodeModelsFromCache({
|
||||||
|
version: 1,
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
...EXPECTED_MODELS[0],
|
||||||
|
id: "deepseek/deepseek-v4-flash",
|
||||||
|
reasoning: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
assert.equal(cached[0]?.reasoning, true)
|
||||||
|
})
|
||||||
|
|
||||||
it("rejects empty, invalid, and unsupported caches", () => {
|
it("rejects empty, invalid, and unsupported caches", () => {
|
||||||
assert.throws(() => commandCodeModelsFromCache({ version: 1, models: [] }))
|
assert.throws(() => commandCodeModelsFromCache({ version: 1, models: [] }))
|
||||||
assert.throws(() => commandCodeModelsFromCache({ version: 2, models: EXPECTED_MODELS }))
|
assert.throws(() => commandCodeModelsFromCache({ version: 2, models: EXPECTED_MODELS }))
|
||||||
@@ -90,7 +158,58 @@ describe("commandCodeModelsFromCache()", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("model discovery configuration", () => {
|
||||||
|
it("uses a safe default timeout and ignores invalid environment values", () => {
|
||||||
|
assert.equal(getModelsTimeoutMs({}), DEFAULT_MODELS_TIMEOUT_MS)
|
||||||
|
assert.equal(
|
||||||
|
getModelsTimeoutMs({ COMMANDCODE_MODELS_TIMEOUT_MS: "0" }),
|
||||||
|
DEFAULT_MODELS_TIMEOUT_MS,
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
getModelsTimeoutMs({ COMMANDCODE_MODELS_TIMEOUT_MS: "invalid" }),
|
||||||
|
DEFAULT_MODELS_TIMEOUT_MS,
|
||||||
|
)
|
||||||
|
assert.equal(getModelsTimeoutMs({ COMMANDCODE_MODELS_TIMEOUT_MS: "25" }), 25)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("loadCommandCodeModels()", () => {
|
describe("loadCommandCodeModels()", () => {
|
||||||
|
it("falls back to cache when live discovery times out", async () => {
|
||||||
|
await withTemporaryCache(async ({ cachePath }) => {
|
||||||
|
await loadCommandCodeModels({ cachePath, fetchImpl: successfulFetch() })
|
||||||
|
|
||||||
|
const startedAt = Date.now()
|
||||||
|
const result = await loadCommandCodeModels({
|
||||||
|
cachePath,
|
||||||
|
fetchImpl: hangingFetch(),
|
||||||
|
timeoutMs: 25,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.ok(Date.now() - startedAt < 500)
|
||||||
|
assert.deepEqual(result.models, EXPECTED_MODELS)
|
||||||
|
assert.equal(result.source, "cache")
|
||||||
|
assert.match(result.warning ?? "", /timed out after 25ms/)
|
||||||
|
assert.match(result.warning ?? "", /Using the cached catalog/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("preserves an external abort instead of falling back to cache", async () => {
|
||||||
|
await withTemporaryCache(async ({ cachePath }) => {
|
||||||
|
await loadCommandCodeModels({ cachePath, fetchImpl: successfulFetch() })
|
||||||
|
const controller = new AbortController()
|
||||||
|
const promise = loadCommandCodeModels({
|
||||||
|
cachePath,
|
||||||
|
fetchImpl: hangingFetch(),
|
||||||
|
timeoutMs: 1_000,
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
|
||||||
|
controller.abort(new Error("caller cancelled discovery"))
|
||||||
|
|
||||||
|
await assert.rejects(promise, /caller cancelled discovery/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("returns live models and writes a validated cache", async () => {
|
it("returns live models and writes a validated cache", async () => {
|
||||||
await withTemporaryCache(async ({ cachePath }) => {
|
await withTemporaryCache(async ({ cachePath }) => {
|
||||||
const result = await loadCommandCodeModels({
|
const result = await loadCommandCodeModels({
|
||||||
@@ -132,7 +251,7 @@ describe("loadCommandCodeModels()", () => {
|
|||||||
assert.deepEqual(result.models, [])
|
assert.deepEqual(result.models, [])
|
||||||
assert.equal(result.source, "empty")
|
assert.equal(result.source, "empty")
|
||||||
assert.match(result.warning ?? "", /no valid cached catalog/)
|
assert.match(result.warning ?? "", /no valid cached catalog/)
|
||||||
assert.match(result.warning ?? "", /until \/reload succeeds/)
|
assert.match(result.warning ?? "", /until \/commandcode-refresh succeeds/)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -277,9 +277,12 @@ try {
|
|||||||
)
|
)
|
||||||
assert.equal(firstOfflineList.code, 0, firstOfflineList.stderr)
|
assert.equal(firstOfflineList.code, 0, firstOfflineList.stderr)
|
||||||
assert.doesNotMatch(firstOfflineList.stderr, /Failed to load extension/)
|
assert.doesNotMatch(firstOfflineList.stderr, /Failed to load extension/)
|
||||||
assert.match(firstOfflineList.stdout || firstOfflineList.stderr, /No models matching/)
|
assert.match(
|
||||||
|
firstOfflineList.stdout || firstOfflineList.stderr,
|
||||||
|
/No models matching|No models available/,
|
||||||
|
)
|
||||||
assert.match(firstOfflineList.stderr, /no valid cached catalog/)
|
assert.match(firstOfflineList.stderr, /no valid cached catalog/)
|
||||||
assert.match(firstOfflineList.stderr, /until \/reload succeeds/)
|
assert.match(firstOfflineList.stderr, /until \/commandcode-refresh succeeds/)
|
||||||
assert.throws(() => accessSync(modelsCachePath, constants.R_OK), /ENOENT|no such file/i)
|
assert.throws(() => accessSync(modelsCachePath, constants.R_OK), /ENOENT|no such file/i)
|
||||||
|
|
||||||
// A fresh process re-runs the extension entrypoint, which is the same path /reload uses.
|
// A fresh process re-runs the extension entrypoint, which is the same path /reload uses.
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import { describe, it } from "node:test"
|
||||||
|
|
||||||
|
import {
|
||||||
|
createCommandCodeRuntime,
|
||||||
|
type CommandCodeCommandContext,
|
||||||
|
type CommandCodeRuntimeApi,
|
||||||
|
} from "../src/runtime.ts"
|
||||||
|
import type { CommandCodeModel, LoadCommandCodeModelsResult } from "../src/models.ts"
|
||||||
|
|
||||||
|
type ProviderConfig = {
|
||||||
|
models: readonly CommandCodeModel[]
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExtensionAPITestDouble implements CommandCodeRuntimeApi<ProviderConfig, CommandContext> {
|
||||||
|
readonly providers: ProviderConfig[] = []
|
||||||
|
readonly commands = new Map<string, (args: string, ctx: CommandContext) => Promise<void> | void>()
|
||||||
|
|
||||||
|
registerProvider(_name: string, config: ProviderConfig): void {
|
||||||
|
this.providers.push(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerCommand(
|
||||||
|
name: string,
|
||||||
|
options: {
|
||||||
|
description: string
|
||||||
|
handler: (args: string, ctx: CommandContext) => Promise<void> | void
|
||||||
|
},
|
||||||
|
): void {
|
||||||
|
this.commands.set(name, options.handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommandContext implements CommandCodeCommandContext {
|
||||||
|
readonly notifications: Array<{ message: string; type?: "info" | "warning" | "error" }> = []
|
||||||
|
waitForIdleCalls = 0
|
||||||
|
|
||||||
|
readonly ui = {
|
||||||
|
notify: (message: string, type?: "info" | "warning" | "error") => {
|
||||||
|
this.notifications.push({ message, type })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForIdle(): Promise<void> {
|
||||||
|
this.waitForIdleCalls += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FIRST_MODEL: CommandCodeModel = {
|
||||||
|
id: "first-model",
|
||||||
|
name: "First Model",
|
||||||
|
reasoning: true,
|
||||||
|
contextWindow: 128_000,
|
||||||
|
maxTokens: 16_384,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECOND_MODEL: CommandCodeModel = {
|
||||||
|
id: "second-model",
|
||||||
|
name: "Second Model",
|
||||||
|
reasoning: true,
|
||||||
|
contextWindow: 256_000,
|
||||||
|
maxTokens: 32_768,
|
||||||
|
}
|
||||||
|
|
||||||
|
function loaded(
|
||||||
|
models: readonly CommandCodeModel[],
|
||||||
|
source: LoadCommandCodeModelsResult["source"] = "live",
|
||||||
|
warning?: string,
|
||||||
|
): LoadCommandCodeModelsResult {
|
||||||
|
return warning ? { models, source, warning } : { models, source }
|
||||||
|
}
|
||||||
|
|
||||||
|
function deferred<T>(): {
|
||||||
|
promise: Promise<T>
|
||||||
|
resolve(value: T): void
|
||||||
|
reject(error: unknown): void
|
||||||
|
} {
|
||||||
|
let resolvePromise: (value: T) => void = () => {}
|
||||||
|
let rejectPromise: (error: unknown) => void = () => {}
|
||||||
|
const promise = new Promise<T>((resolve, reject) => {
|
||||||
|
resolvePromise = resolve
|
||||||
|
rejectPromise = reject
|
||||||
|
})
|
||||||
|
return { promise, resolve: resolvePromise, reject: rejectPromise }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Command Code runtime", () => {
|
||||||
|
it("registers refresh and status commands and exposes redacted state", async () => {
|
||||||
|
const pi = new ExtensionAPITestDouble()
|
||||||
|
const context = new CommandContext()
|
||||||
|
let now = 1_700_000_000_000
|
||||||
|
const firstLoad = deferred<LoadCommandCodeModelsResult>()
|
||||||
|
|
||||||
|
const runtime = createCommandCodeRuntime(pi, {
|
||||||
|
endpoint: "https://api.commandcode.ai/provider/v1/models?token=user_secret_value",
|
||||||
|
cachePath: "/tmp/commandcode-models.json",
|
||||||
|
loadModels: () => firstLoad.promise,
|
||||||
|
createProviderConfig: (models) => ({ models }),
|
||||||
|
now: () => now,
|
||||||
|
logWarning: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialization = runtime.initialize()
|
||||||
|
assert.deepEqual([...pi.commands.keys()], ["commandcode-refresh", "commandcode-status"])
|
||||||
|
assert.equal(runtime.getStatus().refreshing, true)
|
||||||
|
assert.equal(runtime.getStatus().lastAttempt, now)
|
||||||
|
|
||||||
|
firstLoad.resolve(loaded([FIRST_MODEL]))
|
||||||
|
await initialization
|
||||||
|
now += 1_000
|
||||||
|
|
||||||
|
const statusCommand = pi.commands.get("commandcode-status")
|
||||||
|
assert.ok(statusCommand)
|
||||||
|
await statusCommand("", context)
|
||||||
|
const statusMessage = context.notifications.at(-1)?.message ?? ""
|
||||||
|
assert.match(statusMessage, /source: live/)
|
||||||
|
assert.match(statusMessage, /model count: 1/)
|
||||||
|
assert.match(statusMessage, /last success:/)
|
||||||
|
assert.match(statusMessage, /last attempt:/)
|
||||||
|
assert.match(statusMessage, /cache path: \/tmp\/commandcode-models\.json/)
|
||||||
|
assert.match(statusMessage, /endpoint: https:\/\/api\.commandcode\.ai\/provider\/v1\/models/)
|
||||||
|
assert.doesNotMatch(statusMessage, /token=user_secret_value/)
|
||||||
|
assert.doesNotMatch(statusMessage, /user_secret_value/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("coalesces overlapping refreshes and preserves the current catalog on failure", async () => {
|
||||||
|
const pi = new ExtensionAPITestDouble()
|
||||||
|
const warnings: string[] = []
|
||||||
|
const loads = [Promise.resolve(loaded([FIRST_MODEL])), deferred<LoadCommandCodeModelsResult>()]
|
||||||
|
let loadCount = 0
|
||||||
|
|
||||||
|
const runtime = createCommandCodeRuntime(pi, {
|
||||||
|
endpoint: "https://api.commandcode.ai/provider/v1/models",
|
||||||
|
cachePath: "/tmp/commandcode-models.json",
|
||||||
|
loadModels: () => {
|
||||||
|
const next = loads[loadCount]
|
||||||
|
loadCount += 1
|
||||||
|
if (!next) throw new Error("unexpected refresh")
|
||||||
|
return next instanceof Promise ? next : next.promise
|
||||||
|
},
|
||||||
|
createProviderConfig: (models) => ({ models }),
|
||||||
|
logWarning: (warning) => warnings.push(warning),
|
||||||
|
})
|
||||||
|
|
||||||
|
await runtime.initialize()
|
||||||
|
assert.equal(pi.providers.length, 1)
|
||||||
|
assert.deepEqual(pi.providers[0]?.models, [FIRST_MODEL])
|
||||||
|
|
||||||
|
const pending = loads[1]
|
||||||
|
assert.ok(!(pending instanceof Promise))
|
||||||
|
const firstRefresh = runtime.refresh()
|
||||||
|
const secondRefresh = runtime.refresh()
|
||||||
|
assert.strictEqual(firstRefresh, secondRefresh)
|
||||||
|
assert.equal(runtime.getStatus().refreshing, true)
|
||||||
|
|
||||||
|
pending.reject(new Error("request failed with apiKey=user_secret_value"))
|
||||||
|
const result = await firstRefresh
|
||||||
|
|
||||||
|
assert.equal(result.refreshed, false)
|
||||||
|
assert.equal(result.modelCount, 1)
|
||||||
|
assert.equal(runtime.getStatus().modelCount, 1)
|
||||||
|
assert.equal(runtime.getStatus().source, "live")
|
||||||
|
assert.equal(pi.providers.length, 1)
|
||||||
|
assert.equal(runtime.getStatus().refreshing, false)
|
||||||
|
assert.match(runtime.getStatus().warning ?? "", /Could not refresh/)
|
||||||
|
assert.doesNotMatch(runtime.getStatus().warning ?? "", /user_secret_value/)
|
||||||
|
assert.doesNotMatch(warnings.join("\n"), /user_secret_value/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("runs the refresh command and reports the updated catalog", async () => {
|
||||||
|
const pi = new ExtensionAPITestDouble()
|
||||||
|
const context = new CommandContext()
|
||||||
|
const results = [
|
||||||
|
Promise.resolve(loaded([FIRST_MODEL])),
|
||||||
|
Promise.resolve(loaded([FIRST_MODEL, SECOND_MODEL])),
|
||||||
|
]
|
||||||
|
let index = 0
|
||||||
|
|
||||||
|
const runtime = createCommandCodeRuntime(pi, {
|
||||||
|
endpoint: "https://api.commandcode.ai/provider/v1/models",
|
||||||
|
cachePath: "/tmp/commandcode-models.json",
|
||||||
|
loadModels: () => {
|
||||||
|
const result = results[index]
|
||||||
|
index += 1
|
||||||
|
if (!result) throw new Error("unexpected refresh")
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
createProviderConfig: (models) => ({ models }),
|
||||||
|
logWarning: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
await runtime.initialize()
|
||||||
|
const refreshCommand = pi.commands.get("commandcode-refresh")
|
||||||
|
assert.ok(refreshCommand)
|
||||||
|
await refreshCommand("", context)
|
||||||
|
|
||||||
|
assert.equal(context.waitForIdleCalls, 1)
|
||||||
|
assert.equal(context.notifications.at(-1)?.type, "info")
|
||||||
|
assert.match(context.notifications.at(-1)?.message ?? "", /2 models from live/)
|
||||||
|
assert.deepEqual(pi.providers.at(-1)?.models, [FIRST_MODEL, SECOND_MODEL])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("installs a cached catalog after an initially empty start", async () => {
|
||||||
|
const pi = new ExtensionAPITestDouble()
|
||||||
|
const results = [
|
||||||
|
Promise.resolve(loaded([], "empty", "offline")),
|
||||||
|
Promise.resolve(loaded([SECOND_MODEL], "cache")),
|
||||||
|
]
|
||||||
|
let index = 0
|
||||||
|
|
||||||
|
const runtime = createCommandCodeRuntime(pi, {
|
||||||
|
endpoint: "https://api.commandcode.ai/provider/v1/models",
|
||||||
|
cachePath: "/tmp/commandcode-models.json",
|
||||||
|
loadModels: () => {
|
||||||
|
const result = results[index]
|
||||||
|
index += 1
|
||||||
|
if (!result) throw new Error("unexpected refresh")
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
createProviderConfig: (models) => ({ models }),
|
||||||
|
logWarning: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
await runtime.initialize()
|
||||||
|
assert.equal(pi.providers.length, 1)
|
||||||
|
assert.deepEqual(pi.providers[0]?.models, [])
|
||||||
|
|
||||||
|
const result = await runtime.refresh()
|
||||||
|
assert.equal(result.refreshed, true)
|
||||||
|
assert.equal(result.source, "cache")
|
||||||
|
assert.deepEqual(pi.providers.at(-1)?.models, [SECOND_MODEL])
|
||||||
|
assert.equal(runtime.getStatus().modelCount, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not replace an existing provider with an empty failed catalog", async () => {
|
||||||
|
const pi = new ExtensionAPITestDouble()
|
||||||
|
const results = [
|
||||||
|
Promise.resolve(loaded([FIRST_MODEL])),
|
||||||
|
Promise.resolve(loaded([], "cache", "No valid catalog is available at /private/cache")),
|
||||||
|
Promise.resolve(loaded([SECOND_MODEL])),
|
||||||
|
]
|
||||||
|
let index = 0
|
||||||
|
|
||||||
|
const runtime = createCommandCodeRuntime(pi, {
|
||||||
|
endpoint: "http://127.0.0.1:1234/provider/v1/models",
|
||||||
|
cachePath: "/private/cache",
|
||||||
|
loadModels: () => {
|
||||||
|
const result = results[index]
|
||||||
|
index += 1
|
||||||
|
if (!result) throw new Error("unexpected refresh")
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
createProviderConfig: (models) => ({ models }),
|
||||||
|
logWarning: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
await runtime.initialize()
|
||||||
|
const refreshResult = await runtime.refresh()
|
||||||
|
assert.equal(refreshResult.refreshed, false)
|
||||||
|
assert.equal(pi.providers.length, 1)
|
||||||
|
assert.deepEqual(pi.providers[0]?.models, [FIRST_MODEL])
|
||||||
|
assert.equal(runtime.getStatus().modelCount, 1)
|
||||||
|
assert.equal(runtime.getStatus().source, "live")
|
||||||
|
|
||||||
|
await runtime.refresh()
|
||||||
|
assert.equal(pi.providers.length, 2)
|
||||||
|
assert.deepEqual(pi.providers[1]?.models, [SECOND_MODEL])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("reports a failed initial refresh without leaking diagnostics", async () => {
|
||||||
|
const pi = new ExtensionAPITestDouble()
|
||||||
|
const context = new CommandContext()
|
||||||
|
const runtime = createCommandCodeRuntime(pi, {
|
||||||
|
endpoint: "https://api.commandcode.ai/provider/v1/models?api_key=user_initial_secret",
|
||||||
|
cachePath: "/tmp/commandcode-models.json",
|
||||||
|
loadModels: async () => {
|
||||||
|
throw new Error("offline; api_key=user_initial_secret")
|
||||||
|
},
|
||||||
|
createProviderConfig: (models) => ({ models }),
|
||||||
|
logWarning: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
await runtime.initialize()
|
||||||
|
const statusCommand = pi.commands.get("commandcode-status")
|
||||||
|
assert.ok(statusCommand)
|
||||||
|
await statusCommand("", context)
|
||||||
|
const message = context.notifications.at(-1)?.message ?? ""
|
||||||
|
assert.match(message, /source: empty/)
|
||||||
|
assert.match(message, /model count: 0/)
|
||||||
|
assert.match(message, /warning:/)
|
||||||
|
assert.doesNotMatch(message, /user_initial_secret/)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -7,6 +7,7 @@ import assert from "node:assert/strict"
|
|||||||
import { after, before, beforeEach, describe, it } from "node:test"
|
import { after, before, beforeEach, describe, it } from "node:test"
|
||||||
|
|
||||||
import type { AssistantMessageEvent } from "../src/core.ts"
|
import type { AssistantMessageEvent } from "../src/core.ts"
|
||||||
|
import { MODEL_EFFORTS, thinkingLevelMapForEfforts } from "../src/models.ts"
|
||||||
import {
|
import {
|
||||||
collectEvents,
|
collectEvents,
|
||||||
createTestDeps,
|
createTestDeps,
|
||||||
@@ -415,6 +416,84 @@ describe("streamCommandCode — request serialization", () => {
|
|||||||
assert.equal(headers["x-session-id"], undefined)
|
assert.equal(headers["x-session-id"], undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("accepts the legacy OMP nested reasoning map", async () => {
|
||||||
|
server.mockResponse({
|
||||||
|
type: "success",
|
||||||
|
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
|
||||||
|
})
|
||||||
|
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||||
|
const model = makeModel({
|
||||||
|
id: "omp-compat-reasoning-model",
|
||||||
|
reasoning: true,
|
||||||
|
thinking: { effortMap: { high: "legacy-high" } },
|
||||||
|
})
|
||||||
|
|
||||||
|
await collectEvents(
|
||||||
|
streamCommandCode(model, makeContext(), { apiKey: "mock-key", reasoning: "high" }),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(objectAt(server.lastRequestBody(), ["params", "reasoning_effort"]), "legacy-high")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("forwards a supported Pi reasoning level as reasoning_effort", async () => {
|
||||||
|
server.mockResponse({
|
||||||
|
type: "success",
|
||||||
|
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
|
||||||
|
})
|
||||||
|
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||||
|
const model = makeModel({
|
||||||
|
id: "deepseek/deepseek-v4-flash",
|
||||||
|
reasoning: true,
|
||||||
|
thinkingLevelMap: thinkingLevelMapForEfforts(MODEL_EFFORTS["deepseek/deepseek-v4-flash"]),
|
||||||
|
})
|
||||||
|
|
||||||
|
await collectEvents(
|
||||||
|
streamCommandCode(model, makeContext(), { apiKey: "mock-key", reasoning: "max" }),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(objectAt(server.lastRequestBody(), ["params", "reasoning_effort"]), "max")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("omits reasoning_effort for off, unsupported, and unknown reasoning levels", async () => {
|
||||||
|
const model = makeModel({
|
||||||
|
id: "deepseek/deepseek-v4-flash",
|
||||||
|
reasoning: true,
|
||||||
|
thinkingLevelMap: thinkingLevelMapForEfforts(MODEL_EFFORTS["deepseek/deepseek-v4-flash"]),
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const reasoning of ["off", "low"] as const) {
|
||||||
|
server.mockResponse({
|
||||||
|
type: "success",
|
||||||
|
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
|
||||||
|
})
|
||||||
|
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||||
|
|
||||||
|
await collectEvents(
|
||||||
|
streamCommandCode(model, makeContext(), { apiKey: "mock-key", reasoning }),
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
objectAt(server.lastRequestBody(), ["params", "reasoning_effort"]),
|
||||||
|
undefined,
|
||||||
|
`${reasoning} should not be sent when it has no supported Command Code field`,
|
||||||
|
)
|
||||||
|
server.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
server.mockResponse({
|
||||||
|
type: "success",
|
||||||
|
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
|
||||||
|
})
|
||||||
|
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||||
|
await collectEvents(
|
||||||
|
streamCommandCode(
|
||||||
|
makeModel({ id: "new-model-without-metadata", reasoning: false }),
|
||||||
|
makeContext(),
|
||||||
|
{ apiKey: "mock-key", reasoning: "high" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert.equal(objectAt(server.lastRequestBody(), ["params", "reasoning_effort"]), undefined)
|
||||||
|
})
|
||||||
|
|
||||||
it("caps maxTokens and passes custom headers", async () => {
|
it("caps maxTokens and passes custom headers", async () => {
|
||||||
server.mockResponse({
|
server.mockResponse({
|
||||||
type: "success",
|
type: "success",
|
||||||
|
|||||||
Reference in New Issue
Block a user