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:
7
.changeset/fn-7690-apitype-resolver-reconcile.md
Normal file
7
.changeset/fn-7690-apitype-resolver-reconcile.md
Normal 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).
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user