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:
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user