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", () => {
|
describe("custom-provider-registry", () => {
|
||||||
it.each([
|
it.each([
|
||||||
["openai-compatible", "openai-completions"],
|
["openai-compatible", "openai-completions"],
|
||||||
["anthropic-compatible", "anthropic"],
|
["anthropic-compatible", "anthropic-messages"],
|
||||||
["openai-responses", "openai-responses"],
|
["openai-responses", "openai-responses"],
|
||||||
])("resolveApiType maps %s -> %s", (apiType, expectedApi) => {
|
])("resolveApiType maps %s -> %s", (apiType, expectedApi) => {
|
||||||
expect(resolveApiType(apiType)).toBe(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", () => {
|
it("registers providers with expected config shape", () => {
|
||||||
const registerProvider = vi.fn();
|
const registerProvider = vi.fn();
|
||||||
const refresh = vi.fn();
|
const refresh = vi.fn();
|
||||||
@@ -48,7 +63,7 @@ describe("custom-provider-registry", () => {
|
|||||||
}));
|
}));
|
||||||
expect(registerProvider).toHaveBeenNthCalledWith(2, "anthropic-custom", expect.objectContaining({
|
expect(registerProvider).toHaveBeenNthCalledWith(2, "anthropic-custom", expect.objectContaining({
|
||||||
baseUrl: "https://anthropic.test",
|
baseUrl: "https://anthropic.test",
|
||||||
api: "anthropic",
|
api: "anthropic-messages",
|
||||||
apiKey: "ANTHROPIC_KEY",
|
apiKey: "ANTHROPIC_KEY",
|
||||||
models: [expect.objectContaining({ id: "claude-x", name: "Claude X" })],
|
models: [expect.objectContaining({ id: "claude-x", name: "Claude X" })],
|
||||||
}));
|
}));
|
||||||
@@ -161,7 +176,7 @@ describe("custom-provider-registry", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(registerProvider).toHaveBeenCalledTimes(1);
|
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);
|
expect(refresh).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,26 @@ describe("seedDashboardProviders", () => {
|
|||||||
expect(providerIds).toEqual(expect.arrayContaining(["zai", "openrouter", "acme-one", "acme-two"]));
|
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 () => {
|
it("does not abort startup when reading custom providers from global settings fails", async () => {
|
||||||
const authStorage = makeAuthStorage();
|
const authStorage = makeAuthStorage();
|
||||||
const modelRegistry = makeModelRegistry();
|
const modelRegistry = makeModelRegistry();
|
||||||
|
|||||||
@@ -28,9 +28,22 @@ interface ModelRegistryLike {
|
|||||||
refresh: () => void;
|
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 {
|
export function resolveApiType(apiType: string): string {
|
||||||
if (apiType === "anthropic-compatible") {
|
if (apiType === "anthropic-compatible") {
|
||||||
return "anthropic";
|
return "anthropic-messages";
|
||||||
}
|
}
|
||||||
if (apiType === "openai-responses") {
|
if (apiType === "openai-responses") {
|
||||||
return "openai-responses";
|
return "openai-responses";
|
||||||
|
|||||||
Reference in New Issue
Block a user