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
```
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
+4 -2
View File
@@ -13,7 +13,8 @@
*/
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 { 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 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 = {
input: number
+4 -9
View File
@@ -1,6 +1,5 @@
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
import { homedir } from "node:os"
import { dirname, join } from "node:path"
import { dirname } from "node:path"
export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models"
@@ -27,7 +26,7 @@ interface FetchCommandCodeModelsOptions {
}
interface LoadCommandCodeModelsOptions extends FetchCommandCodeModelsOptions {
cachePath?: string
cachePath: string
}
export interface LoadCommandCodeModelsResult {
@@ -93,10 +92,6 @@ function errorMessage(error: unknown): string {
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[] {
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'")
@@ -174,9 +169,9 @@ async function writeCommandCodeModelsCache(
}
export async function loadCommandCodeModels(
options: LoadCommandCodeModelsOptions = {},
options: LoadCommandCodeModelsOptions,
): Promise<LoadCommandCodeModelsResult> {
const cachePath = options.cachePath ?? defaultCommandCodeModelsCachePath()
const cachePath = options.cachePath
try {
const models = await fetchCommandCodeModels(options)
+3
View File
@@ -166,6 +166,9 @@ try {
assert.match(listOutput, /commandcode/)
assert.match(listOutput, /deepseek\/deepseek-v4-flash/)
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/)
console.log("[omp-compat] print mode through real extension and mock API")
+4 -2
View File
@@ -129,10 +129,10 @@ const env = {
...process.env,
HOME: tempHome,
USERPROFILE: tempHome,
PI_CODING_AGENT_DIR: join(tempHome, "custom-pi-agent"),
COMMANDCODE_API_BASE: apiBase,
COMMANDCODE_API_KEY: "mock-key",
COMMANDCODE_MODELS_URL: `${apiBase}/provider/v1/models`,
COMMANDCODE_MODELS_CACHE: join(tempHome, "commandcode-models.json"),
}
function runPi(args, timeoutMs = 30_000) {
@@ -269,7 +269,8 @@ try {
console.log("[pi-local] first offline start without a cache")
const onlineModelsUrl = env.COMMANDCODE_MODELS_URL
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(
["--no-extensions", "-e", EXT_PATH, "--list-models", "commandcode"],
20_000,
@@ -289,6 +290,7 @@ try {
assert.match(listOutput, /cc-offline-cache-model/)
assert.match(listOutput, /cc-second-model/)
assert.equal(modelListRequestCount, 1)
assert.doesNotThrow(() => accessSync(modelsCachePath, constants.R_OK))
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"