import { type AccountInfo, type ModelInfo, type Options, query, } from "@anthropic-ai/claude-agent-sdk"; import { Type } from "typebox"; import { errorMessage } from "./errors.js"; import { sdkChildEnv } from "./sdk-child-env.js"; import type { ProviderSettings } from "./settings.js"; import { parseValue } from "./validation.js"; const ACCOUNT_INFO_SCHEMA = Type.Object({ email: Type.Optional(Type.String()), organization: Type.Optional(Type.String()), subscriptionType: Type.Optional(Type.String()), tokenSource: Type.Optional(Type.String()), apiKeySource: Type.Optional(Type.String()), apiProvider: Type.Optional(Type.String()), }); // Parsing cleans unknown keys, so anything the catalog synthesizes a model from has to be named // here or it never survives the probe. const MODEL_INFO_SCHEMA = Type.Object({ value: Type.String(), resolvedModel: Type.Optional(Type.String()), displayName: Type.String(), description: Type.String(), supportedEffortLevels: Type.Optional( Type.Array( Type.Union([ Type.Literal("low"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh"), Type.Literal("max"), ]), ), ), supportsAdaptiveThinking: Type.Optional(Type.Boolean()), }); const SUPPORTED_MODELS_SCHEMA = Type.Array(MODEL_INFO_SCHEMA); interface AccountQuery { accountInfo(): Promise; supportedModels(): Promise; close(): void; } interface AccountProbeDependencies { providerSettings: ProviderSettings; queryFactory(request: { prompt: AsyncIterable; options?: Options }): AccountQuery; } export interface AccountSnapshot { available: boolean; supportedModels: readonly ModelInfo[]; } export type AccountProbe = () => Promise; async function* noPrompt(): AsyncGenerator {} function nonEmpty(value: unknown): value is string { return typeof value === "string" && value.length > 0; } function parseAccountInfo(value: unknown): boolean { const account = parseValue( ACCOUNT_INFO_SCHEMA, value, "Claude Code returned malformed account information", ); if (account.apiProvider !== undefined && account.apiProvider !== "firstParty") { throw new Error(`Claude Code is using unsupported API provider "${account.apiProvider}"`); } const hasAccountIdentity = nonEmpty(account.email) || nonEmpty(account.subscriptionType); const hasCredentialSource = (nonEmpty(account.tokenSource) && account.tokenSource !== "none") || (nonEmpty(account.apiKeySource) && account.apiKeySource !== "none"); if (!hasAccountIdentity && !hasCredentialSource) return false; if (account.apiProvider === undefined) { throw new Error("Claude Code returned an authenticated account without an API provider"); } return true; } function parseSupportedModels(value: unknown): ModelInfo[] { return parseValue( SUPPORTED_MODELS_SCHEMA, value, "Claude Code returned malformed supported-model information", ); } export function createAccountProbe(dependencies: AccountProbeDependencies): AccountProbe { const { providerSettings, queryFactory } = dependencies; return async () => { let controlQuery: AccountQuery | undefined; const abortController = new AbortController(); let timeout: ReturnType | undefined; try { const claudeExecutable = providerSettings.pathToClaudeCodeExecutable; controlQuery = queryFactory({ prompt: noPrompt(), options: { abortController, env: sdkChildEnv(), tools: [], mcpServers: {}, strictMcpConfig: true, settingSources: [], skills: [], persistSession: false, ...(claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}), }, }); const deadline = new Promise((_resolve, reject) => { timeout = setTimeout(() => { abortController.abort(); reject(new Error("Claude Code account probe timed out after 15 seconds")); }, 15_000); }); const [account, supportedModels] = await Promise.race([ Promise.all([controlQuery.accountInfo(), controlQuery.supportedModels()]), deadline, ]); return { available: parseAccountInfo(account), supportedModels: parseSupportedModels(supportedModels), }; } catch (error) { throw new Error( `Claude Code authentication check failed: ${errorMessage(error)}. Run \`claude auth login\` and try again.`, { cause: error }, ); } finally { if (timeout) clearTimeout(timeout); abortController.abort(); controlQuery?.close(); } }; } export function createDefaultAccountProbe(providerSettings: ProviderSettings): AccountProbe { return createAccountProbe({ providerSettings, queryFactory: query }); }