FN-5833: fix custom Responses API model lookup
Ensure custom OpenAI Responses providers resolve configured models without requiring registry entries. - update global settings model resolution to preserve custom provider model configuration - adjust engine custom-provider and fn-agent creation paths to avoid false 'model not found' failures - add regression tests for OpenAI Responses custom providers and pi create-fn-agent flows - document the custom provider model behavior and add a changeset for @runfusion/fusion Files changed: .../FN-5833-custom-provider-responses-fix.md | 10 ++++ docs/settings-reference.md | 2 +- packages/core/src/global-settings.ts | 56 +++++++++---------- packages/core/src/index.ts | 2 +- .../custom-providers-openai-responses.test.ts | 64 ++++++++++++++++++++++ .../engine/src/__tests__/custom-providers.test.ts | 33 ++++++++++- .../src/__tests__/pi-create-fn-agent.test.ts | 11 +++- packages/engine/src/custom-providers.ts | 4 +- packages/engine/src/pi.ts | 21 +++++-- 9 files changed, 162 insertions(+), 41 deletions(-) Fusion-Task-Id: FN-5833 Fusion-Task-Lineage: a59481cf-faf5-42b8-97ce-7b3eca27b4fb
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
||||
import { customProviderRegistryKey, type CustomProvider } from "@fusion/core";
|
||||
import { readCustomProviders } from "../custom-providers.js";
|
||||
|
||||
describe("custom providers openai-responses regression", () => {
|
||||
let homeDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
homeDir = await mkdtemp(join(tmpdir(), "fn-custom-providers-responses-"));
|
||||
await mkdir(join(homeDir, ".pi", "fusion"), { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(homeDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("loads openai-responses providers from active global settings path and resolves model", async () => {
|
||||
const providers: CustomProvider[] = [
|
||||
{
|
||||
id: "550e8400-e29b-41d4-a716-446655440002",
|
||||
name: "MyAPI",
|
||||
apiType: "openai-responses",
|
||||
baseUrl: "https://responses.example.test/v1",
|
||||
apiKey: "RESPONSES_KEY",
|
||||
models: [{ id: "gpt-5.4", name: "GPT 5.4" }],
|
||||
},
|
||||
];
|
||||
|
||||
await writeFile(
|
||||
join(homeDir, ".pi", "fusion", "settings.json"),
|
||||
JSON.stringify({ customProviders: providers }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const loadedProviders = readCustomProviders(homeDir);
|
||||
expect(loadedProviders).toEqual(providers);
|
||||
|
||||
const authStorage = AuthStorage.inMemory();
|
||||
const modelRegistry = ModelRegistry.inMemory(authStorage);
|
||||
|
||||
const provider = loadedProviders[0]!;
|
||||
modelRegistry.registerProvider(customProviderRegistryKey(provider, loadedProviders), {
|
||||
baseUrl: provider.baseUrl,
|
||||
api: "openai-responses",
|
||||
apiKey: provider.apiKey,
|
||||
models: [{
|
||||
id: "gpt-5.4",
|
||||
name: "GPT 5.4",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 16384,
|
||||
}],
|
||||
});
|
||||
modelRegistry.refresh();
|
||||
|
||||
expect(modelRegistry.find("myapi", "gpt-5.4")).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,6 @@ describe("readCustomProviders", () => {
|
||||
beforeEach(async () => {
|
||||
homeDir = await mkdtemp(join(tmpdir(), "fn-custom-providers-home-"));
|
||||
settingsPath = join(homeDir, ".fusion", "settings.json");
|
||||
await mkdir(join(homeDir, ".fusion"), { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -21,6 +20,7 @@ describe("readCustomProviders", () => {
|
||||
it("returns an empty list when settings are missing or malformed", async () => {
|
||||
expect(readCustomProviders(homeDir)).toEqual([]);
|
||||
|
||||
await mkdir(join(homeDir, ".fusion"), { recursive: true });
|
||||
await writeFile(settingsPath, "{ invalid json", "utf-8");
|
||||
expect(readCustomProviders(homeDir)).toEqual([]);
|
||||
|
||||
@@ -49,6 +49,7 @@ describe("readCustomProviders", () => {
|
||||
baseUrl: "https://anthropic.example.test",
|
||||
},
|
||||
];
|
||||
await mkdir(join(homeDir, ".fusion"), { recursive: true });
|
||||
await writeFile(
|
||||
settingsPath,
|
||||
JSON.stringify({ customProviders: providers }),
|
||||
@@ -57,4 +58,34 @@ describe("readCustomProviders", () => {
|
||||
|
||||
expect(readCustomProviders(homeDir)).toEqual(providers);
|
||||
});
|
||||
|
||||
it("reads from legacy ~/.pi/fusion when ~/.fusion does not exist", async () => {
|
||||
const providers = [{
|
||||
id: "legacy-provider",
|
||||
name: "Legacy Provider",
|
||||
apiType: "openai-responses",
|
||||
baseUrl: "https://legacy.example.test/v1",
|
||||
models: [{ id: "gpt-legacy", name: "GPT Legacy" }],
|
||||
}];
|
||||
const legacyPath = join(homeDir, ".pi", "fusion", "settings.json");
|
||||
await mkdir(join(homeDir, ".pi", "fusion"), { recursive: true });
|
||||
await writeFile(legacyPath, JSON.stringify({ customProviders: providers }), "utf-8");
|
||||
|
||||
expect(readCustomProviders(homeDir)).toEqual(providers);
|
||||
});
|
||||
|
||||
it("reads from legacy ~/.pi/kb when newer settings dirs do not exist", async () => {
|
||||
const providers = [{
|
||||
id: "legacy-original-provider",
|
||||
name: "Legacy Original Provider",
|
||||
apiType: "openai-compatible",
|
||||
baseUrl: "https://legacy-original.example.test/v1",
|
||||
models: [{ id: "gpt-original", name: "GPT Original" }],
|
||||
}];
|
||||
const legacyPath = join(homeDir, ".pi", "kb", "settings.json");
|
||||
await mkdir(join(homeDir, ".pi", "kb"), { recursive: true });
|
||||
await writeFile(legacyPath, JSON.stringify({ customProviders: providers }), "utf-8");
|
||||
|
||||
expect(readCustomProviders(homeDir)).toEqual(providers);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1342,7 +1342,14 @@ describe("createFnAgent", () => {
|
||||
tools: "readonly",
|
||||
defaultProvider: "zai",
|
||||
defaultModelId: "glm-5.1",
|
||||
})).rejects.toThrow("Configured primary model zai/glm-5.1 was not found");
|
||||
})).rejects.toThrow("Configured model zai/glm-5.1 (primary selection) was not found in the pi model registry");
|
||||
await expect(createFnAgent({
|
||||
cwd: "/tmp",
|
||||
systemPrompt: "test",
|
||||
tools: "readonly",
|
||||
defaultProvider: "zai",
|
||||
defaultModelId: "glm-5.1",
|
||||
})).rejects.toThrow("Settings → Custom Providers");
|
||||
|
||||
expect(createAgentSessionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1386,7 +1393,7 @@ describe("createFnAgent", () => {
|
||||
defaultModelId: "gpt-5.4",
|
||||
fallbackProvider: "openai-codex",
|
||||
fallbackModelId: "missing-model",
|
||||
})).rejects.toThrow("Configured fallback model openai-codex/missing-model was not found");
|
||||
})).rejects.toThrow("Configured model openai-codex/missing-model (fallback selection) was not found in the pi model registry");
|
||||
|
||||
expect(createAgentSessionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user