From 64d4bb753adfb5c61a28ba59ccf1354b0e4c612b Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 8 Jul 2026 13:06:37 -0700 Subject: [PATCH] FN-7690: fix custom-provider anthropic-compatible apiType resolution Reconciles a naming drift where resolveApiType() mapped anthropic-compatible custom providers to an unregistered pi-ai api key, causing streaming failures. - resolveApiType() now maps anthropic-compatible to "anthropic-messages" (was "anthropic"), matching pi.ts's resolveCustomProviderApiType and the built-in Anthropic provider config - Added FNXC:CustomProviders comment documenting why anthropic-messages is the only key pi-ai's ModelRegistry actually registers - Added/updated regression tests in custom-provider-registry.test.ts and provider-registration.test.ts - Added changeset (patch) documenting the fix Files changed: .changeset/fn-7690-apitype-resolver-reconcile.md | 7 +++++++ packages/cli/src/commands/__tests__/custom-provider-registry.test.ts | 21 ++++++++++++++++++--- packages/engine/src/__tests__/provider-registration.test.ts | 20 ++++++++++++++++++++ packages/engine/src/custom-provider-registry.ts | 15 ++++++++++++++- 4 files changed, 59 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-7690 Fusion-Task-Lineage: 461c340f-bc29-44a2-b8ec-19f0b03224d7 Co-authored-by: Fusion (runfusion.ai) --- .../fn-7690-apitype-resolver-reconcile.md | 7 +++++++ .../custom-provider-registry.test.ts | 21 ++++++++++++++++--- .../__tests__/provider-registration.test.ts | 20 ++++++++++++++++++ .../engine/src/custom-provider-registry.ts | 15 ++++++++++++- 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 .changeset/fn-7690-apitype-resolver-reconcile.md diff --git a/.changeset/fn-7690-apitype-resolver-reconcile.md b/.changeset/fn-7690-apitype-resolver-reconcile.md new file mode 100644 index 0000000000..62512e2709 --- /dev/null +++ b/.changeset/fn-7690-apitype-resolver-reconcile.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix Anthropic-compatible custom providers registering under an unregistered API key. +category: fix +dev: Aligns custom-provider-registry `resolveApiType("anthropic-compatible")` on "anthropic-messages" (the registered pi-ai api key), matching pi.ts `resolveCustomProviderApiType` and removing the latent "No API provider registered for api: anthropic" drift (FN-7690). diff --git a/packages/cli/src/commands/__tests__/custom-provider-registry.test.ts b/packages/cli/src/commands/__tests__/custom-provider-registry.test.ts index 8336948543..68668798de 100644 --- a/packages/cli/src/commands/__tests__/custom-provider-registry.test.ts +++ b/packages/cli/src/commands/__tests__/custom-provider-registry.test.ts @@ -9,12 +9,27 @@ import { describe("custom-provider-registry", () => { it.each([ ["openai-compatible", "openai-completions"], - ["anthropic-compatible", "anthropic"], + ["anthropic-compatible", "anthropic-messages"], ["openai-responses", "openai-responses"], ])("resolveApiType maps %s -> %s", (apiType, expectedApi) => { expect(resolveApiType(apiType)).toBe(expectedApi); }); + // FN-7690: resolveApiType() (this module) and resolveCustomProviderApiType() + // (packages/engine/src/pi.ts, module-private) must agree on the pi-ai api key + // for every apiType input, or the registration path and the streaming path + // register/consume different (and possibly unregistered) api keys. pi.ts's + // resolver is not importable here, so we pin resolveApiType's outputs against + // the literal keys pi.ts is known (and tested) to return. + it.each([ + ["openai-compatible", "openai-completions"], + ["anthropic-compatible", "anthropic-messages"], + ["openai-responses", "openai-responses"], + ["unknown-type", "openai-completions"], + ])("resolveApiType(%s) matches pi.ts resolveCustomProviderApiType's expected key (%s)", (apiType, expectedApi) => { + expect(resolveApiType(apiType)).toBe(expectedApi); + }); + it("registers providers with expected config shape", () => { const registerProvider = vi.fn(); const refresh = vi.fn(); @@ -48,7 +63,7 @@ describe("custom-provider-registry", () => { })); expect(registerProvider).toHaveBeenNthCalledWith(2, "anthropic-custom", expect.objectContaining({ baseUrl: "https://anthropic.test", - api: "anthropic", + api: "anthropic-messages", apiKey: "ANTHROPIC_KEY", models: [expect.objectContaining({ id: "claude-x", name: "Claude X" })], })); @@ -161,7 +176,7 @@ describe("custom-provider-registry", () => { ); expect(registerProvider).toHaveBeenCalledTimes(1); - expect(registerProvider).toHaveBeenCalledWith("new", expect.objectContaining({ api: "anthropic" })); + expect(registerProvider).toHaveBeenCalledWith("new", expect.objectContaining({ api: "anthropic-messages" })); expect(refresh).toHaveBeenCalledTimes(1); }); diff --git a/packages/engine/src/__tests__/provider-registration.test.ts b/packages/engine/src/__tests__/provider-registration.test.ts index 53b178f40c..aa6c9e9791 100644 --- a/packages/engine/src/__tests__/provider-registration.test.ts +++ b/packages/engine/src/__tests__/provider-registration.test.ts @@ -144,6 +144,26 @@ describe("seedDashboardProviders", () => { expect(providerIds).toEqual(expect.arrayContaining(["zai", "openrouter", "acme-one", "acme-two"])); }); + it("registers an anthropic-compatible custom provider under the anthropic-messages api key (FN-7690)", async () => { + const store = makeStore([ + customProvider({ + id: "anthropic-id", + name: "Anthropic Custom", + apiType: "anthropic-compatible", + baseUrl: "https://anthropic.test", + }), + ]); + const authStorage = makeAuthStorage(); + const modelRegistry = makeModelRegistry(); + + await seedDashboardProviders({ store, authStorage, modelRegistry }); + + expect(modelRegistry.registerProvider).toHaveBeenCalledWith( + "anthropic-custom", + expect.objectContaining({ api: "anthropic-messages" }), + ); + }); + it("does not abort startup when reading custom providers from global settings fails", async () => { const authStorage = makeAuthStorage(); const modelRegistry = makeModelRegistry(); diff --git a/packages/engine/src/custom-provider-registry.ts b/packages/engine/src/custom-provider-registry.ts index cb9a84682a..1543996d86 100644 --- a/packages/engine/src/custom-provider-registry.ts +++ b/packages/engine/src/custom-provider-registry.ts @@ -28,9 +28,22 @@ interface ModelRegistryLike { refresh: () => void; } +/* +FNXC:CustomProviders 2026-07-08-00:00: +FN-7690: resolveApiType() and pi.ts's resolveCustomProviderApiType() both translate a +custom provider's declared apiType into the api key handed to pi-ai's +ModelRegistry.registerProvider({ api }). Both call sites register into a REAL pi-ai +ModelRegistry (registerCustomProviders/reregisterCustomProviders below feed +seedDashboardProviders, used by desktop + CLI serve/dashboard/daemon), so every arm here +must return a key pi-ai's api-registry actually registers. `anthropic-compatible` resolves +to "anthropic-messages" — matching pi.ts's resolveCustomProviderApiType and the built-in +Anthropic provider config (packages/core/src/anthropic-models.ts, api: "anthropic-messages"). +The bare "anthropic" key is never registered and throws "No API provider registered for +api: anthropic" the moment a task streams against it. +*/ export function resolveApiType(apiType: string): string { if (apiType === "anthropic-compatible") { - return "anthropic"; + return "anthropic-messages"; } if (apiType === "openai-responses") { return "openai-responses";