From 5a422b0c464819a37f5a77046934d2dcd88056a0 Mon Sep 17 00:00:00 2001 From: vamsi-ship-it Date: Sun, 21 Jun 2026 13:52:47 +0530 Subject: [PATCH 1/2] fix(engine): map anthropic-compatible custom providers to anthropic-messages api resolveCustomProviderApiType mapped the `anthropic-compatible` provider type to the api key "anthropic", but pi-ai (@earendil-works/pi-ai) registers the Anthropic Messages API under "anthropic-messages". Any custom provider configured as anthropic-compatible selected a model whose `api` did not match a registered provider, throwing "No API provider registered for api: anthropic" at stream time (the model registered fine, but failed when a task tried to stream). The openai-responses and default (openai-completions) arms already map to real registry keys and work; only the anthropic arm pointed at an unregistered key. Extend the existing custom-provider registration test (which covered openai-compatible and openai-responses but not anthropic-compatible) with a regression assertion that anthropic-compatible maps to "anthropic-messages". Co-Authored-By: Claude Opus 4.8 (1M context) --- .../fix-anthropic-compatible-custom-provider.md | 7 +++++++ .../src/__tests__/pi-create-fn-agent.test.ts | 17 +++++++++++++++++ packages/engine/src/pi.ts | 8 ++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-anthropic-compatible-custom-provider.md 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"; From efd3743624b5446d86859ed790c691566386a113 Mon Sep 17 00:00:00 2001 From: vamsi-ship-it Date: Sun, 21 Jun 2026 14:30:04 +0530 Subject: [PATCH 2/2] fix(review): address CodeRabbit feedback on anthropic-compatible fix - Add JSDoc to resolveCustomProviderApiType (docstring coverage + AGENTS.md jsdoc convention) and convert the inline rationale to FNXC format. - FNXC-format the test rationale comment. - Strengthen the regression test per FN-5893 (fix the invariant, not the repro): - negative assertion that no provider is ever registered with the bare "anthropic" api key; - assert every api key passed to registerProvider is one pi-ai's registry actually registers, so a typo in any arm (not just anthropic) fails here. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/__tests__/pi-create-fn-agent.test.ts | 31 +++++++++++++++++-- packages/engine/src/pi.ts | 18 ++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) 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 05c8c5e68d..2d2b169ba6 100644 --- a/packages/engine/src/__tests__/pi-create-fn-agent.test.ts +++ b/packages/engine/src/__tests__/pi-create-fn-agent.test.ts @@ -1395,15 +1395,40 @@ 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. + /* + FNXC:CustomProviders 2026-06-21-13:45: + Invariant (FN-5893 surface = providers/execution paths): every custom-provider apiType must map to an api key pi-ai's registry actually registers. anthropic-compatible maps to "anthropic-messages", NOT bare "anthropic" — the latter registered fine but threw "No API provider registered for api: anthropic" the moment a task streamed. Assert the corrected value AND that the broken bare key is never used, so a future regression to "anthropic" fails here. + */ 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" })], })); + // Negative guard: the unregistered bare "anthropic" api key must never be emitted for any provider. + expect(registerProviderMock).not.toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ api: "anthropic" }), + ); + // Invariant: every api key handed to registerProvider must be one pi-ai registers + // (mirrors @earendil-works/pi-ai register-builtins). Catches a typo in any arm. + const PI_AI_REGISTERED_APIS = new Set([ + "anthropic-messages", + "openai-completions", + "openai-responses", + "azure-openai-responses", + "openai-codex-responses", + "google-generative-ai", + "google-vertex", + "mistral-conversations", + "bedrock-converse-stream", + ]); + for (const [, config] of registerProviderMock.mock.calls) { + const api = (config as { api?: string } | undefined)?.api; + if (typeof api === "string") { + expect(PI_AI_REGISTERED_APIS.has(api)).toBe(true); + } + } }); 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 84dd590f0f..84c2bdac7e 100644 --- a/packages/engine/src/pi.ts +++ b/packages/engine/src/pi.ts @@ -1006,12 +1006,22 @@ export interface AgentOptions { permanentAgentGating?: PermanentAgentGatingContext; } +/** + * Map a user-facing custom-provider `apiType` to the pi-ai api-registry key. + * + * FNXC:CustomProviders 2026-06-21-13:45: + * Every arm must return a key that pi-ai's api-registry actually registers + * (see @earendil-works/pi-ai register-builtins). `anthropic-compatible` resolves + * to "anthropic-messages" — the key the Anthropic Messages API is registered + * under. The bare "anthropic" key is never registered, so returning it let a + * provider register but threw "No API provider registered for api: anthropic" + * the moment a task tried to stream. + * + * @param apiType - the custom provider's declared compatibility type. + * @returns the registered pi-ai api key to stream against. + */ function resolveCustomProviderApiType(apiType: string): "anthropic-messages" | "openai-responses" | "openai-completions" { if (apiType === "anthropic-compatible") { - // 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") {