fix: resolve Anthropic subscription auth through getApiKey for pi ModelRuntime.getAuth
pi >=0.80.8 moved session request auth to ModelRuntime.getAuth -> pi-ai
resolveProviderAuth, which reads credentials.read("anthropic") and refreshes an
OAuth credential via credentials.modify("anthropic"). Fusion stores the
subscription login under `anthropic-subscription` with no raw `anthropic` row,
so the refresh callback saw current===undefined, bailed, and auth resolved to
undefined -> "Provider is not configured: anthropic" (then fell back).
Resolve read("anthropic") through fusion's getApiKey (refresh + raw/legacy/
subscription/fallback precedence) and hand pi-ai a ready api_key credential;
pi-ai routes it as OAuth by the sk-ant-oat token prefix. Supersedes the prior
read-alias, which fixed lookup but not the broken OAuth refresh-via-modify path.
Verified end-to-end: ModelRuntime.getAuth(anthropic/opus) now resolves the
subscription token instead of returning undefined.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,4 +4,4 @@
|
||||
|
||||
summary: Fix Anthropic subscription logins failing tasks with "Provider is not configured: anthropic".
|
||||
category: fix
|
||||
dev: pi-ai >=0.80 resolves provider auth via `credentials.read(provider.id)` instead of `getApiKey()`, bypassing fusion's `anthropic-subscription` -> `anthropic` alias; alias it at the credential-store `read()` layer (`createFusionCredentialStore`). Also add "not configured" to `isRetryableModelSelectionError` so an unresolved provider triggers the configured fallback model instead of hard-failing.
|
||||
dev: pi >=0.80.8 moved session auth from `ModelRegistry.getApiKeyAndHeaders` (fusion's `getApiKey`) to `ModelRuntime.getAuth` -> pi-ai `resolveProviderAuth`, which reads `credentials.read("anthropic")` and refreshes OAuth via `credentials.modify("anthropic")`. Fusion stores the subscription login under `anthropic-subscription` (no raw `anthropic` row), so the refresh saw `current === undefined` and auth resolved to undefined. Fix: `createFusionCredentialStore.read("anthropic")` now resolves through `getApiKey("anthropic")` (handles refresh + raw/legacy/subscription/fallback precedence) and returns a ready `api_key` credential; pi-ai routes it as OAuth by the `sk-ant-oat` token prefix. Also add "not configured" to `isRetryableModelSelectionError` so an unresolved provider triggers the configured fallback model instead of hard-failing.
|
||||
|
||||
@@ -218,17 +218,19 @@ describe("createFusionAuthStorage", () => {
|
||||
expect(authStorage.hasAuth("anthropic")).toBe(true);
|
||||
});
|
||||
|
||||
// FNXC:ProviderAuth 2026-07-16-11:00 — Symptom Verification for the pi-ai >=0.80 read()-based
|
||||
// auth-resolution regression. Original symptom: a subscription-only Anthropic login (stored only
|
||||
// under `anthropic-subscription`) failed every task with "Provider is not configured: anthropic"
|
||||
// because pi-ai's resolveProviderAuth calls credentials.read("anthropic") directly instead of
|
||||
// fusion's getApiKey("anthropic"), so the subscription->anthropic alias was bypassed. Assert the
|
||||
// credential store's read() path (the exact surface pi-ai uses) resolves the subscription credential.
|
||||
it("aliases Anthropic subscription OAuth through the pi-ai credential-store read('anthropic') path", async () => {
|
||||
// FNXC:ProviderAuth 2026-07-17-06:30 — Symptom Verification for the pi >=0.80.8 ModelRuntime.getAuth
|
||||
// regression. Original symptom: a subscription-only Anthropic login (stored only under
|
||||
// `anthropic-subscription`) failed every task with "Provider is not configured: anthropic" and then
|
||||
// fell back, because pi-ai's resolveProviderAuth reads credentials.read("anthropic") directly and
|
||||
// refreshes an OAuth credential via credentials.modify("anthropic") — but fusion has no raw
|
||||
// `anthropic` row, so the refresh callback saw `current === undefined` and bailed. Fix: read("anthropic")
|
||||
// resolves through fusion's getApiKey (refresh + subscription/raw precedence) and returns a ready
|
||||
// api_key credential carrying the OAuth token (which pi-ai routes as OAuth by its sk-ant-oat prefix).
|
||||
it("resolves Anthropic subscription auth into a ready api_key credential via read('anthropic')", async () => {
|
||||
writeFusionAuth(homeDir, {
|
||||
"anthropic-subscription": {
|
||||
type: "oauth",
|
||||
access: "subscription-access-token",
|
||||
access: "sk-ant-oat01-subscription-access-token",
|
||||
refresh: "subscription-refresh-token",
|
||||
expires: Date.now() + 3_600_000,
|
||||
},
|
||||
@@ -237,15 +239,11 @@ describe("createFusionAuthStorage", () => {
|
||||
const authStorage = createFusionAuthStorage();
|
||||
const credentialStore = createFusionCredentialStore(authStorage);
|
||||
|
||||
// pi-ai reads the credential for provider `anthropic` directly; it must see the subscription OAuth.
|
||||
expect(await credentialStore.read("anthropic")).toMatchObject({
|
||||
type: "oauth",
|
||||
access: "subscription-access-token",
|
||||
});
|
||||
// `anthropic-subscription` still reads its own credential unchanged.
|
||||
expect(await credentialStore.read("anthropic-subscription")).toMatchObject({
|
||||
type: "oauth",
|
||||
access: "subscription-access-token",
|
||||
// pi-ai's resolveProviderAuth reads provider `anthropic` directly; it must get a usable credential
|
||||
// whose token still carries the sk-ant-oat prefix so the anthropic-messages layer routes it as OAuth.
|
||||
expect(await credentialStore.read("anthropic")).toEqual({
|
||||
type: "api_key",
|
||||
key: "sk-ant-oat01-subscription-access-token",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -254,7 +252,7 @@ describe("createFusionAuthStorage", () => {
|
||||
anthropic: { type: "api_key", key: "sk-ant-api03-runtime-key" },
|
||||
"anthropic-subscription": {
|
||||
type: "oauth",
|
||||
access: "subscription-access-token",
|
||||
access: "sk-ant-oat01-subscription-access-token",
|
||||
refresh: "subscription-refresh-token",
|
||||
expires: Date.now() + 3_600_000,
|
||||
},
|
||||
@@ -263,7 +261,7 @@ describe("createFusionAuthStorage", () => {
|
||||
const authStorage = createFusionAuthStorage();
|
||||
const credentialStore = createFusionCredentialStore(authStorage);
|
||||
|
||||
// Raw api_key wins; the subscription alias only fills the gap when no raw/legacy row exists.
|
||||
// Raw api_key wins (getApiKey precedence); the subscription token only fills the gap otherwise.
|
||||
expect(await credentialStore.read("anthropic")).toEqual({
|
||||
type: "api_key",
|
||||
key: "sk-ant-api03-runtime-key",
|
||||
|
||||
@@ -147,15 +147,15 @@ class FusionFileAuthStorage implements FusionAuthStorage {
|
||||
export function createFusionCredentialStore(authStorage: FusionAuthStorage): CredentialStore {
|
||||
return {
|
||||
/*
|
||||
FNXC:ProviderAuth 2026-07-16-11:00:
|
||||
pi-ai >=0.80 resolves provider auth by reading the credential store directly (`resolveProviderAuth` -> `credentials.read(provider.id)`) and performs OAuth refresh + auth derivation itself, instead of calling fusion's `getApiKey(provider)`. Fusion persists an Anthropic subscription login under `anthropic-subscription`, but Anthropic model execution requests provider `anthropic`, so the subscription->anthropic aliasing that lived only in `resolveAnthropicRuntimeApiKey` (the getApiKey path) is now bypassed. Without aliasing at the read() layer a subscription-only login surfaces at prompt time as `Provider is not configured: anthropic` even though the status card shows "connected" (status uses hasVisibleAnthropicCredential, a different path). When no raw/legacy `anthropic` credential exists, alias the separated `anthropic-subscription` OAuth credential into read("anthropic") so pi-ai runs it on the built-in provider (/v1 Claude Code impersonation). A raw `anthropic` api_key or legacy oauth row still wins. See resolveAnthropicRuntimeApiKey for the mirror precedence.
|
||||
FNXC:ProviderAuth 2026-07-17-06:30:
|
||||
pi >=0.80.8 moved session request auth from `ModelRegistry.getApiKeyAndHeaders` (which called fusion's `getApiKey(provider)`) to `ModelRuntime.getAuth` -> pi-ai `resolveProviderAuth`, which reads the credential store directly (`credentials.read(provider.id)`) and, for an OAuth credential, refreshes it ITSELF via `credentials.modify(provider.id, ...)`. That refresh path is broken for Anthropic: fusion persists the subscription login under `anthropic-subscription` (there is NO raw `anthropic` row), so `modify("anthropic")` reads `current === undefined`, the refresh callback bails, and `resolveStoredOAuth` returns undefined -> the task fails with `Provider is not configured: anthropic` (then falls back). The status card still shows "connected" because status uses a different path (hasVisibleAnthropicCredential). Fix: resolve Anthropic auth through fusion's `getApiKey("anthropic")`, the battle-tested path that already handles token refresh + the raw-key/legacy-oauth/subscription/fallback precedence (see resolveAnthropicRuntimeApiKey), and hand pi-ai a ready-to-use api_key credential. pi-ai's anthropic-messages layer routes by token prefix — `sk-ant-oat*` -> OAuth Bearer + Claude Code identity headers, otherwise x-api-key — so a subscription OAuth token still runs as OAuth, and returning it as `api_key` deliberately bypasses pi-ai's own (broken-for-us) OAuth refresh-via-modify. Other OAuth providers (openai-codex, github-copilot) are stored under their own provider id, so read/modify share an id and pi-ai's refresh works — only Anthropic needs this indirection.
|
||||
*/
|
||||
read: async (providerId) => {
|
||||
const credential = authStorage.get(providerId) as Credential | undefined;
|
||||
if (!credential && providerId === ANTHROPIC_PROVIDER_ID) {
|
||||
return authStorage.get(ANTHROPIC_SUBSCRIPTION_PROVIDER_ID) as Credential | undefined;
|
||||
if (providerId === ANTHROPIC_PROVIDER_ID) {
|
||||
const token = await authStorage.getApiKey(ANTHROPIC_PROVIDER_ID);
|
||||
return token ? ({ type: "api_key", key: token } as Credential) : undefined;
|
||||
}
|
||||
return credential;
|
||||
return authStorage.get(providerId) as Credential | undefined;
|
||||
},
|
||||
list: async () => authStorage.list().flatMap((providerId): CredentialInfo[] => {
|
||||
const credential = authStorage.get(providerId);
|
||||
|
||||
Reference in New Issue
Block a user