diff --git a/.changeset/fix-anthropic-compatible-custom-provider.md b/.changeset/fix-anthropic-compatible-custom-provider.md new file mode 100644 index 0000000000..227f5ac008 --- /dev/null +++ b/.changeset/fix-anthropic-compatible-custom-provider.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +Fix anthropic-compatible custom providers failing with "No API provider registered for api: anthropic". + +`resolveCustomProviderApiType` mapped the `anthropic-compatible` provider type to the api key `"anthropic"`, but pi-ai registers the Anthropic Messages API under `"anthropic-messages"`. Any custom provider configured as `anthropic-compatible` (self-hosted Claude proxy, gateway, etc.) therefore selected a model whose `api` did not match a registered provider and threw at stream time. Mapped it to `"anthropic-messages"` and added a regression assertion alongside the existing openai-compatible / openai-responses coverage. diff --git a/packages/engine/src/__tests__/pi-create-fn-agent.test.ts b/packages/engine/src/__tests__/pi-create-fn-agent.test.ts index 321358c385..05c8c5e68d 100644 --- a/packages/engine/src/__tests__/pi-create-fn-agent.test.ts +++ b/packages/engine/src/__tests__/pi-create-fn-agent.test.ts @@ -1363,6 +1363,14 @@ describe("createFnAgent", () => { apiKey: "RESPONSES_API_KEY", models: [{ id: "responses-model", name: "Responses Model" }], }, + { + id: "770e8400-e29b-41d4-a716-446655440002", + name: "Custom Anthropic", + apiType: "anthropic-compatible", + baseUrl: "https://anthropic.example", + apiKey: "ANTHROPIC_API_KEY", + models: [{ id: "anthropic-model", name: "Anthropic Model" }], + }, ] as any); const { createFnAgent } = await import("../pi.js"); @@ -1387,6 +1395,15 @@ describe("createFnAgent", () => { apiKey: "RESPONSES_API_KEY", models: [expect.objectContaining({ id: "responses-model", name: "Responses Model" })], })); + // anthropic-compatible must map to pi-ai's registered "anthropic-messages" + // api key, NOT bare "anthropic" (which throws "No API provider registered + // for api: anthropic" at stream time). Regression guard for that bug. + expect(registerProviderMock).toHaveBeenCalledWith("custom-anthropic", expect.objectContaining({ + baseUrl: "https://anthropic.example", + api: "anthropic-messages", + apiKey: "ANTHROPIC_API_KEY", + models: [expect.objectContaining({ id: "anthropic-model", name: "Anthropic Model" })], + })); }); it("avoids lock-based SettingsManager.create when loading extension providers", async () => { diff --git a/packages/engine/src/pi.ts b/packages/engine/src/pi.ts index 84ee5cc048..84dd590f0f 100644 --- a/packages/engine/src/pi.ts +++ b/packages/engine/src/pi.ts @@ -1006,9 +1006,13 @@ export interface AgentOptions { permanentAgentGating?: PermanentAgentGatingContext; } -function resolveCustomProviderApiType(apiType: string): "anthropic" | "openai-responses" | "openai-completions" { +function resolveCustomProviderApiType(apiType: string): "anthropic-messages" | "openai-responses" | "openai-completions" { if (apiType === "anthropic-compatible") { - return "anthropic"; + // pi-ai registers the Anthropic Messages API under the key + // "anthropic-messages" (see @earendil-works/pi-ai register-builtins). + // Returning bare "anthropic" throws "No API provider registered for + // api: anthropic" at stream time, so map to the real registry key. + return "anthropic-messages"; } if (apiType === "openai-responses") { return "openai-responses";