From 7fa5029ef804367554d62d5cdbd0ef2b40a55964 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 15 Aug 2026 14:48:07 -0700 Subject: [PATCH] fix: route Anthropic subscription instance login through upstream anthropic provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Authentication cards pass an explicit credential-instance id, which sent dashboard subscription logins through loginInstance. That seam mapped the card to the anthropic-subscription storage row id and passed it verbatim to ModelRuntime.login, which pi rejects with 'Unknown provider: anthropic-subscription' (GitHub #3462) — every subscription login failed while the credential path itself was healthy. loginInstance now reuses the Anthropic-aware login seam (upstream login as 'anthropic', credential relocated to the anthropic-subscription row) with a regression test. Co-Authored-By: Claude Fable 5 --- ...x-anthropic-subscription-instance-login.md | 7 +++ .../__tests__/provider-auth-instances.test.ts | 33 +++++++++++++ packages/engine/src/auth/provider-auth.ts | 47 +++++++++++++------ 3 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 .changeset/fix-anthropic-subscription-instance-login.md diff --git a/.changeset/fix-anthropic-subscription-instance-login.md b/.changeset/fix-anthropic-subscription-instance-login.md new file mode 100644 index 0000000000..847ed27edf --- /dev/null +++ b/.changeset/fix-anthropic-subscription-instance-login.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix Anthropic Subscription login failing with "Unknown provider: anthropic-subscription". +category: fix +dev: Instance-scoped OAuth login (`loginInstance`) now reuses the Anthropic-aware login seam, logging in upstream as `anthropic` and persisting to the `anthropic-subscription` storage row, instead of passing the storage-only id to `ModelRuntime.login` (GitHub #3462). diff --git a/packages/engine/src/__tests__/provider-auth-instances.test.ts b/packages/engine/src/__tests__/provider-auth-instances.test.ts index c85a0c3dc4..7f4bed09c8 100644 --- a/packages/engine/src/__tests__/provider-auth-instances.test.ts +++ b/packages/engine/src/__tests__/provider-auth-instances.test.ts @@ -62,4 +62,37 @@ describe("DashboardAuthStorage instance facade", () => { expect(credentials.has("default")).toBe(false); expect(defaultId).toBe("acct-first"); }); + + /* + FNXC:ProviderAuth 2026-08-15-21:46: + Regression for GitHub #3462: instance-scoped Anthropic subscription login must reach the + upstream runtime as `anthropic` and persist the OAuth result under the `anthropic-subscription` + storage row. Passing the storage row id to the runtime login fails with + `Unknown provider: anthropic-subscription` because pi never registers that id as a provider. + */ + it("routes Anthropic subscription instance login through the upstream anthropic provider", async () => { + const rows = new Map(); + const storage = { + ...storageFixture(), + login: vi.fn(async (providerId: string) => { + rows.set(providerId, { type: "oauth", expires: Date.now() + 60_000 }); + }), + get: vi.fn((providerId: string) => rows.get(providerId)), + set: vi.fn(async (providerId: string, credential: { type: string }) => { + rows.set(providerId, credential as never); + }), + remove: vi.fn(async (providerId: string) => { rows.delete(providerId); }), + getDefaultInstance: vi.fn(() => undefined), + getInstance: vi.fn(() => undefined), + setInstance: vi.fn(), + }; + const facade = wrapAuthStorageWithApiKeyProviders(storage as unknown as FusionAuthStorage, {} as ModelRegistry); + await facade.loginInstance?.({ providerId: "anthropic", instanceId: "default" }, {} as never); + expect(storage.login).toHaveBeenCalledTimes(1); + expect(storage.login.mock.calls[0]?.[0]).toBe("anthropic"); + expect(storage.setInstance).toHaveBeenCalledWith( + { providerId: "anthropic-subscription", instanceId: "default" }, + expect.objectContaining({ type: "oauth" }), + ); + }); }); diff --git a/packages/engine/src/auth/provider-auth.ts b/packages/engine/src/auth/provider-auth.ts index 5735b417ef..08c3496642 100644 --- a/packages/engine/src/auth/provider-auth.ts +++ b/packages/engine/src/auth/provider-auth.ts @@ -142,18 +142,16 @@ export function wrapAuthStorageWithApiKeyProviders( return legacySubscription; }; - return { - reload: () => mergedAuthStorage.reload(), - getOAuthProviders: () => - mergedAuthStorage - .getOAuthProviders() - .map((provider) => provider.id === ANTHROPIC_STORAGE_PROVIDER_ID - ? ({ id: ANTHROPIC_STORAGE_PROVIDER_ID, name: "Anthropic Subscription" }) - : ({ id: provider.id, name: provider.name })), - hasAuth: (provider) => provider === ANTHROPIC_STORAGE_PROVIDER_ID || provider === ANTHROPIC_SUBSCRIPTION_STORAGE_PROVIDER_ID - ? Boolean(getAnthropicSubscriptionCredential()) - : mergedAuthStorage.hasAuth(provider), - login: async (providerId, callbacks) => { + /* + FNXC:ProviderAuth 2026-08-15-21:46: + This is the ONLY seam allowed to start an OAuth login: it maps the split Anthropic ids + (`anthropic` / `anthropic-subscription`) onto the upstream `anthropic` runtime login before + relocating the credential to the subscription storage row. Instance-scoped login must reuse it — + calling `mergedAuthStorage.login("anthropic-subscription", ...)` directly reaches + `ModelRuntime.login` with a storage-only id pi does not register and fails with + `Unknown provider: anthropic-subscription` (GitHub #3462). + */ + const login = async (providerId: string, callbacks: LoginCallbacks): Promise => { if (providerId !== ANTHROPIC_STORAGE_PROVIDER_ID && providerId !== ANTHROPIC_SUBSCRIPTION_STORAGE_PROVIDER_ID) { await mergedAuthStorage.login( providerId, @@ -181,7 +179,20 @@ export function wrapAuthStorageWithApiKeyProviders( await authStorage.remove(ANTHROPIC_STORAGE_PROVIDER_ID); } } - }, + }; + + return { + reload: () => mergedAuthStorage.reload(), + getOAuthProviders: () => + mergedAuthStorage + .getOAuthProviders() + .map((provider) => provider.id === ANTHROPIC_STORAGE_PROVIDER_ID + ? ({ id: ANTHROPIC_STORAGE_PROVIDER_ID, name: "Anthropic Subscription" }) + : ({ id: provider.id, name: provider.name })), + hasAuth: (provider) => provider === ANTHROPIC_STORAGE_PROVIDER_ID || provider === ANTHROPIC_SUBSCRIPTION_STORAGE_PROVIDER_ID + ? Boolean(getAnthropicSubscriptionCredential()) + : mergedAuthStorage.hasAuth(provider), + login, logout: async (provider) => { if (provider !== ANTHROPIC_STORAGE_PROVIDER_ID && provider !== ANTHROPIC_SUBSCRIPTION_STORAGE_PROVIDER_ID) { await mergedAuthStorage.logout(provider); @@ -294,8 +305,16 @@ export function wrapAuthStorageWithApiKeyProviders( The runtime OAuth adapter only accepts a bare provider and therefore writes its resolved default slot. Capture and restore that slot around the login before persisting the result to the requested instance, so adding or reauthorizing an account never repoints its credential. + + FNXC:ProviderAuth 2026-08-15-21:46: + Instance login must go through the Anthropic-aware `login` seam above, never raw + `mergedAuthStorage.login(providerId, ...)`: for the subscription card `providerId` here is the + storage row id `anthropic-subscription`, which pi's ModelRuntime does not register as a + provider, so the raw call failed every dashboard subscription login with + `Unknown provider: anthropic-subscription` (GitHub #3462) once the Authentication cards began + passing an explicit credential-instance id. */ - await mergedAuthStorage.login(providerId, callbacks); + await login(providerId, callbacks); const credential = mergedAuthStorage.get(providerId); if (!credential) return; await mergedAuthStorage.setInstance(target, { ...credential, ...(label ? { label } : {}) });