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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-08 13:06:37 -07:00
parent 6a81871781
commit 64d4bb753a
4 changed files with 59 additions and 4 deletions

View File

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

View File

@@ -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);
});

View File

@@ -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();

View File

@@ -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";