Merge pull request #1702 from vamsi-ship-it/fix/anthropic-compatible-custom-provider-api-key

fix(engine): map anthropic-compatible custom providers to anthropic-messages api
This commit is contained in:
gsxdsm
2026-06-21 02:23:11 -07:00
committed by GitHub
3 changed files with 65 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",
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,40 @@ describe("createFnAgent", () => {
apiKey: "RESPONSES_API_KEY",
models: [expect.objectContaining({ id: "responses-model", name: "Responses Model" })],
}));
/*
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 () => {

View File

@@ -1006,9 +1006,23 @@ export interface AgentOptions {
permanentAgentGating?: PermanentAgentGatingContext;
}
function resolveCustomProviderApiType(apiType: string): "anthropic" | "openai-responses" | "openai-completions" {
/**
* 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") {
return "anthropic";
return "anthropic-messages";
}
if (apiType === "openai-responses") {
return "openai-responses";