Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions apps/server/src/provider/Layers/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<ServerProviderSlashCommand>;
};

Expand Down Expand Up @@ -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;
});
Expand Down Expand Up @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ServerProviderSlashCommand>;
};

Expand All @@ -109,6 +110,7 @@ function claudeCapabilities(overrides: Partial<TestClaudeCapabilities> = {}) {
email: undefined,
subscriptionType: undefined,
tokenSource: undefined,
apiProvider: undefined,
slashCommands: [],
...overrides,
});
Expand Down Expand Up @@ -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(
Expand Down
Loading