sf snapshot: uncommitted changes after 56m inactivity
This commit is contained in:
parent
4442400d11
commit
b73e386090
7 changed files with 80 additions and 72 deletions
|
|
@ -512,9 +512,9 @@ export function registerHooks(pi, ecosystemHandlers = []) {
|
|||
}
|
||||
// Refresh the gemini-cli model catalog separately because google-gemini-cli
|
||||
// uses OAuth via @google/gemini-cli-core, not API-key REST, so it is not
|
||||
// reachable through the generic refresh above. The cache lands in
|
||||
// .sf/runtime/model-catalog/google-gemini-cli.json so getKnownModelIds and
|
||||
// the model picker pick it up the same way as other providers.
|
||||
// reachable through the generic refresh above. The cache lands in the
|
||||
// global ~/.sf/model-catalog/google-gemini-cli.json so getKnownModelIds
|
||||
// and the model picker pick it up the same way as other providers.
|
||||
try {
|
||||
const { scheduleGeminiCatalogRefresh } = await import(
|
||||
"../gemini-catalog.js"
|
||||
|
|
@ -524,7 +524,7 @@ export function registerHooks(pi, ecosystemHandlers = []) {
|
|||
/* non-fatal — gemini catalog refresh must never block session start */
|
||||
}
|
||||
// Refresh the openai-codex model catalog by mirroring the codex CLI's
|
||||
// own ~/.codex/models_cache.json into .sf/runtime/model-catalog/
|
||||
// own ~/.codex/models_cache.json into ~/.sf/model-catalog/
|
||||
// openai-codex.json. Without this, the static catalog in
|
||||
// models.generated.ts carries phantom slugs (e.g. gpt-5-codex) that
|
||||
// the ChatGPT-account API rejects with 400 ("model is not supported
|
||||
|
|
|
|||
|
|
@ -15,22 +15,22 @@
|
|||
*/
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { sfRuntimeRoot } from "./paths.js";
|
||||
import { sfHome } from "./sf-home.js";
|
||||
|
||||
const GEMINI_CLI_PROVIDER_ID = "google-gemini-cli";
|
||||
const CATALOG_TTL_MS = 6 * 60 * 60 * 1000;
|
||||
|
||||
function cacheFilePath(basePath) {
|
||||
return join(
|
||||
sfRuntimeRoot(basePath),
|
||||
"model-catalog",
|
||||
`${GEMINI_CLI_PROVIDER_ID}.json`,
|
||||
);
|
||||
function cacheDir() {
|
||||
return join(sfHome(), "model-catalog");
|
||||
}
|
||||
|
||||
function isCacheFresh(basePath) {
|
||||
function cacheFilePath() {
|
||||
return join(cacheDir(), `${GEMINI_CLI_PROVIDER_ID}.json`);
|
||||
}
|
||||
|
||||
function isCacheFresh() {
|
||||
try {
|
||||
const path = cacheFilePath(basePath);
|
||||
const path = cacheFilePath();
|
||||
if (!existsSync(path)) return false;
|
||||
const entry = JSON.parse(readFileSync(path, "utf-8"));
|
||||
if (!entry?.fetchedAt || !Array.isArray(entry.modelIds)) return false;
|
||||
|
|
@ -40,13 +40,11 @@ function isCacheFresh(basePath) {
|
|||
}
|
||||
}
|
||||
|
||||
function writeCacheEntry(basePath, modelIds) {
|
||||
function writeCacheEntry(modelIds) {
|
||||
try {
|
||||
mkdirSync(join(sfRuntimeRoot(basePath), "model-catalog"), {
|
||||
recursive: true,
|
||||
});
|
||||
mkdirSync(cacheDir(), { recursive: true });
|
||||
writeFileSync(
|
||||
cacheFilePath(basePath),
|
||||
cacheFilePath(),
|
||||
JSON.stringify({
|
||||
fetchedAt: new Date().toISOString(),
|
||||
modelIds,
|
||||
|
|
@ -71,7 +69,7 @@ export async function refreshGeminiCatalog(basePath) {
|
|||
);
|
||||
const modelIds = await discoverGeminiCliModels(basePath);
|
||||
if (!modelIds || modelIds.length === 0) return null;
|
||||
writeCacheEntry(basePath, modelIds);
|
||||
writeCacheEntry(modelIds);
|
||||
return modelIds;
|
||||
} catch {
|
||||
return null;
|
||||
|
|
@ -85,7 +83,7 @@ export async function refreshGeminiCatalog(basePath) {
|
|||
* Consumer: bootstrap/register-hooks.js session_start hook.
|
||||
*/
|
||||
export function scheduleGeminiCatalogRefresh(basePath) {
|
||||
if (isCacheFresh(basePath)) return;
|
||||
if (isCacheFresh()) return;
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await refreshGeminiCatalog(basePath);
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
* model-catalog-cache.js — live model-list discovery with disk cache.
|
||||
*
|
||||
* Fetches model lists from providers that publish a /v1/models (or equivalent)
|
||||
* endpoint. Results are cached to .sf/runtime/model-catalog/<providerId>.json
|
||||
* with a 6-hour TTL so the model picker and preference validation see fresh
|
||||
* model IDs without blocking the event loop.
|
||||
* endpoint. Results are cached globally to ~/.sf/model-catalog/<providerId>.json
|
||||
* (SF_HOME-aware) with a 6-hour TTL so the model picker and preference validation
|
||||
* see fresh model IDs without blocking the event loop. Catalog contents are
|
||||
* provider-level — identical across projects — so a single global cache avoids
|
||||
* the per-repo re-discovery cost.
|
||||
*
|
||||
* For providers that the SDK's built-in discovery adapter registry does NOT
|
||||
* cover with supportsDiscovery=true (opencode, opencode-go, kimi-coding, xiaomi),
|
||||
|
|
@ -17,7 +19,6 @@
|
|||
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { sfHome } from "./sf-home.js";
|
||||
import { sfRuntimeRoot } from "./paths.js";
|
||||
import {
|
||||
DISCOVERABLE_PROVIDER_IDS,
|
||||
getProviderCatalogConfig,
|
||||
|
|
@ -48,12 +49,14 @@ const SDK_NATIVE_DISCOVERY_PROVIDERS = new Set([
|
|||
|
||||
const SDK_DISCOVERY_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour — matches DISCOVERY_TTLS defaults
|
||||
|
||||
function cacheDirPath(basePath) {
|
||||
return join(sfRuntimeRoot(basePath), "model-catalog");
|
||||
// Global catalog dir — identical across projects. `basePath` is accepted on
|
||||
// public API for backward compat but ignored for path resolution.
|
||||
function cacheDirPath(_basePath) {
|
||||
return join(sfHome(), "model-catalog");
|
||||
}
|
||||
|
||||
function cacheFilePath(basePath, providerId) {
|
||||
return join(cacheDirPath(basePath), `${providerId}.json`);
|
||||
function cacheFilePath(_basePath, providerId) {
|
||||
return join(cacheDirPath(), `${providerId}.json`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,22 +12,22 @@
|
|||
*/
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { sfRuntimeRoot } from "./paths.js";
|
||||
import { sfHome } from "./sf-home.js";
|
||||
|
||||
const PROVIDER_ID = "openai-codex";
|
||||
const CATALOG_TTL_MS = 6 * 60 * 60 * 1000;
|
||||
|
||||
function sfCacheFilePath(basePath) {
|
||||
return join(
|
||||
sfRuntimeRoot(basePath),
|
||||
"model-catalog",
|
||||
`${PROVIDER_ID}.json`,
|
||||
);
|
||||
function sfCacheDir() {
|
||||
return join(sfHome(), "model-catalog");
|
||||
}
|
||||
|
||||
function isSfCacheFresh(basePath) {
|
||||
function sfCacheFilePath() {
|
||||
return join(sfCacheDir(), `${PROVIDER_ID}.json`);
|
||||
}
|
||||
|
||||
function isSfCacheFresh() {
|
||||
try {
|
||||
const path = sfCacheFilePath(basePath);
|
||||
const path = sfCacheFilePath();
|
||||
if (!existsSync(path)) return false;
|
||||
const entry = JSON.parse(readFileSync(path, "utf-8"));
|
||||
if (!entry?.fetchedAt || !Array.isArray(entry.modelIds)) return false;
|
||||
|
|
@ -37,13 +37,11 @@ function isSfCacheFresh(basePath) {
|
|||
}
|
||||
}
|
||||
|
||||
function writeSfCache(basePath, modelIds) {
|
||||
function writeSfCache(modelIds) {
|
||||
try {
|
||||
mkdirSync(join(sfRuntimeRoot(basePath), "model-catalog"), {
|
||||
recursive: true,
|
||||
});
|
||||
mkdirSync(sfCacheDir(), { recursive: true });
|
||||
writeFileSync(
|
||||
sfCacheFilePath(basePath),
|
||||
sfCacheFilePath(),
|
||||
JSON.stringify({
|
||||
fetchedAt: new Date().toISOString(),
|
||||
modelIds,
|
||||
|
|
@ -80,10 +78,10 @@ export async function readCodexAvailableModels() {
|
|||
*
|
||||
* Consumer: scheduleOpenaiCodexCatalogRefresh during session_start.
|
||||
*/
|
||||
export async function refreshOpenaiCodexCatalog(basePath) {
|
||||
export async function refreshOpenaiCodexCatalog(_basePath) {
|
||||
const modelIds = await readCodexAvailableModels();
|
||||
if (!modelIds || modelIds.length === 0) return null;
|
||||
writeSfCache(basePath, modelIds);
|
||||
writeSfCache(modelIds);
|
||||
return modelIds;
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +91,7 @@ export async function refreshOpenaiCodexCatalog(basePath) {
|
|||
* Consumer: bootstrap/register-hooks.js session_start hook.
|
||||
*/
|
||||
export function scheduleOpenaiCodexCatalogRefresh(basePath) {
|
||||
if (isSfCacheFresh(basePath)) return;
|
||||
if (isSfCacheFresh()) return;
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await refreshOpenaiCodexCatalog(basePath);
|
||||
|
|
|
|||
|
|
@ -25,24 +25,36 @@ import { isProviderModelAllowed } from "../preferences-models.js";
|
|||
// ─── Test helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
const tmpDirs = [];
|
||||
let originalSfHomeTop;
|
||||
|
||||
beforeEach(() => {
|
||||
originalSfHomeTop = process.env.SF_HOME;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (tmpDirs.length > 0) {
|
||||
rmSync(tmpDirs.pop(), { recursive: true, force: true });
|
||||
}
|
||||
if (originalSfHomeTop === undefined) delete process.env.SF_HOME;
|
||||
else process.env.SF_HOME = originalSfHomeTop;
|
||||
});
|
||||
|
||||
function tempBasePath() {
|
||||
const dir = mkdtempSync(join(tmpdir(), "sf-discovery-cache-test-"));
|
||||
tmpDirs.push(dir);
|
||||
// Catalog is global (~/.sf/model-catalog) — pin SF_HOME at the temp dir so
|
||||
// each test's reads/writes land in isolation.
|
||||
process.env.SF_HOME = dir;
|
||||
return dir;
|
||||
}
|
||||
|
||||
/** Write a model-catalog cache entry directly for testing. */
|
||||
/** Write a model-catalog cache entry directly for testing.
|
||||
* Catalog is global (~/.sf/model-catalog/) — tests must point SF_HOME at the
|
||||
* temp basePath for isolation. The caller is expected to have set
|
||||
* `process.env.SF_HOME = basePath` before invoking this helper.
|
||||
*/
|
||||
function writeModelCatalogCache(basePath, providerId, modelEntries) {
|
||||
// sfRuntimeRoot(basePath) resolves to sfRoot(basePath) which is basePath/.sf
|
||||
// cacheDirPath = join(sfRuntimeRoot(basePath), "model-catalog") = basePath/.sf/model-catalog
|
||||
const dir = join(basePath, ".sf", "model-catalog");
|
||||
const dir = join(basePath, "model-catalog");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(dir, `${providerId}.json`),
|
||||
|
|
@ -193,7 +205,8 @@ describe("legacy cache migration", () => {
|
|||
test("string[] cache migrates to {id}[] on read", () => {
|
||||
const basePath = tempBasePath();
|
||||
// Write legacy format: modelIds is a string[]
|
||||
const dir = join(basePath, ".sf", "model-catalog");
|
||||
// Global catalog dir (tempBasePath() pins SF_HOME=basePath).
|
||||
const dir = join(basePath, "model-catalog");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(dir, "openrouter.json"),
|
||||
|
|
@ -229,8 +242,9 @@ describe("isProviderModelAllowed — openrouter zero-cost policy", () => {
|
|||
});
|
||||
|
||||
function makeProjectWithDiscoveryCache(modelEntries) {
|
||||
// tempBasePath() pins SF_HOME at the temp dir, so the global catalog
|
||||
// writes/reads land in isolation per test.
|
||||
const basePath = tempBasePath();
|
||||
// Create a valid .sf directory (needed for sfRoot resolution)
|
||||
mkdirSync(join(basePath, ".sf"), { recursive: true });
|
||||
writeModelCatalogCache(basePath, "openrouter", modelEntries);
|
||||
process.chdir(basePath);
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ import {
|
|||
|
||||
const tmpDirs = [];
|
||||
const ORIGINAL_CODEX_HOME = process.env.CODEX_HOME;
|
||||
let originalSfHome;
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.CODEX_HOME;
|
||||
originalSfHome = process.env.SF_HOME;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -33,6 +35,8 @@ afterEach(() => {
|
|||
} else {
|
||||
process.env.CODEX_HOME = ORIGINAL_CODEX_HOME;
|
||||
}
|
||||
if (originalSfHome === undefined) delete process.env.SF_HOME;
|
||||
else process.env.SF_HOME = originalSfHome;
|
||||
while (tmpDirs.length > 0) {
|
||||
const dir = tmpDirs.pop();
|
||||
if (dir) rmSync(dir, { recursive: true, force: true });
|
||||
|
|
@ -43,6 +47,9 @@ function makeProject() {
|
|||
const dir = mkdtempSync(join(tmpdir(), "sf-codex-catalog-"));
|
||||
tmpDirs.push(dir);
|
||||
mkdirSync(join(dir, ".sf"), { recursive: true });
|
||||
// Catalog is global (~/.sf/model-catalog/) — pin SF_HOME at the temp dir
|
||||
// so each test's catalog reads/writes land in isolation.
|
||||
process.env.SF_HOME = dir;
|
||||
return dir;
|
||||
}
|
||||
|
||||
|
|
@ -137,12 +144,7 @@ describe("refreshOpenaiCodexCatalog", () => {
|
|||
"gpt-5.2",
|
||||
]);
|
||||
|
||||
const cachePath = join(
|
||||
project,
|
||||
".sf",
|
||||
"model-catalog",
|
||||
"openai-codex.json",
|
||||
);
|
||||
const cachePath = join(project, "model-catalog", "openai-codex.json");
|
||||
const cache = JSON.parse(readFileSync(cachePath, "utf-8"));
|
||||
expect(cache.modelIds).toEqual(result);
|
||||
expect(typeof cache.fetchedAt).toBe("string");
|
||||
|
|
@ -180,12 +182,7 @@ describe("scheduleOpenaiCodexCatalogRefresh", () => {
|
|||
|
||||
scheduleOpenaiCodexCatalogRefresh(project);
|
||||
|
||||
const cachePath = join(
|
||||
project,
|
||||
".sf",
|
||||
"model-catalog",
|
||||
"openai-codex.json",
|
||||
);
|
||||
const cachePath = join(project, "model-catalog", "openai-codex.json");
|
||||
const cache = await waitForCacheWrite(cachePath);
|
||||
expect(cache.modelIds).toContain("gpt-5.5");
|
||||
});
|
||||
|
|
@ -195,9 +192,9 @@ describe("scheduleOpenaiCodexCatalogRefresh", () => {
|
|||
makeCodexHome(REAL_SHAPE_CACHE);
|
||||
// Pre-seed a fresh cache with a deliberately different model list
|
||||
// to confirm the next call does NOT overwrite it.
|
||||
mkdirSync(join(project, ".sf", "model-catalog"), { recursive: true });
|
||||
mkdirSync(join(project, "model-catalog"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(project, ".sf", "model-catalog", "openai-codex.json"),
|
||||
join(project, "model-catalog", "openai-codex.json"),
|
||||
JSON.stringify({
|
||||
fetchedAt: new Date().toISOString(),
|
||||
modelIds: ["sentinel-only"],
|
||||
|
|
@ -211,7 +208,7 @@ describe("scheduleOpenaiCodexCatalogRefresh", () => {
|
|||
|
||||
const cache = JSON.parse(
|
||||
readFileSync(
|
||||
join(project, ".sf", "model-catalog", "openai-codex.json"),
|
||||
join(project, "model-catalog", "openai-codex.json"),
|
||||
"utf-8",
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -476,12 +476,10 @@ describe("refreshSfManagedProviders — auth header and iteration", () => {
|
|||
const { refreshSfManagedProviders } = await import("../model-catalog-cache.js");
|
||||
const { writeFileSync, mkdirSync } = await import("node:fs");
|
||||
|
||||
// Pre-populate the runtime cache for opencode to simulate a cache hit.
|
||||
// sfRoot(tmp) → join(tmp, ".sf") when no .sf dir exists (probeSfRoot fallback).
|
||||
// cacheDirPath(tmp) → join(sfRuntimeRoot(tmp), "model-catalog")
|
||||
// → join(sfRoot(tmp), "model-catalog") (isRunningOnSelf=false for tmp)
|
||||
// → join(tmp, ".sf", "model-catalog")
|
||||
const cacheDir = join(tmp, ".sf", "model-catalog");
|
||||
// Pre-populate the global catalog for opencode to simulate a cache hit.
|
||||
// SF_HOME is pinned to `tmp` in beforeEach, so the global catalog dir
|
||||
// resolves to join(tmp, "model-catalog").
|
||||
const cacheDir = join(tmp, "model-catalog");
|
||||
mkdirSync(cacheDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(cacheDir, "opencode.json"),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue