fix(models): store cache in agent directory

Resolve the Command Code model cache through the host's getAgentDir helper so pi, OMP, and PI_CODING_AGENT_DIR use their own agent state directory instead of the official Command Code client directory.
This commit is contained in:
Patrick Wozniak
2026-08-02 01:37:34 +02:00
parent 070ef3c07a
commit b1a60da6b4
5 changed files with 17 additions and 15 deletions
+2 -2
View File
@@ -134,9 +134,9 @@ On startup, the provider fetches:
https://api.commandcode.ai/provider/v1/models https://api.commandcode.ai/provider/v1/models
``` ```
The last successfully fetched catalog is cached at `~/.commandcode/pi-models.json`. If model discovery is temporarily unavailable, the provider uses this cached catalog so previously discovered Command Code models remain selectable. On a first offline start without a cache, pi still loads, but Command Code models remain unavailable until the connection is restored and `/reload` succeeds. The last successfully fetched catalog is cached at `<agent-dir>/commandcode-models.json` (`~/.pi/agent/commandcode-models.json` by default). The agent directory follows pi's `PI_CODING_AGENT_DIR` setting, so compatible hosts such as OMP keep the cache in their own agent directory. If model discovery is temporarily unavailable, the provider uses this cached catalog so previously discovered Command Code models remain selectable. On a first offline start without a cache, pi still loads, but Command Code models remain unavailable until the connection is restored and `/reload` succeeds.
For tests or local mocks, override the endpoint with `COMMANDCODE_MODELS_URL` and the cache location with `COMMANDCODE_MODELS_CACHE`. For tests or local mocks, override the endpoint with `COMMANDCODE_MODELS_URL` and the cache file with `COMMANDCODE_MODELS_CACHE`.
## Pricing ## Pricing
+4 -2
View File
@@ -13,7 +13,8 @@
*/ */
import { AssistantMessageEventStream } from "@earendil-works/pi-ai" import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent"
import { join } from "node:path"
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"
@@ -22,7 +23,8 @@ import { getApiKey, login, refreshToken } from "./src/oauth.ts"
const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE
const MODELS_URL = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL const MODELS_URL = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
const MODELS_CACHE_PATH = process.env.COMMANDCODE_MODELS_CACHE const MODELS_CACHE_PATH =
process.env.COMMANDCODE_MODELS_CACHE ?? join(getAgentDir(), "commandcode-models.json")
type CommandCodeModelCost = { type CommandCodeModelCost = {
input: number input: number
+4 -9
View File
@@ -1,6 +1,5 @@
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises" import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
import { homedir } from "node:os" import { dirname } from "node:path"
import { dirname, join } 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"
@@ -27,7 +26,7 @@ interface FetchCommandCodeModelsOptions {
} }
interface LoadCommandCodeModelsOptions extends FetchCommandCodeModelsOptions { interface LoadCommandCodeModelsOptions extends FetchCommandCodeModelsOptions {
cachePath?: string cachePath: string
} }
export interface LoadCommandCodeModelsResult { export interface LoadCommandCodeModelsResult {
@@ -93,10 +92,6 @@ function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error) return error instanceof Error ? error.message : String(error)
} }
export function defaultCommandCodeModelsCachePath(): string {
return join(homedir(), ".commandcode", "pi-models.json")
}
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'")
@@ -174,9 +169,9 @@ async function writeCommandCodeModelsCache(
} }
export async function loadCommandCodeModels( export async function loadCommandCodeModels(
options: LoadCommandCodeModelsOptions = {}, options: LoadCommandCodeModelsOptions,
): Promise<LoadCommandCodeModelsResult> { ): Promise<LoadCommandCodeModelsResult> {
const cachePath = options.cachePath ?? defaultCommandCodeModelsCachePath() const cachePath = options.cachePath
try { try {
const models = await fetchCommandCodeModels(options) const models = await fetchCommandCodeModels(options)
+3
View File
@@ -166,6 +166,9 @@ try {
assert.match(listOutput, /commandcode/) assert.match(listOutput, /commandcode/)
assert.match(listOutput, /deepseek\/deepseek-v4-flash/) assert.match(listOutput, /deepseek\/deepseek-v4-flash/)
assert.equal(modelListRequestCount, 1) assert.equal(modelListRequestCount, 1)
assert.doesNotThrow(() =>
accessSync(join(tempHome, ".omp", "agent", "commandcode-models.json"), constants.R_OK),
)
assert.doesNotMatch(result.stdout + result.stderr, /Failed to load extension/) assert.doesNotMatch(result.stdout + result.stderr, /Failed to load extension/)
console.log("[omp-compat] print mode through real extension and mock API") console.log("[omp-compat] print mode through real extension and mock API")
+4 -2
View File
@@ -129,10 +129,10 @@ const env = {
...process.env, ...process.env,
HOME: tempHome, HOME: tempHome,
USERPROFILE: tempHome, USERPROFILE: tempHome,
PI_CODING_AGENT_DIR: join(tempHome, "custom-pi-agent"),
COMMANDCODE_API_BASE: apiBase, COMMANDCODE_API_BASE: apiBase,
COMMANDCODE_API_KEY: "mock-key", COMMANDCODE_API_KEY: "mock-key",
COMMANDCODE_MODELS_URL: `${apiBase}/provider/v1/models`, COMMANDCODE_MODELS_URL: `${apiBase}/provider/v1/models`,
COMMANDCODE_MODELS_CACHE: join(tempHome, "commandcode-models.json"),
} }
function runPi(args, timeoutMs = 30_000) { function runPi(args, timeoutMs = 30_000) {
@@ -269,7 +269,8 @@ try {
console.log("[pi-local] first offline start without a cache") console.log("[pi-local] first offline start without a cache")
const onlineModelsUrl = env.COMMANDCODE_MODELS_URL const onlineModelsUrl = env.COMMANDCODE_MODELS_URL
env.COMMANDCODE_MODELS_URL = "http://127.0.0.1:1/provider/v1/models" env.COMMANDCODE_MODELS_URL = "http://127.0.0.1:1/provider/v1/models"
rmSync(env.COMMANDCODE_MODELS_CACHE, { force: true }) const modelsCachePath = join(env.PI_CODING_AGENT_DIR, "commandcode-models.json")
rmSync(modelsCachePath, { force: true })
const firstOfflineList = await runPi( const firstOfflineList = await runPi(
["--no-extensions", "-e", EXT_PATH, "--list-models", "commandcode"], ["--no-extensions", "-e", EXT_PATH, "--list-models", "commandcode"],
20_000, 20_000,
@@ -289,6 +290,7 @@ try {
assert.match(listOutput, /cc-offline-cache-model/) assert.match(listOutput, /cc-offline-cache-model/)
assert.match(listOutput, /cc-second-model/) assert.match(listOutput, /cc-second-model/)
assert.equal(modelListRequestCount, 1) assert.equal(modelListRequestCount, 1)
assert.doesNotThrow(() => accessSync(modelsCachePath, constants.R_OK))
console.log("[pi-local] list cached models while model discovery is offline") console.log("[pi-local] list cached models while model discovery is offline")
env.COMMANDCODE_MODELS_URL = "http://127.0.0.1:1/provider/v1/models" env.COMMANDCODE_MODELS_URL = "http://127.0.0.1:1/provider/v1/models"