From b766764e5508cb14d71b8f91b6b68c39f090ae50 Mon Sep 17 00:00:00 2001 From: Pieter van Zyl <20579513+PieterVanZyl-Dev@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:30:58 +0200 Subject: [PATCH] fix(server): recognize Bedrock-backed Claude as authenticated The Claude provider verifies auth by probing the Claude Agent SDK initialization result. Amazon Bedrock init is much slower than first-party auth (measured p50 ~16.7s, p90 ~18.2s, max ~20.2s locally, since the SDK boots the Bedrock backend and runs the awsAuthRefresh credential hook). The 8s probe timeout expired mid-init, so the probe returned undefined and the provider was reported as "Could not verify authentication" - a warning state that left it unselectable in the model picker. Raise CAPABILITIES_PROBE_TIMEOUT_MS to 25s (above the measured p90 with headroom for the observed max) and surface the SDK AccountInfo.apiProvider, labeling apiProvider === "bedrock" as authenticated ("Amazon Bedrock"). Adds a unit test for the Bedrock path. --- .../src/provider/Layers/ClaudeProvider.ts | 29 +++++++++++++++---- .../provider/Layers/ProviderRegistry.test.ts | 26 +++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index 59b7d0464de..c486f5ff759 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -479,9 +479,19 @@ function claudeAuthMetadata(input: { return undefined; } +function apiProviderAuthMetadata( + apiProvider: string | undefined, +): { readonly type: string; readonly label: string } | undefined { + return apiProvider === "bedrock" ? { type: "bedrock", label: "Amazon Bedrock" } : undefined; +} + // ── SDK capability probe ──────────────────────────────────────────── -const CAPABILITIES_PROBE_TIMEOUT_MS = 8_000; +// Amazon Bedrock initializes far slower than first-party auth: the SDK boots the +// Bedrock backend and runs the `awsAuthRefresh` credential hook before returning +// account info. The previous 8s budget expired mid-init, so the probe returned +// `undefined` and left the provider unverified and unselectable in the picker. +const CAPABILITIES_PROBE_TIMEOUT_MS = 25_000; function nonEmptyProbeString(value: string): string | undefined { const candidate = value.trim(); @@ -492,6 +502,12 @@ type ClaudeCapabilitiesProbe = { readonly email: string | undefined; readonly subscriptionType: string | undefined; readonly tokenSource: string | undefined; + /** + * Active API backend reported by the SDK's `AccountInfo`. Anthropic OAuth + * login only applies when `"firstParty"`; for Amazon Bedrock (`"bedrock"`) + * the subscription/token fields are absent and auth is external AWS creds. + */ + readonly apiProvider: string | undefined; readonly slashCommands: ReadonlyArray; }; @@ -611,12 +627,14 @@ const probeClaudeCapabilities = ( readonly email?: string; readonly subscriptionType?: string; readonly tokenSource?: string; + readonly apiProvider?: string; } | undefined; return { email: account?.email, subscriptionType: account?.subscriptionType, tokenSource: account?.tokenSource, + apiProvider: account?.apiProvider, slashCommands: parseClaudeInitializationCommands(init.commands), } satisfies ClaudeCapabilitiesProbe; }); @@ -792,10 +810,11 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( }); } - const authMetadata = claudeAuthMetadata({ - subscriptionType: capabilities.subscriptionType, - authMethod: capabilities.tokenSource, - }); + const authMetadata = + claudeAuthMetadata({ + subscriptionType: capabilities.subscriptionType, + authMethod: capabilities.tokenSource, + }) ?? apiProviderAuthMetadata(capabilities.apiProvider); return buildServerProvider({ presentation: CLAUDE_PRESENTATION, enabled: claudeSettings.enabled, diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index b3ab1145495..355dccd34d8 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -100,6 +100,7 @@ type TestClaudeCapabilities = { readonly email: string | undefined; readonly subscriptionType: string | undefined; readonly tokenSource: string | undefined; + readonly apiProvider: string | undefined; readonly slashCommands: ReadonlyArray; }; @@ -109,6 +110,7 @@ function claudeCapabilities(overrides: Partial = {}) { email: undefined, subscriptionType: undefined, tokenSource: undefined, + apiProvider: undefined, slashCommands: [], ...overrides, }); @@ -1492,6 +1494,30 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te ), ); + it.effect("returns ready and labels Bedrock-backed Claude as authenticated", () => + Effect.gen(function* () { + // Bedrock authenticates via external AWS credentials, so the SDK init + // reports only `apiProvider` with no subscription or token. + const status = yield* checkClaudeProviderStatus( + defaultClaudeSettings, + claudeCapabilities({ apiProvider: "bedrock" }), + ); + assert.strictEqual(status.status, "ready"); + assert.strictEqual(status.installed, true); + assert.strictEqual(status.auth.status, "authenticated"); + assert.strictEqual(status.auth.type, "bedrock"); + assert.strictEqual(status.auth.label, "Amazon Bedrock"); + }).pipe( + Effect.provide( + mockSpawnerLayer((args) => { + const joined = args.join(" "); + if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 }; + throw new Error(`Unexpected args: ${joined}`); + }), + ), + ), + ); + it.effect("includes Claude Fable 5 on supported Claude Code versions", () => Effect.gen(function* () { const status = yield* checkClaudeProviderStatus(