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) <noreply@anthropic.com>
This commit is contained in:
vamsi-ship-it
2026-06-21 13:52:47 +05:30
parent 2c1cff820b
commit 5a422b0c46
3 changed files with 30 additions and 2 deletions

View File

@@ -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.

View File

@@ -1363,6 +1363,14 @@ describe("createFnAgent", () => {
apiKey: "RESPONSES_API_KEY", apiKey: "RESPONSES_API_KEY",
models: [{ id: "responses-model", name: "Responses Model" }], 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); ] as any);
const { createFnAgent } = await import("../pi.js"); const { createFnAgent } = await import("../pi.js");
@@ -1387,6 +1395,15 @@ describe("createFnAgent", () => {
apiKey: "RESPONSES_API_KEY", apiKey: "RESPONSES_API_KEY",
models: [expect.objectContaining({ id: "responses-model", name: "Responses Model" })], 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 () => { it("avoids lock-based SettingsManager.create when loading extension providers", async () => {

View File

@@ -1006,9 +1006,13 @@ export interface AgentOptions {
permanentAgentGating?: PermanentAgentGatingContext; 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") { 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") { if (apiType === "openai-responses") {
return "openai-responses"; return "openai-responses";