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:
gsxdsm
2026-06-01 08:23:02 -07:00
parent b4230c06b1
commit f76716e55b
9 changed files with 167 additions and 46 deletions

View File

@@ -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();
});
});

View File

@@ -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);
});
});

View File

@@ -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();
});