fix(catalog): drop stale xAI Chat Completions model-cache rows

Invalidate cached paid-xAI ids on static fingerprint mismatch so the
Responses migration is not stuck behind a fresh completions cache overlay.
This commit is contained in:
Yang Yang
2026-08-02 21:23:06 -07:00
parent bd44ff190c
commit ef7759782d
3 changed files with 91 additions and 1 deletions
+4
View File
@@ -143,6 +143,10 @@
- Requested `reasoning.encrypted_content` on first-party xAI Responses calls (`xai` and `xai-oauth`) via the `include` parameter.
- Replayed xAI encrypted reasoning items on later Responses turns instead of stripping `type: "reasoning"` history.
### Fixed
- Invalidated stale paid-xAI model-cache rows written under Chat Completions so the Responses migration takes effect immediately instead of waiting for TTL expiry.
## [17.2.5] - 2026-08-03
### Fixed
@@ -1258,7 +1258,15 @@ export interface XaiModelManagerConfig {
}
export function xaiModelManagerOptions(config?: XaiModelManagerConfig): ModelManagerOptions<"openai-responses"> {
return createSimpleOpenAIResponsesOptions("xai", "https://api.x.ai/v1", config);
return {
...createSimpleOpenAIResponsesOptions("xai", "https://api.x.ai/v1", config),
// Completions → Responses migration: a fresh authoritative cache written
// by the old resolver stores `api: "openai-completions"` for these ids.
// Without a drop list, `online-if-uncached` skips the network and
// `mergeDynamicModel` lets the cached api win over the new static
// Responses entries until TTL expiry.
dropCachedModelIdsOnStaticMismatch: getBundledModels("xai").map(model => model.id),
};
}
export interface XaiOAuthModelManagerConfig {
@@ -1,7 +1,37 @@
import { describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { resolveProviderModels } from "@oh-my-pi/pi-catalog/model-manager";
import { getBundledModels } from "@oh-my-pi/pi-catalog/models";
import { CATALOG_PROVIDERS } from "@oh-my-pi/pi-catalog/provider-models/descriptors";
import { xaiModelManagerOptions } from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
import type { ModelSpec } from "@oh-my-pi/pi-catalog/types";
const XAI_RESPONSES_SPEC: ModelSpec<"openai-responses"> = {
id: "grok-4.5",
name: "Grok 4.5",
api: "openai-responses",
provider: "xai",
baseUrl: "https://api.x.ai/v1",
reasoning: true,
input: ["text", "image"],
cost: { input: 2, output: 6, cacheRead: 0.3, cacheWrite: 0 },
contextWindow: 500_000,
maxTokens: 500_000,
};
const XAI_COMPLETIONS_SPEC: ModelSpec<"openai-completions"> = {
id: "grok-4.5",
name: "Grok 4.5",
api: "openai-completions",
provider: "xai",
baseUrl: "https://api.x.ai/v1",
reasoning: true,
input: ["text", "image"],
cost: { input: 2, output: 6, cacheRead: 0.3, cacheWrite: 0 },
contextWindow: 500_000,
maxTokens: 500_000,
};
describe("paid xai (XAI_API_KEY) Responses contract", () => {
it("registers xai on the catalog Responses discovery path", () => {
@@ -12,6 +42,8 @@ describe("paid xai (XAI_API_KEY) Responses contract", () => {
const options = xaiModelManagerOptions({ apiKey: "test-key" });
expect(options.providerId).toBe("xai");
expect(options.fetchDynamicModels, "live /v1/models overlay").toBeTypeOf("function");
expect(options.dropCachedModelIdsOnStaticMismatch).toEqual(getBundledModels("xai").map(model => model.id));
expect(options.dropCachedModelIdsOnStaticMismatch).toContain("grok-4.5");
});
it("bundles every paid xai chat model on openai-responses", () => {
@@ -22,4 +54,50 @@ describe("paid xai (XAI_API_KEY) Responses contract", () => {
expect(model.baseUrl).toBe("https://api.x.ai/v1");
}
});
it("drops stale Chat Completions cache rows so Responses takes effect immediately", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-catalog-xai-completions-cache-"));
const dbPath = path.join(tempDir, "models.db");
try {
await resolveProviderModels(
{
providerId: "xai",
staticModels: [XAI_COMPLETIONS_SPEC],
fetchDynamicModels: async () => [XAI_COMPLETIONS_SPEC],
cacheDbPath: dbPath,
},
"online",
);
let fetches = 0;
const migrated = await resolveProviderModels(
{
...xaiModelManagerOptions(),
staticModels: [XAI_RESPONSES_SPEC],
cacheDbPath: dbPath,
fetchDynamicModels: async () => {
fetches += 1;
return [XAI_RESPONSES_SPEC];
},
},
"online-if-uncached",
);
expect(fetches).toBe(1);
expect(migrated.models.find(model => model.id === "grok-4.5")?.api).toBe("openai-responses");
const offline = await resolveProviderModels(
{
...xaiModelManagerOptions(),
staticModels: [XAI_RESPONSES_SPEC],
cacheDbPath: dbPath,
fetchDynamicModels: async () => null,
},
"offline",
);
expect(offline.models.find(model => model.id === "grok-4.5")?.api).toBe("openai-responses");
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
});