UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

2,408 lines • 125 kB
import Anthropic from "@anthropic-ai/sdk";
import { SpanKind, trace } from "@opentelemetry/api";
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "fs";
import { homedir } from "os";
import { join } from "path";
import { ANTHROPIC_TOKEN_URL, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_CLIENT_ID, CLAUDE_CODE_OAUTH_BETAS, } from "../../auth/anthropicOAuth.js";
import { AnthropicModels, TOKEN_EXPIRY_BUFFER_MS, } from "../../constants/enums.js";
import { BaseProvider } from "../../core/baseProvider.js";
import { DEFAULT_MAX_STEPS } from "../../core/constants.js";
import { streamAnalyticsCollector } from "../../core/streamAnalytics.js";
import { getModelCapabilities, getRecommendedModelForTier, isModelAvailableForTier, } from "../../models/anthropicModels.js";
import { createOAuthFetch } from "../../proxy/oauthFetch.js";
import { createProxyFetch } from "../../proxy/proxyFetch.js";
import { getCapturedLimitSnapshot, getCapturedResponseHeaders, logClaudeLimitSnapshot, runInLimitCaptureScope, setLimitSpanAttributes, withLimitCapture, wrapFetchWithLimitCapture, } from "./rateLimitCapture.js";
import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../../types/index.js";
import { classifyProviderError } from "../../utils/errorClassifier.js";
import { logger } from "../../utils/logger.js";
import { drainDetachedPump } from "../../utils/drainDetachedPump.js";
import { ANTHROPIC_ELISION_NOTE, planAnthropicLoopReclaim, previewAnthropicToolResultText, } from "../../context/anthropicLoopGuard.js";
import { getAvailableInputTokens } from "../../constants/contextWindows.js";
import { estimateTokens, serializeForEstimate, } from "../../utils/tokenEstimation.js";
import { redactUrlCredentials } from "../../utils/logSanitize.js";
import { ANTHROPIC_MAX_CACHE_BREAKPOINTS, applyAnthropicHistoryCacheBreakpoints, countAnthropicCacheMarkers, } from "../../utils/anthropicCacheBreakpoints.js";
import { calculateCost } from "../../utils/pricing.js";
import { stringifyAnthropicToolOutput } from "./toolOutput.js";
import { createAnthropicLoopAdapter } from "./loopAdapter.js";
import { DEFAULT_BEFORE_STEP_TIMEOUT_MS } from "../../utils/parameterValidation.js";
import { runAgenticLoop } from "../../core/loopEngine.js";
import { hasNativeDoGenerate, runNativeGenerateLoop, } from "../../core/nativeGenerateLoop.js";
import { withProviderRetry } from "../../utils/providerRetry.js";
import { resolveRequestKind } from "../../core/resolveRequestKind.js";
import { resolveToolExecutionRecords, toolCallsFromSummaries, } from "../../core/toolExecutionRecorder.js";
import { transformToolExecutions } from "../../utils/transformationUtils.js";
import { createAnthropicConfig, getProviderModel, validateApiKey, } from "../../utils/providerConfig.js";
import { composeAbortSignals, composeAbortSignalsScoped, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../../utils/timeout.js";
import { raceWithAbort } from "../../utils/async/index.js";
import { normalizeResolvedToolChoice, resolveToolChoice, } from "../../utils/toolChoice.js";
import { emitToolEndFromStepFinish } from "../../utils/toolEndEmitter.js";
import { NoOutputGeneratedError } from "../../utils/generationErrors.js";
import { buildNoOutputSentinel, stampNoOutputSpan, } from "../../utils/noOutputSentinel.js";
import { convertZodToJsonSchema } from "../../utils/schemaConversion.js";
import { resolveClaudeMaxTokens } from "../../utils/tokenLimits.js";
import { toAnthropicImageBlock, fileToAnthropicBlock, } from "../anthropicImageBlocks.js";
import { modelSupportsForcedToolChoice, resolveSamplingParams, } from "../../models/modelRegistry.js";
import { createDeferredAnalytics, stringifyToolInput, } from "../openaiChatCompletionsClient.js";
import { createStreamChannel } from "../../core/streamChannel.js";
import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
import { ANTHROPIC_BETA_HEADERS } from "./constants.js";
import { cacheControlOf, withLastToolCacheBreakpoint } from "./cacheControl.js";
import { createNativeGenerateGuard } from "../../context/nativeGenerateGuard.js";
import { appendFinalResultInstruction, appendFinalResultTool, appendToolAnswerInstruction, FINAL_RESULT_TOOL_NAME, } from "./structuredOutput.js";
// AnthropicProviderConfig is imported from types/providers.ts
// Re-export for backward compatibility
// Configuration helpers - now using consolidated utility
const getAnthropicApiKey = () => {
    return validateApiKey(createAnthropicConfig());
};
const getDefaultAnthropicModel = () => {
    return getProviderModel("ANTHROPIC_MODEL", AnthropicModels.CLAUDE_SONNET_4_6);
};
/**
 * The official Anthropic SDK builds `${baseURL}/v1/messages` itself, so a
 * version-suffixed base URL — the form the previous @ai-sdk/anthropic
 * implementation REQUIRED (`https://api.anthropic.com/v1`) — would double up
 * as `/v1/v1/messages`. Normalize the inverse way: strip trailing slashes and
 * a trailing `/vN` segment, so both historical forms keep working whether the
 * URL comes from the credentials, the config or `ANTHROPIC_BASE_URL`. Blank
 * means "the vendor's endpoint".
 */
function normalizeAnthropicBaseURL(raw) {
    const value = raw?.trim();
    if (!value) {
        return undefined;
    }
    const trimmed = value.replace(/\/+$/, "");
    const stripped = trimmed.replace(/\/v\d+$/, "");
    if (stripped !== trimmed) {
        logger.debug("[AnthropicProvider] Stripping the version suffix from the base URL — " +
            "the official Anthropic SDK appends /v1 to the base URL itself.", {
            baseURL: redactUrlCredentials(value),
            rewrittenTo: redactUrlCredentials(stripped),
        });
    }
    return stripped;
}
const streamTracer = trace.getTracer("neurolink.provider.anthropic");
/**
 * Get OAuth token from stored credentials file or environment.
 * Priority:
 * 1. Stored credentials file (~/.neurolink/anthropic-credentials.json)
 * 2. Environment variables (ANTHROPIC_OAUTH_TOKEN or CLAUDE_OAUTH_TOKEN)
 */
const getOAuthToken = () => {
    // First, check stored credentials file (highest priority)
    try {
        const credentialsPath = join(homedir(), ".neurolink", "anthropic-credentials.json");
        if (existsSync(credentialsPath)) {
            const credentialsContent = readFileSync(credentialsPath, "utf-8");
            const credentials = JSON.parse(credentialsContent);
            if (credentials.type === "oauth" && credentials.oauth?.accessToken) {
                logger.debug("[AnthropicProvider] Using OAuth token from stored credentials file");
                return credentials.oauth;
            }
        }
    }
    catch (error) {
        logger.debug("[AnthropicProvider] Failed to read stored credentials:", error);
    }
    // Fallback to environment variables
    const tokenString = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.CLAUDE_OAUTH_TOKEN;
    if (!tokenString) {
        return null;
    }
    // Try to parse as JSON (for full token object with refresh token and expiry)
    try {
        const parsed = JSON.parse(tokenString);
        if (typeof parsed === "object" && parsed.accessToken) {
            return parsed;
        }
        // If it's a simple string in JSON, use it as access token
        if (typeof parsed === "string") {
            return { accessToken: parsed };
        }
    }
    catch {
        // Not JSON, treat as plain access token string
    }
    // Treat as plain access token string
    return { accessToken: tokenString };
};
/**
 * Detect subscription tier from environment or token.
 * Environment variable ANTHROPIC_SUBSCRIPTION_TIER takes precedence.
 */
const detectSubscriptionTier = (oauthToken) => {
    // Check explicit environment variable first
    const envTier = process.env.ANTHROPIC_SUBSCRIPTION_TIER?.toLowerCase();
    if (envTier) {
        const validTiers = [
            "free",
            "pro",
            "max",
            "max_5",
            "max_20",
            "api",
        ];
        if (validTiers.includes(envTier)) {
            logger.debug("[detectSubscriptionTier] Using environment override", {
                tier: envTier,
            });
            return envTier;
        }
        logger.warn("[detectSubscriptionTier] Invalid ANTHROPIC_SUBSCRIPTION_TIER", {
            value: envTier,
            validTiers,
        });
    }
    // If using OAuth, default to 'pro' (most common subscription tier)
    if (oauthToken) {
        // Check if token scopes indicate tier (future-proofing)
        const scopes = oauthToken.scopes ?? [];
        let detectedTier = "pro";
        if (scopes.includes("max_20")) {
            detectedTier = "max_20";
        }
        else if (scopes.includes("max_5")) {
            detectedTier = "max_5";
        }
        else if (scopes.includes("max")) {
            detectedTier = "max";
        }
        logger.debug("[detectSubscriptionTier] Detected from OAuth token", {
            tier: detectedTier,
            scopes,
        });
        return detectedTier;
    }
    // Default to 'api' for API key authentication
    logger.debug("[detectSubscriptionTier] No OAuth token, defaulting to API tier");
    return "api";
};
/**
 * Determine authentication method based on available credentials.
 * OAuth takes precedence over API key if both are available.
 */
const detectAuthMethod = (oauthToken) => {
    // Explicit env var takes highest precedence — allows forcing api_key mode
    // even when OAuth credentials exist (e.g., when using a proxy that handles auth)
    const explicit = process.env.ANTHROPIC_AUTH_METHOD?.toLowerCase();
    if (explicit === "api_key" || explicit === "apikey") {
        logger.debug("[detectAuthMethod] Forced to api_key by ANTHROPIC_AUTH_METHOD env var");
        return "api_key";
    }
    if (explicit === "oauth") {
        if (oauthToken) {
            logger.debug("[detectAuthMethod] Forced to oauth by ANTHROPIC_AUTH_METHOD env var");
            return "oauth";
        }
        logger.warn("[detectAuthMethod] ANTHROPIC_AUTH_METHOD=oauth but no OAuth token found; falling through to auto-detection");
    }
    else if (explicit) {
        logger.warn("[detectAuthMethod] Unrecognized ANTHROPIC_AUTH_METHOD value; falling through to auto-detection", {
            value: explicit,
        });
    }
    // Auto-detect: OAuth takes precedence if available
    const method = oauthToken ? "oauth" : "api_key";
    logger.debug("[detectAuthMethod] Auth method resolved", {
        method,
        hasOAuthToken: !!oauthToken,
    });
    return method;
};
// Rate-limit header parsing lives in `rateLimitCapture.ts`, which sees the raw
// fetch Response and understands both header families (unified subscription
// windows and the legacy per-tier counters). The module-private copy that used
// to sit here parsed only the legacy family and had no callers.
// ───────────────────────────────────────────────────────────────────────────
// Native Messages-API conversion helpers (NeuroLink/V3 shapes → Anthropic)
// ───────────────────────────────────────────────────────────────────────────
/**
 * Convert NeuroLink/V3-shaped messages (the shape produced by
 * buildMessagesForStream and by the AI-SDK prompt on the V3 doGenerate path)
 * into the Anthropic Messages payload: a top-level `system` string plus
 * alternating user/assistant messages with typed content blocks.
 */
const messagesToAnthropic = (msgs) => {
    const systemBlocks = [];
    const messages = [];
    const partsOf = (content) => Array.isArray(content) ? content : [content];
    // Message-level cache breakpoints apply to the message's LAST content
    // block (the AI-SDK convention MessageBuilder relies on).
    const applyMessageCacheControl = (blocks, msg) => {
        const cc = cacheControlOf(msg);
        if (cc && blocks.length > 0) {
            const last = blocks[blocks.length - 1];
            last.cache_control = cc;
        }
    };
    for (const msg of msgs) {
        switch (msg.role) {
            case "system": {
                const text = typeof msg.content === "string"
                    ? msg.content
                    : partsOf(msg.content)
                        .map((p) => typeof p === "string"
                        ? p
                        : String(p?.text ?? ""))
                        .join("\n");
                const cc = cacheControlOf(msg);
                systemBlocks.push({
                    type: "text",
                    text,
                    ...(cc ? { cache_control: cc } : {}),
                });
                break;
            }
            case "user": {
                const blocks = [];
                for (const part of partsOf(msg.content)) {
                    if (typeof part === "string") {
                        if (part.length > 0) {
                            blocks.push({ type: "text", text: part });
                        }
                        continue;
                    }
                    const p = part;
                    if (p?.type === "text" && typeof p.text === "string") {
                        const cc = cacheControlOf(p);
                        blocks.push({
                            type: "text",
                            text: p.text,
                            ...(cc ? { cache_control: cc } : {}),
                        });
                    }
                    else if (p?.type === "image" || p?.type === "image_url") {
                        const img = toAnthropicImageBlock(p.image ?? p.data ?? p.url);
                        if (img) {
                            const cc = cacheControlOf(p);
                            blocks.push(cc ? { ...img, cache_control: cc } : img);
                        }
                    }
                    else if (p?.type === "file") {
                        // AI-SDK v6 encodes images AND PDFs as `type:"file"` parts in the
                        // LanguageModel prompt that `doGenerate` receives. Without this
                        // branch the image is dropped on the tool-using generate path and
                        // the model never sees it ("no image detected").
                        //
                        // Runtime guard: p comes from message parsing and may not match the
                        // expected shape. Verify p is an object and that mediaType, if
                        // present, is a string (not an object/array from a malformed part).
                        // Skip gracefully rather than passing a bad shape to fileToAnthropicBlock.
                        const isValidFilePart = typeof p === "object" &&
                            p !== null &&
                            ("mediaType" in p
                                ? typeof p.mediaType === "string"
                                : true);
                        const block = isValidFilePart
                            ? fileToAnthropicBlock(p)
                            : undefined;
                        if (block) {
                            const cc = cacheControlOf(p);
                            if (cc) {
                                // block is a fresh object from fileToAnthropicBlock; mutate in
                                // place to keep the discriminated-union type (a spread widens it
                                // past ContentBlockParam).
                                block.cache_control = cc;
                            }
                            blocks.push(block);
                        }
                    }
                }
                if (blocks.length > 0) {
                    applyMessageCacheControl(blocks, msg);
                    messages.push({ role: "user", content: blocks });
                }
                break;
            }
            case "assistant": {
                const blocks = [];
                // Extended thinking must come back byte-identical — signature
                // included — or Anthropic rejects the turn, and the loop replays this
                // message on every tool step. Blocks are emitted in content order
                // rather than hoisted: `interleaved-thinking-2025-05-14` (requested in
                // the beta header) lets thinking appear between tool calls, so
                // reordering would corrupt the chain it validates.
                for (const part of partsOf(msg.content)) {
                    if (typeof part === "string") {
                        if (part.length > 0) {
                            blocks.push({ type: "text", text: part });
                        }
                        continue;
                    }
                    const p = part;
                    if (p?.type === "reasoning") {
                        const meta = p.providerOptions?.anthropic;
                        const redacted = meta?.redactedData;
                        if (typeof redacted === "string" && redacted.length > 0) {
                            blocks.push({ type: "redacted_thinking", data: redacted });
                            continue;
                        }
                        const signature = meta?.signature;
                        // Both halves required, matching loopAdapter's check on the
                        // streaming path: Anthropic rejects a thinking block that is
                        // unsigned, and equally one whose text is empty. Reasoning from a
                        // provider that never produced a signature (a reasoner model's
                        // plain text) is not an Anthropic thinking block at all, and an
                        // empty one carries nothing worth replaying — either way, dropping
                        // it beats sending a block that will be refused.
                        if (typeof signature === "string" &&
                            signature.length > 0 &&
                            typeof p.text === "string" &&
                            p.text.length > 0) {
                            blocks.push({ type: "thinking", thinking: p.text, signature });
                        }
                        continue;
                    }
                    if (p?.type === "text" && typeof p.text === "string") {
                        if (p.text.length > 0) {
                            const cc = cacheControlOf(p);
                            blocks.push({
                                type: "text",
                                text: p.text,
                                ...(cc ? { cache_control: cc } : {}),
                            });
                        }
                    }
                    else if (p?.type === "tool-call") {
                        let input = p.input;
                        if (typeof input === "string") {
                            try {
                                input = JSON.parse(input);
                            }
                            catch {
                                input = {};
                            }
                        }
                        blocks.push({
                            type: "tool_use",
                            id: p.toolCallId ?? "",
                            name: p.toolName ?? "",
                            input: input ?? {},
                        });
                    }
                }
                if (blocks.length > 0) {
                    applyMessageCacheControl(blocks, msg);
                    messages.push({ role: "assistant", content: blocks });
                }
                break;
            }
            case "tool": {
                // Tool results are user-role messages with tool_result blocks.
                const blocks = [];
                if (Array.isArray(msg.content)) {
                    for (const part of msg.content) {
                        const p = part;
                        if (p?.type === "tool-result") {
                            blocks.push({
                                type: "tool_result",
                                tool_use_id: p.toolCallId ?? "",
                                content: stringifyAnthropicToolOutput(p.output),
                            });
                        }
                    }
                }
                else if (typeof msg.content === "string") {
                    blocks.push({
                        type: "tool_result",
                        tool_use_id: msg.toolCallId ?? "",
                        content: msg.content,
                    });
                }
                if (blocks.length > 0) {
                    messages.push({ role: "user", content: blocks });
                }
                break;
            }
        }
    }
    // Plain-string system when no cache breakpoints are present (matches the
    // previous wire shape); block form only when cache_control must ride along.
    const system = systemBlocks.length === 0
        ? undefined
        : systemBlocks.some((b) => b.cache_control)
            ? systemBlocks
            : systemBlocks.map((b) => b.text).join("\n\n");
    return {
        ...(system !== undefined ? { system } : {}),
        messages,
    };
};
/** Map a NeuroLink tool choice onto Anthropic's tool_choice shape. */
const toolChoiceToAnthropic = (choice) => {
    if (!choice || choice === "auto") {
        return undefined; // Anthropic defaults to auto when tools are present
    }
    if (choice === "none") {
        return { type: "none" };
    }
    if (choice === "required") {
        return { type: "any" };
    }
    if (typeof choice === "object") {
        const c = choice;
        if (c.type === "tool" && c.toolName) {
            return { type: "tool", name: c.toolName };
        }
    }
    return undefined;
};
/**
 * Claude 5.5 / 5.1 models answer a forced choice with a 400, so for them it
 * becomes `auto`: the tool stays declared and the model still picks it.
 */
const relaxForcedToolChoice = (modelId, choice) => {
    if ((choice?.type !== "any" && choice?.type !== "tool") ||
        modelSupportsForcedToolChoice(modelId)) {
        return choice;
    }
    logger.warn(`[anthropic] ${modelId} rejects tool_choice "${choice.type}"; sending "auto" instead`);
    return {
        type: "auto",
        ...(choice.disable_parallel_tool_use !== undefined
            ? { disable_parallel_tool_use: choice.disable_parallel_tool_use }
            : {}),
    };
};
/** Map Anthropic stop_reason onto the V3 unified finish reason. */
const mapAnthropicStopReason = (raw) => {
    switch (raw) {
        case "max_tokens":
            return "length";
        case "tool_use":
            return "tool-calls";
        case "refusal":
            return "content-filter";
        default:
            return "stop";
    }
};
// Anthropic's Messages API requires max_tokens on every request. When the
// caller omits it, default to the model's real output ceiling via
// resolveClaudeMaxTokens (e.g. 64K for Sonnet 4.x) instead of the legacy 4096,
// which silently truncated large structured responses mid-JSON.
//
// Client-level request timeout. The Anthropic SDK throws "Streaming is required
// for long requests" from a NON-streaming `messages.create` when `max_tokens`
// is large AND no client-level timeout is configured (it can't estimate a safe
// timeout). Setting an explicit client timeout — equal to the SDK's own default
// for the non-throwing path — suppresses that pre-flight throw so large
// max_tokens (our model-ceiling default) works. Per-request duration is still
// bounded by the abort signal NeuroLink composes for each call.
const ANTHROPIC_CLIENT_TIMEOUT_MS = 600_000;
/**
 * Anthropic Provider v2 - BaseProvider Implementation
 * Enhanced with OAuth support, subscription tiers, and beta headers for Claude Code integration.
 */
export class AnthropicProvider extends BaseProvider {
    client;
    authMethod;
    subscriptionTier;
    enableBetaFeatures;
    /**
     * Where requests go when not to `api.anthropic.com`: the credentials'
     * `baseURL`, else the config's, else `ANTHROPIC_BASE_URL` — normalized once
     * (no trailing slash, no `/vN` suffix: the SDK appends `/v1` itself).
     * Undefined means the vendor's own endpoint; every "proxy in use" decision
     * reads this, never the environment directly, so a per-instance gateway is
     * honoured the same way the environment one always was.
     */
    baseURL;
    oauthToken;
    lastResponseMetadata = null;
    usageInfo = null;
    refreshPromise;
    /**
     * Create a new Anthropic provider instance.
     *
     * @param modelName - Optional model name to use (defaults to CLAUDE_3_5_SONNET)
     * @param sdk - Optional NeuroLink SDK instance
     * @param config - Optional configuration options for auth, subscription tier, and beta features
     */
    constructor(modelName, sdk, config, credentials) {
        // Pre-compute effective model with tier validation before calling super.
        //
        // When per-request credentials supply an apiKey (without oauthToken),
        // force api_key auth — skip OAuth detection entirely so the caller's
        // key is used rather than a stale OAuth token from ~/.neurolink/.
        const forceApiKey = !!(credentials?.apiKey && !credentials?.oauthToken);
        const oauthToken = forceApiKey
            ? null
            : ((credentials?.oauthToken
                ? { accessToken: credentials.oauthToken }
                : null) ??
                config?.oauthToken ??
                getOAuthToken());
        // Resolve auth method FIRST so that tier detection uses the chosen method.
        // If ANTHROPIC_AUTH_METHOD=api_key wins over an existing OAuth token, the
        // tier must reflect api_key mode (full model access) rather than the OAuth
        // token's subscription level.
        const authMethod = forceApiKey
            ? "api_key"
            : (config?.authMethod ?? detectAuthMethod(oauthToken));
        const subscriptionTier = config?.subscriptionTier ??
            (authMethod === "oauth" ? detectSubscriptionTier(oauthToken) : "api");
        const targetModel = modelName || getDefaultAnthropicModel();
        // Resolved before super(): the tier check below needs it, and the
        // credentials win over the config, which wins over the environment —
        // the same precedence `apiKey` has.
        const baseURL = normalizeAnthropicBaseURL(credentials?.baseURL ?? config?.baseURL ?? process.env.ANTHROPIC_BASE_URL);
        // Determine effective model based on tier access.
        // Skip tier validation when a proxy is in use (a base URL is set) — the
        // proxy handles model access and auth, so the SDK should pass the
        // requested model through without downgrading.
        let effectiveModel = targetModel;
        const usingProxy = baseURL !== undefined;
        if (!usingProxy &&
            subscriptionTier !== "api" &&
            !isModelAvailableForTier(targetModel, subscriptionTier)) {
            effectiveModel = getRecommendedModelForTier(subscriptionTier);
            logger.warn("Model not available for subscription tier, using recommended model", {
                requestedModel: targetModel,
                subscriptionTier,
                recommendedModel: effectiveModel,
            });
        }
        super(effectiveModel, "anthropic", sdk);
        // Apply configuration with defaults
        this.enableBetaFeatures = config?.enableBetaFeatures ?? true;
        // Store computed values
        this.oauthToken = oauthToken;
        this.subscriptionTier = subscriptionTier;
        this.baseURL = baseURL;
        // Use the auth method already resolved above (before tier computation)
        this.authMethod = authMethod;
        // Build headers based on auth method and subscription tier
        const headers = this.getAuthHeaders();
        // Create the official Anthropic SDK client based on auth method
        let client;
        logger.debug("[AnthropicProvider] Constructor - checking OAuth:", {
            authMethod: this.authMethod,
            hasOAuthToken: !!this.oauthToken,
            hasAccessToken: !!this.oauthToken?.accessToken,
        });
        if (this.authMethod === "oauth" && this.oauthToken) {
            // OAuth authentication - use custom fetch wrapper that handles:
            // - Bearer token authorization
            // - OAuth beta headers (oauth-2025-04-20, NOT claude-code-20250219)
            // - User-Agent spoofing
            // - ?beta=true query param
            // - Tool name prefixing/stripping
            logger.debug("[AnthropicProvider] Creating OAuth fetch wrapper...");
            // Pass a getter so the fetch wrapper always uses the current token,
            // even after an automatic token refresh.
            // oauthToken is guaranteed non-null here (checked by the enclosing if-guard).
            const tokenRef = this.oauthToken;
            // skipBodyTransform=true: For the SDK client path, body transforms ARE
            // intentionally skipped because the official Anthropic SDK builds its
            // own request format (system prompts, metadata, tool definitions). The
            // billing header, agent block, user_id injection, and mcp_ tool-name
            // prefixing are only needed for proxy passthrough of raw Claude API
            // requests where we must make the request look like it came from
            // Claude Code / CLIProxyAPI.
            const oauthFetch = createOAuthFetch(() => tokenRef.accessToken, this.enableBetaFeatures, false, // No mcp_ prefix — tool names pass through as-is (matches CLIProxyAPI)
            true);
            // For OAuth, we use a dummy API key since our fetch wrapper handles auth
            // IMPORTANT: Do NOT pass beta headers here - our fetch wrapper handles them
            // The claude-code-20250219 beta header triggers "credential only for Claude Code" error
            client = new Anthropic({
                apiKey: "oauth-authenticated", // Placeholder, actual auth is in fetch wrapper
                // The same gateway the API-key branch honours: without it, OAuth
                // credentials that also name a base URL would still reach the vendor.
                ...(this.baseURL && { baseURL: this.baseURL }),
                // Note: No headers passed - fetch wrapper sets oauth-2025-04-20 beta header
                // Limit capture wraps the OAuth fetch so subscription quota headers
                // (anthropic-ratelimit-unified-*) are recorded on every request —
                // streaming and non-streaming alike.
                fetch: wrapFetchWithLimitCapture(oauthFetch),
                timeout: ANTHROPIC_CLIENT_TIMEOUT_MS,
                // The SDK's built-in retry honors Retry-After hints without any
                // upper bound (a 429 with retry-after: 8549 sleeps 2.4h per retry,
                // invisible to fallback orchestration). Retries are the
                // orchestrator's job; transient network blips are still retried by
                // the fetch wrapper.
                maxRetries: 0,
            });
            logger.debug("[AnthropicProvider] Anthropic SDK client created with OAuth fetch wrapper");
            logger.debug("Anthropic Provider initialized with OAuth", {
                modelName: this.modelName,
                provider: this.providerName,
                authMethod: this.authMethod,
                subscriptionTier: this.subscriptionTier,
                enableBetaFeatures: this.enableBetaFeatures,
                hasRefreshToken: !!this.oauthToken.refreshToken,
                tokenExpiry: this.oauthToken.expiresAt
                    ? new Date(this.oauthToken.expiresAt).toISOString()
                    : "none",
            });
        }
        else {
            // Traditional API key authentication
            const apiKeyToUse = credentials?.apiKey ?? config?.apiKey ?? getAnthropicApiKey();
            client = new Anthropic({
                apiKey: apiKeyToUse,
                defaultHeaders: headers,
                ...(this.baseURL && { baseURL: this.baseURL }),
                // Same capture as the OAuth branch: works for direct API-key traffic
                // (legacy requests/tokens counters) and for the NeuroLink Claude proxy
                // (verbatim unified quota plus x-neurolink-* account/pool state).
                fetch: wrapFetchWithLimitCapture(createProxyFetch()),
                timeout: ANTHROPIC_CLIENT_TIMEOUT_MS,
                // See the OAuth-path client above: unbounded Retry-After sleeps in
                // the SDK's retry loop must never stall fallback orchestration.
                maxRetries: 0,
            });
            logger.debug("Anthropic Provider initialized with API key", {
                modelName: this.modelName,
                provider: this.providerName,
                authMethod: this.authMethod,
                subscriptionTier: this.subscriptionTier,
                enableBetaFeatures: this.enableBetaFeatures,
            });
        }
        this.client = client;
        // Initialize usage tracking
        this.usageInfo = {
            messagesUsed: 0,
            messagesRemaining: -1, // Unknown until we get rate limit headers
            tokensUsed: 0,
            tokensRemaining: -1,
            inputTokensUsed: 0,
            outputTokensUsed: 0,
            lastRequestTimestamp: 0,
            isRateLimited: false,
            requestCount: 0,
            messageQuotaPercent: 0,
            tokenQuotaPercent: 0,
        };
        logger.debug("Anthropic Provider v2 initialized", {
            modelName: this.modelName,
            provider: this.providerName,
            authMethod: this.authMethod,
            subscriptionTier: this.subscriptionTier,
            enableBetaFeatures: this.enableBetaFeatures,
            betaFeatures: this.enableBetaFeatures
                ? ANTHROPIC_BETA_HEADERS["anthropic-beta"]
                : "disabled",
        });
    }
    /**
     * Get authentication headers based on current auth method and configuration.
     *
     * @returns Headers object containing auth and beta feature headers
     */
    getAuthHeaders() {
        const headers = {};
        // When routing through a proxy (a base URL is set — per instance or
        // ANTHROPIC_BASE_URL), use the full OAuth beta set so the proxy forwards
        // them upstream. Without these, Anthropic treats the request with tighter
        // non-subscription rate limits.
        const usingProxy = this.baseURL !== undefined;
        if (this.enableBetaFeatures) {
            if (usingProxy) {
                // The 1M-context beta requires a plan upgrade on most accounts;
                // surfacing it by default forces a "The long context beta is not
                // yet available for this subscription." failure for everyone else.
                // Gate behind ANTHROPIC_ENABLE_LONG_CONTEXT_BETA=1 so default-tier
                // accounts (and CI) can use the proxy without the gated feature.
                const longContextOptIn = process.env.ANTHROPIC_ENABLE_LONG_CONTEXT_BETA === "1" ||
                    process.env.ANTHROPIC_ENABLE_LONG_CONTEXT_BETA === "true";
                const betas = [
                    ...CLAUDE_CODE_OAUTH_BETAS,
                    "fine-grained-tool-streaming-2025-05-14",
                    "interleaved-thinking-2025-05-14",
                    "redact-thinking-2026-02-12",
                ];
                if (longContextOptIn) {
                    betas.push("context-1m-2025-08-07");
                }
                headers["anthropic-beta"] = betas.join(",");
            }
            else {
                headers["anthropic-beta"] = ANTHROPIC_BETA_HEADERS["anthropic-beta"];
            }
        }
        if (usingProxy) {
            // WAFs in front of Anthropic proxies commonly block the bare SDK UA
            // ("Anthropic/JS x.y.z"); send the claude-cli UA the OAuth path already
            // uses. Direct-to-Anthropic traffic keeps the honest SDK UA.
            headers["User-Agent"] = CLAUDE_CLI_USER_AGENT;
        }
        // Add subscription-specific headers if applicable
        if (this.subscriptionTier !== "api") {
            headers["x-subscription-tier"] = this.subscriptionTier;
        }
        return headers;
    }
    /**
     * Validate if a model is accessible with the current subscription tier.
     *
     * @param model - The model ID to validate
     * @returns true if the model is accessible, false otherwise
     *
     * @example
     * ```typescript
     * const provider = new AnthropicProvider();
     * if (provider.validateModelAccess("claude-opus-4-5-20251101")) {
     *   // Use the model
     * } else {
     *   // Fall back to a different model or show upgrade prompt
     * }
     * ```
     */
    validateModelAccess(model) {
        // Proxy mode: bypass tier validation entirely — the proxy handles model
        // access. Log at debug level so users can tell why an unknown model name
        // "validated" when their proxy may not actually expose it.
        if (this.baseURL !== undefined) {
            logger.debug("[validateModelAccess] Bypassing tier check (a base URL is set — the proxy enforces access)", { model });
            return true;
        }
        // API tier has access to all models
        if (this.subscriptionTier === "api") {
            return true;
        }
        const hasAccess = isModelAvailableForTier(model, this.subscriptionTier);
        if (!hasAccess) {
            logger.debug("[validateModelAccess] Model not available for tier", {
                model,
                tier: this.subscriptionTier,
            });
        }
        return hasAccess;
    }
    /**
     * Get current usage information.
     *
     * Returns usage tracking data including messages sent, tokens consumed,
     * and remaining quotas. This information is updated after each API request.
     *
     * @returns Current usage info or null if no requests have been made
     *
     * @example
     * ```typescript
     * const usage = provider.getUsageInfo();
     * if (usage && usage.tokenQuotaPercent > 80) {
     *   console.warn("Approaching token quota limit");
     * }
     * ```
     */
    getUsageInfo() {
        return this.usageInfo;
    }
    /**
     * Check if beta features are enabled for this provider instance.
     *
     * @returns true if beta features are enabled
     */
    areBetaFeaturesEnabled() {
        return this.enableBetaFeatures;
    }
    /**
     * Get model capabilities for the current model.
     *
     * @returns The model capabilities or undefined if not found
     */
    getModelCapabilities() {
        return getModelCapabilities(this.modelName || this.getDefaultModel());
    }
    /**
     * Get the current subscription tier.
     * @returns The detected or configured subscription tier
     */
    getSubscriptionTier() {
        return this.subscriptionTier;
    }
    /**
     * Get the authentication method being used.
     * @returns The current authentication method
     */
    getAuthMethod() {
        return this.authMethod;
    }
    /**
     * Refresh OAuth token if needed and possible.
     * This method checks if the token is expired or about to expire,
     * and attempts to refresh it using the refresh token if available.
     *
     * @returns Promise that resolves when refresh is complete (or not needed)
     * @throws Error if refresh is needed but fails
     */
    async refreshAuthIfNeeded() {
        // Only applicable for OAuth authentication
        if (this.authMethod !== "oauth" || !this.oauthToken) {
            logger.debug("Token refresh not applicable for API key authentication");
            return;
        }
        // Check if token has expiry information
        if (!this.oauthToken.expiresAt) {
            logger.debug("Token has no expiry information, assuming valid");
            return;
        }
        // expiresAt is stored as Unix milliseconds (matching how auth status/refresh stores it).
        // Compare against Date.now() so both sides are in milliseconds.
        const now = Date.now();
        const isExpired = this.oauthToken.expiresAt <= now;
        const isExpiringSoon = this.oauthToken.expiresAt <= now + TOKEN_EXPIRY_BUFFER_MS;
        if (!isExpired && !isExpiringSoon) {
            logger.debug("OAuth token is still valid", {
                expiresInMs: this.oauthToken.expiresAt - now,
            });
            return;
        }
        // Check if we have a refresh token
        if (!this.oauthToken.refreshToken) {
            if (isExpired) {
                throw new AuthenticationError("OAuth token expired and no refresh token available. Please re-authenticate.", this.providerName);
            }
            logger.warn("OAuth token expiring soon but no refresh token available", {
                expiresInMs: this.oauthToken.expiresAt - now,
            });
            return;
        }
        // Serialize concurrent refresh attempts — if a refresh is already in flight,
        // wait for it rather than issuing a duplicate request.
        if (this.refreshPromise) {
            await this.refreshPromise;
            return;
        }
        // Attempt to refresh the token using the correct Anthropic token endpoint.
        logger.info("Refreshing OAuth token", {
            isExpired,
            expiresInMs: this.oauthToken.expiresAt - now,
        });
        // Capture the token reference before entering the async IIFE;
        // the enclosing guards already verified both fields are non-null.
        const tokenRef = this.oauthToken;
        const refreshToken = tokenRef.refreshToken;
        this.refreshPromise = (async () => {
            const REFRESH_TIMEOUT_MS = 30_000;
            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), REFRESH_TIMEOUT_MS);
            // User-Agent is set to CLAUDE_CLI_USER_AGENT so the refresh request
            // matches what the official Claude CLI / CLIProxyAPI sends. Anthropic
            // gates parts of the OAuth flow on this UA (the same `client_id` is
            // rejected by `ANTHROPIC_TOKEN_URL` if the UA looks like a generic
            // SDK), so this is required for OAuth refresh to succeed — not a
            // cosmetic choice. If Anthropic ever publishes a separate UA for
            // third-party OAuth clients, switch to that. See `auth/anthropicOAuth.ts`
            // for the source of `CLAUDE_CLI_USER_AGENT` / `CLAUDE_CODE_CLIENT_ID`.
            const response = await fetch(ANTHROPIC_TOKEN_URL, {
                method: "POST",
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                    "User-Agent": CLAUDE_CLI_USER_AGENT,
                },
                body: new URLSearchParams({
                    grant_type: "refresh_token",
                    refresh_token: refreshToken,
                    client_id: CLAUDE_CODE_CLIENT_ID,
                }),
                signal: controller.signal,
            });
            clearTimeout(timeoutId);
            if (!response.ok) {
                const errorText = await response.text();
                throw new AuthenticationError(`Failed to refresh OAuth token: ${response.status} ${errorText}`, this.providerName);
            }
            const newToken = (await response.json());
            // Mutate the existing oauthToken object in-place so that the fetch wrapper
            // closure (which captured the object reference, not a copy) picks up the
            // new accessToken automatically on the next request.
            // Store expiresAt as milliseconds to match the format used by auth status/refresh.
            tokenRef.accessToken = newToken.access_token;
            tokenRef.refreshToken = newToken.refresh_token || tokenRef.refreshToken;
            tokenRef.expiresAt = newToken.expires_in
                ? Date.now() + newToken.expires_in * 1000
                : undefined;
            tokenRef.tokenType = newToken.token_type || "Bearer";
            const updatedToken = tokenRef;
            // Persist the refreshed token to disk atomically (tmp + rename) so
            // subsequent provider instances and the CLI pick up the new credentials.
            try {
                const credentialsDir = join(homedir(), ".neurolink");
                if (!existsSync(credentialsDir)) {
                    mkdirSync(credentialsDir, { recursive: true });
                }
                const credentialsPath = join(credentialsDir, "anthropic-credentials.json");
                const tmpPath = `${credentialsPath}.tmp`;
                const existingRaw = existsSync(credentialsPath)
                    ? JSON.parse(readFileSync(credentialsPath, "utf-8"))
                    : {};
                const updated = {
                    ...existingRaw,
                    type: "oauth",
                    oauth: updatedToken,
                    updatedAt: Date.now(),
                };
                writeFileSync(tmpPath, JSON.stringify(updated, null, 2), {
                    mode: 0o600,
                });
                renameSync(tmpPath, credentialsPath);
                logger.debug("Refreshed OAuth credentials persisted to disk");
            }
            catch (persistError) {
                // Non-fatal: in-memory token is already updated; next CLI start will
                // need a manual refresh but the current session will work.
                logger.warn("Failed to persist refreshed OAuth token to disk", {
                    error: persistError instanceof Error
                        ? persistError.message
                        : String(persistError),
                });
            }
            logger.info("OAuth token refreshed successfully", {
                hasNewRefreshToken: !!newToken.refresh_token,
                expiresIn: newToken.expires_in,
            });
        })();
        try {
            await this.refreshPromise;
        }
        catch (error) {
            if (error instanceof AuthenticationError) {
                throw error;
            }
            throw new AuthenticationError(`Failed to refresh OAuth token: ${error instanceof Error ? error.message : String(error)}`, this.providerName);
        }
        finally {
            this.refreshPromise = undefined;
        }
    }
    /**
     * Get the last response metadata including rate limit information.
     * @returns The last response metadata or null if no request has been made
     */
    getLastResponseMetadata() {
        return this.lastResponseMetadata;
    }
    /**
     * Update response metadata from a captured limit snapshot.
     *
     * Takes already-parsed rate-limit info rather than raw headers: parsing now
     * lives in `rateLimitCapture`, which is the only layer that sees the raw
     * response and understands both header families (unified subscription
     * windows and legacy per-tier counters).
     *
     * @param rateLimit - Parsed rate-limit figures
     * @param requestId - Optional Anthropic request ID
     * @param usageUpdate - Optional token counts to fold into usage tracking
     */
    updateResponseMetadata(rateLimit, requestId, usageUpdate) {
        this.lastResponseMetadata = {
            rateLimit,
            ...(requestId ? { requestId } : {}),
        };
        if (this.usageInfo) {
            this.usageInfo.requestCount++;
            this.usageInfo.messagesUsed++;
            this.usageInfo.lastRequestTimestamp = Date.now();
            // Update token usage if provided
            if (usageUpdate) {
                if (usageUpdate.inputTokens !== undefined) {
                    this.usageInfo.inputTokensUsed += usageUpdate.inputTokens;
                    this.usageInfo.tokensUsed += usageUpdate.inputTokens;
                }
                if (usageUpdate.outputTokens !== undefined) {
                    this.usageInfo.outputTokensUsed += usageUpdate.outputTokens;
                    this.usageInfo.tokensUsed += usageUpdate.outputTokens;
                }
            }
            // Update remaining quotas from rate limit headers
            if (rateLimit?.requestsRemaining !== undefined) {
                this.usageInfo.messagesRemaining = rateLimit.requestsRemaining;
            }
            if (rateLimit?.tokensRemaining !== undefined) {
                this.usageInfo.tokensRemaining = rateLimit.tokensRemaining;
            }
            // Calculate quota percentages
            if (rateLimit?.requestsLimit && rateLimit.requestsLimit > 0) {
                this.usageInfo.messageQuotaPercent = Math.round(((rateLimit.requestsLimit - (rateLimit.requestsRemaining ?? 0)) /
                    rateLimit.requestsLimit) *
                    100);
            }
            if (rateLimit?.tokensLimit && rateLimit.tokensLimit > 0) {
                this.usageInfo.tokenQuotaPercent = Math.round(((rateLimit.tokensLimit - (rateLimit.tokensRemaining ?? 0)) /
                    rateLimit.tokensLimit) *
                    100);
            }
            // Check for rate limiting
            if (rateLimit?.retryAfter !== undefined) {
                this.usageInfo.isRateLimited = true;
                this.usageInfo.rateLimitExpiresAt =
                    Date.now() + rateLimit.retryAfter * 1000;
            }
            else {
                this.usageInfo.isRateLimited = false;
                this.usageInfo.rateLimitExpiresAt = undefined;
            }
        }
        // Log rate limit warnings if approaching limits
        if (rateLimit?.requestsRemaining !== undefined) {
            if (rateLimit.requestsRemaining <= 5) {
                logger.warn("Approaching Anthropic request rate limit", {
                    remaining: rateLimit.requestsRemaining,
                    limit: rateLimit.requestsLimit,
                    reset: rateLimit.requestsReset,
                });
            }
        }
        if (rateLimit?.tokensRemaining !== undefined) {
            if (rateLimit.tokensLimit &&
                rateLimit.tokensRemaining < rateLimit.tokensLimit * 0.1) {
                logger.warn("Approaching Anthropic token rate limit", {
                    remaining: rateLimit.tokensRemaining,
                    limit: rateLimit.tokensLimit,
                    reset: rateLimit.tokensReset,
                });
            }
        }
    }
    getProviderName() {
        return "anthropic";
    }
    getDefaultModel() {
        return getDefaultAnthropicModel();
    }
    /**
     * Returns a V3-shaped delegating model whose `doGenerate` drives the
     * official Anthropic Messages API directly. BaseProvider's `generate()`
     * path (and its middleware wrapping) keeps working unchanged; the
     * streaming path bypasses this entirely via `executeStream`.
     */
    getAISDKModel() {
        const client = this.client;
        const providerName = this.providerName;
        const modelId = this.modelName || getDefaultAnthropicModel();
        const getTimeoutForOptions = (opts) => this.getTimeout((opts ?? {}));
        const refreshAuth = () => this.refreshAuthIfNeeded();
        // Structural AI-SDK model shape (`SageMakerAsLanguageModel`, the
        // sanctioned intermediate from src/lib/types/providers.ts): assigning the
        // literal to it keeps the compiler's member checks, and the intermediate
        // is single-assertable to the ai-package `LanguageModel` handle.
        const delegatingModel = {
            specificationVersion: "v3",
            provider: providerName,
            modelId,
            supportedUrls: {},
            doGenerate: async (options) => {
                await refreshAuth();
                const built = messagesToAnthropic(options.prompt);
                const messages = built.messages;
                // `let`: the additive structured-output path below appends the
                // final_result instruction to the system prompt.
                let system = built.system;
                let tools = (options.tools ?? [])
                    .filter((t) => t.type === "function")
                    .map((t) => {
                    // Honor a cache breakpoint the caller set on this tool. When no
                    // tool carries one, the last tool is marked further below.
                    const cc = cacheControlOf(t);
                    return {
                        name: t.name,
                        ...(t.description ? { description: t.description } : {}),
                        input_schema: (t.inputSchema ?? {
                            type: "object",
                            properties: {},
                        }),
                        ...(cc ? { cache_control: cc } : {}),
                    };
                });
                if (tools && tools.length === 0) {
                    tools = undefined;
                }
                // The value here has already been through `resolveToolChoice`
                // upstream in `executeNativeGenerate`, so this layer translates
                // rather than re-resolves — re-resolving would apply the auto/none
                // defaulting twice, and `toolsRecord`/`shouldUseTools` are not in
                // scope inside `getAISDKModel` in any case.
                //
                // Only the SHAPE varies, and reading `.type` off it was the bug:
                // a bare `"required"` has no `.type`, so the translator received
                // `undefined` and the field was dropped from the request entirely.
                // Normalisation now lives in the shared util rather than being
                // re-derived here, so this site cannot drift from its siblings again.
                const normalisedToolChoice = normalizeResolvedToolChoice(options.toolChoice);
                let toolChoice = normalisedToolChoice
                    ? toolChoiceToAnthropic(normalisedToolChoice)
                    : undefined;
                // JSON/structured output: Anthropic has no response_format — emulate
                // with a forced synthetic tool whose input IS the JSON payload (the
                // same object-tool strategy @ai-sdk/anthropic used).
                let jsonTool;
                if (options.responseFormat?.type === "json") {
                    jsonTool = options.responseFormat.name ?? "json";
                    const schema = (options.responseFormat.schema ?? {
                        type: "object",
                    });
                    tools = [
                        {
                            name: jsonTool,
                            description: options.responseFormat.description ??
                                "Respond by calling this tool with the answer as its input.",
                            input_schema: schema,
                        },
                    ];
                    if (modelSupportsForcedToolChoice(modelId)) {
                        toolChoice = { type: "tool", name: jsonTool };
                    }
                    else {
                        toolChoice = undefined;
                        system = appendToolAnswerInstruction(system, jsonTool);
                    }
                }
                toolChoice = relaxForcedToolChoice(modelId, toolChoice);
                // Additive structured output: when the caller wants a schema AND real
                // tools, the forced-json path above cannot be used (it replaces the
                // tools array), and structured output is excluded for this surface
                // by structuredOutputPolicy. The schema arrives instead on
                // `providerOptions.anthropic.finalResultSchema` and we APPEND a
                // `final_result` tool — tool_choice stays auto, so every real tool keeps working and
                // the model self-selects final_result when it is ready to answer.
                const finalResultSchema = options.providerOptions?.anthropic
                    ?.finalResultSchema;
                let finalResultActive = false;
                if (!jsonTool && finalResultSchema) {
                    const appended = appendFinalResultTool(tools, finalResultSchema);
                    tools = appended.tools;
                    finalResultActive = appended.applied;
                    if (appended.applied) {
                        system = appendFinalResultInstruction(system);
                    }
                }
                // Extended thinking passthrough (providerOptions.anthropic.thinking).
                const thinking = options.providerOptions?.anthropic?.thinking;
                // `disabled` is sent so a model that thinks by default (claude-sonnet-5
                // does, unasked) is told not to, but it fixes nothing about sampling: only a
                // block that turns thinking ON makes Anthropic reject temperature.
                const thinkingOn = thinking?.type === "enabled";
                // Close the stable prefix with a breakpoint on the last tool. The
                // stream path does the same, just before its own marker count.
                tools = withLastToolCacheBreakpoint(tools);
                // Prompt-cache parity with the native Vertex+Claude path: upstream
                // layers mark the stable prefix (system via MessageBuilder, and the
                // last tool just above) — the growing conversation
                // history has no breakpoint, so on every turn it falls after the
                // last marker and is re-billed as fresh input. Add rolling history
                // breakpoints in whatever budget remains under Anthropic's
                // four-marker ceiling; pre-existing markers are counted so the
                // request can never exceed the cap.
                const cacheMarkersUsed = countAnthropicCacheMarkers({
                    system,
                    tools,
                    messages: messages,
                });
                const cachedMessages = applyAnthropicHistoryCacheBreakpoints(messages, ANTHROPIC_MAX_CACHE_BREAKPOINTS - cacheMarkersUsed);
                // Registry-driven strip: Sonnet 5 / Opus 4.7+ / Fable 5 families
                // reject sampling params (also covers the retry paths — this params
                // object is what any rebuild spreads).
                const samplingParams = resolveSamplingParams("anthropic", modelId, {
                    ...(options.temperature !== undefined &&
                        options.temperature !== null
                        ? { temperature: options.temperature }
                        : {}),
                    ...(options.topP !== undefined && options.topP !== null
                        ? { topP: options.topP }
                        : {}),
                }, "anthropic.doGenerate");
                // Dropping a caller's explicit sampling parameters is exactly the
                // kind of silent discard this change fixes elsewhere, so say so.
                if (thinkingOn &&
                    (samplingParams.temperature !== undefined ||
                        samplingParams.topP !== undefined)) {
                    logger.debug("[anthropic] extended thinking is enabled, so temperature/top_p are omitted — Anthropic rejects any temperature but 1 while thinking is set");
                }
                const params = {
                    model: modelId,
                    messages: cachedMessages,
                    max_tokens: resolveClaudeMaxTokens(modelId, options.maxOutputTokens),
                    ...(system ? { system } : {}),
                    // Extended thinking fixes sampling: Anthropic rejects any
                    // temperature but 1 while `thinking` is set, and does not honour
                    // top_p there. The CLI always sends a default temperature, so
                    // forwarding it alongside thinking turns a call that used to work
                    // into a 400. Drop the sampling knobs for exactly those turns and
                    // let Anthropic's thinking defaults stand.
                    ...(!thinkingOn && samplingParams.temperature !== undefined
                        ? { temperature: samplingParams.temperature }
                        : {}),
                    ...(!thinkingOn && samplingParams.topP !== undefined
                        ? { top_p: samplingParams.topP }
                        : {}),
                    ...(options.stopSequences && options.stopSequences.length > 0
                        ? { stop_sequences: options.stopSequences }
                        : {}),
                    ...(tools ? { tools } : {}),
                    ...(toolChoice ? { tool_choice: toolChoice } : {}),
                    ...(thinking ? { thinking } : {}),
                };
                // The caller's resolved `timeout` reaches this layer only through
                // providerOptions.neurolink.timeoutMs (AI-SDK call options carry no
                // `timeout`; the old `options.timeout` read here never fired on V3).
                // An explicit value is a per-call contract: never floored, never
                // extended. Without one, the 60s anthropic default was tuned for the
                // old ~4096 max_tokens — now that the default ceiling is the model's
                // real max, raise the floor to 5 min when a large output budget is
                // in play. The abort signal stays the real bound.
                const neurolinkNs = options.providerOptions?.neurolink;
                const forwardedTimeoutMs = typeof neurolinkNs?.timeoutMs === "number" &&
                    Number.isFinite(neurolinkNs.timeoutMs) &&
                    neurolinkNs.timeoutMs > 0
                    ? neurolinkNs.timeoutMs
                    : undefined;
                const generateTimeoutMs = forwardedTimeoutMs !== undefined
                    ? forwardedTimeoutMs
                    : params.max_tokens > 8192
                        ? Math.max(getTimeoutForOptions(options), 300_000)
                        : getTimeoutForOptions(options);
                const timeoutController = createTimeoutController(generateTimeoutMs, providerName, "generate");
                const requestSignal = composeAbortSignals(options.abortSignal, timeoutController?.controller.signal);
                let response;
                try {
                    response = await client.messages.create(params, {
                        signal: requestSignal,
                    });
                }
                catch (error) {
                    // The Anthropic SDK collapses ANY fired signal into its generic
                    // APIUserAbortError ("Request was aborted."), discarding the
                    // signal's reason. When the abort came from one of NeuroLink's own
                    // timers (this per-call timer, or the turn-level one upstream),
                    // the TimeoutError reason is the honest identity — surface it.
                    const reason = requestSignal?.aborted
                        ? requestSignal.reason
                        : undefined;
                    if (reason instanceof TimeoutError) {
                        throw reason;
                    }
                    throw error;
                }
                finally {
                    timeoutController?.cleanup();
                }
                const content = [];
                let finalResultText;
                // Text emitted in forced-json mode, kept only as a fallback (see below).
                const jsonModeText = [];
                let jsonToolAnswered = false;
                for (const block of response.content) {
                    if (block.type === "thinking") {
                        // The signature rides along in providerOptions because Anthropic
                        // rejects a replayed thinking block without it, and the tool loop
                        // pushes this part straight back into the conversation.
                        content.push({
                            type: "reasoning",
                            text: block.thinking,
                            providerOptions: {
                                anthropic: { signature: block.signature },
                            },
                        });
                    }
                    else if (block.type === "redacted_thinking") {
                        // Encrypted reasoning: no readable text, but it must still be
                        // replayed verbatim or the turn is rejected.
                        content.push({
                            type: "reasoning",
                            text: "",
                            providerOptions: {
                                anthropic: {
                                    redactedData: block.data,
                                },
                            },
                        });
                    }
                    else if (block.type === "text") {
                        // In forced-json mode the payload arrives via the tool input, not
                        // text — pass text through only in normal mode.
                        if (jsonTool) {
                            jsonModeText.push(block.text);
                        }
                        else {
                            content.push({ type: "text", text: block.text });
                        }
                    }
                    else if (block.type === "tool_use") {
                        if (jsonTool && block.name === jsonTool) {
                            // Unwrap the synthetic tool call back into text JSON.
                            jsonToolAnswered = true;
                            content.push({
                                type: "text",
                                text: stringifyToolInput(block.input),
                            });
                        }
                        else if (finalResultActive &&
                            block.name === FINAL_RESULT_TOOL_NAME) {
                            // Internal pattern: never surfaced as a tool call. Its arguments
                            // ARE the structured answer.
                            finalResultText = stringifyToolInput(block.input);
                        }
                        else {
                            content.push({
                                type: "tool-call",
                                toolCallId: block.id,
                                toolName: block.name,
                                input: stringifyToolInput(block.input),
                            });
                        }
                    }
                }
                // Forced-json mode normally drops text blocks because the payload rides
                // in the synthetic tool's input. But when the response is cut short
                // (stop_reason "max_tokens") the tool call can be missing entirely, and
                // dropping the text would leave an EMPTY completion with nothing for
                // coerceJsonToSchema to recover. Fall back to the text so a partial
                // object can still be salvaged and flagged truncated.
                if (jsonTool && !jsonToolAnswered && jsonModeText.length > 0) {
                    content.push({ type: "text", text: jsonModeText.join("") });
                }
                // final_result is terminal — parity with the native Claude-on-Vertex
                // and Gemini loops, which break out of the tool loop the moment it
                // arrives. Reasoning blocks are kept; any prose preamble and any tool
                // calls issued alongside it are dropped so `text` is exactly the
                // structured payload and the AI-SDK loop stops here.
                if (finalResultText !== undefined) {
                    const reasoning = content.filter((part) => part.type === "reasoning");
                    content.length = 0;
                    content.push(...reasoning, { type: "text", text: finalResultText });
                    logger.debug("[Anthropic] Extracted structured output from final_result tool (generate)", { chars: finalResultText.length });
                }
                const cacheRead = response.usage.cache_read_input_tokens ?? 0;
                const cacheWrite = response.usage.cache_creation_input_tokens ?? 0;
                return {
                    content,
                    finishReason: {
                        // A final_result call ends the turn: the provider reports
                        // stop_reason "tool_use", but no tool call is surfaced, so
                        // reporting "tool-calls" would misread as a step-capped turn.
                        // `raw` still carries the provider's verbatim stop_reason.
                        //
                        // Only "tool-calls" is substituted, never every reason. The turn
                        // can also end at stop_reason "max_tokens" — the model was still
                        // writing the final_result arguments when the output ceiling hit,
                        // so the structured payload is cut mid-JSON. Reporting that as
                        // "stop" told the caller the response was complete: neurolink.ts's
                        // `finishReason === "length"` check never fired, so `jsonTruncated`
                        // stayed unset and no truncation warning was logged, leaving a
                        // partial object indistinguishable from a whole one. This mirrors
                        // the streaming path, which already narrows the same substitution
                        // to a "tool-calls" reason.
                        unified: finalResultText !== undefined &&
                            mapAnthropicStopReason(response.stop_reason) === "tool-calls"
                            ? "stop"
                            : mapAnthropicStopReason(response.stop_reason),
                        raw: response.stop_reason ?? "stop",
                    },
                    usage: {
                        inputTokens: {
                            total: response.usage.input_tokens + cacheRead + cacheWrite,
                            noCache: response.usage.input_tokens,
                            cacheRead,
                            cacheWrite,
                        },
                        outputTokens: {
                            total: response.usage.output_tokens,
                            text: response.usage.output_tokens,
                            reasoning: undefined,
                        },
                    },
                    warnings: [],
                    request: { body: params },
                    response: {
                        id: response.id,
                        modelId: response.model,
                        // Real response headers, captured by the fetch wrapper. This used
                        // to be a hardcoded `{}`, which silently discarded every
                        // rate-limit and quota header Anthropic returns.
                        headers: getCapturedResponseHeaders() ?? {},
                        body: response,
                    },
                };
            },
            doStream: () => {
                throw new Error(`${providerName}: doStream is not implemented on the delegating model. ` +
                    `NeuroLink streams through executeStream, reached via NeuroLink.stream() — ` +
                    `use that (the browser bundle exports the NeuroLink class) rather than ` +
                    `calling doStream on a model handle.`);
            },
        };
        return delegatingModel;
    }
    formatProviderError(error) {
        const rules = [
            {
                // Plan 02's mocked-contract work documented a pre-existing gap: the
                // Anthropic SDK formats auth failures as a bare "401 <msg>" string,
                // which the message-text regex alone does not catch — mirrors
                // Vertex's statusCode === 401 fallback in this same task.
                match: (ctx) => ctx.statusCode === 401 ||
                    /API_KEY_INVALID|Invalid API key/i.test(ctx.message),
                errorClass: AuthenticationError,
                message: "Invalid Anthropic API key. Please check your ANTHROPIC_API_KEY environment variable.",
            },
            {
                match: (ctx) => ctx.statusCode === 429 ||
                    /rate limit|too_many_requests|429/i.test(ctx.message),
                errorClass: RateLimitError,
                message: "Anthropic rate limit exceeded. Please try again later.",
            },
            {
                match: (ctx) => /ECONNRESET|ENOTFOUND|ECONNREFUSED|network|connection/i.test(ctx.message),
                errorClass: NetworkError,
                message: (ctx) => `Connection error: ${ctx.message}`,
            },
            {
                // Match the shared 5xx predicate: HTTP status or a bounded phrase,
                // rather than digits embedded in an unrelated value.
                match: (ctx) => (ctx.statusCode !== undefined &&
                    ctx.statusCode >= 500 &&
                    ctx.statusCode <= 599) ||
                    /server error|bad gateway|service unavailable|gateway timeout|\berror\b\D{0,12}\b5\d\d\b|\b5\d\d\b\D{0,12}\berror\b|\bstatus(?:\s*code)?\b\D{0,12}\b5\d\d\b/i.test(ctx.message),
                errorClass: ProviderError,
                message: (ctx) => `Server error: ${ctx.message}`,
            },
            {
                match: () => true,
                errorClass: ProviderError,
                message: (ctx) => `Anthropic error: ${ctx.message}`,
            },
        ];
        return classifyProviderError(error, rules, this.providerName, this.modelName);
    }
    // executeGenerate removed - BaseProvider handles all generation with tools
    /**
     * Override generate to refresh the OAuth token before delegating to
     * BaseProvider so that expired tokens are renewed automatically.
     */
    async generate(optionsOrPrompt, analysisSchema) {
        await this.refreshAuthIfNeeded();
        // Open a per-request capture scope around the whole turn. Scoping here
        // rather than on the instance is what makes it concurrency-safe: several
        // generate() calls can be in flight on one provider instance, and an
        // instance field would attribute one call's limits to another.
        const { result, snapshot } = await withLimitCapture(() => this.dispatchGenerate(optionsOrPrompt, analysisSchema));
        if (result && snapshot) {
            this.recordLimitSnapshot(snapshot);
            result.limits = snapshot;
            if (result.analytics) {
                result.analytics.limits = snapshot;
            }
        }
        return result;
    }
    /**
     * Text turns run natively; every other request kind still goes to
     * BaseProvider.generate().
     *
     * The loop runs over this provider's own delegating-model `doGenerate`,
     * which issues a NON-streaming `messages.create`. That matters: the streaming
     * loop adapter hardcodes `stream: true`, and an earlier attempt that routed
     * generate through it silently changed the wire.
     */
    async dispatchGenerate(optionsOrPrompt, analysisSchema) {
        await this.ensureModelLimits();
        const options = this.normalizeTextOptions(optionsOrPrompt);
        if (resolveRequestKind(options, this.modelName) !== "text") {
            return super.generate(options, analysisSchema);
        }
        this.validateOptions(options);
        const mergedTools = await this.getToolsForStream(options);
        const callerOwnsFallback = "disableInternalFallback" in options &&
            options.disableInternalFallback === true;
        // The native loop bypasses BaseProvider.executeGeneration, so the turn
        // budget has to be composed here or it stops existing for this provider.
        return this.runGenerateWithModelFallback(() => this.withTurnTimeout({ ...options, tools: mergedTools }, this.getDescriptorGenerateMs(), (timedOptions) => this.executeNativeGenerate(timedOptions)), callerOwnsFallback, options);
    }
    async executeNativeGenerate(options) {
        const startTime = Date.now();
        const modelId = this.modelName || getDefaultAnthropicModel();
        // Middleware must wrap the model here. The native loop bypasses
        // BaseProvider.executeGeneration, and with it the only place middleware was
        // ever applied — a probe showed a caller's wrapGenerate running zero times
        // on every native provider while their onFinish still fired, because
        // onFinish had been special-cased and nothing else had.
        const model = await this.getAISDKModelWithMiddleware(options);
        if (!hasNativeDoGenerate(model)) {
            throw this.handleProviderError(new Error("anthropic: model handle exposes no doGenerate()"));
        }
        const doGenerate = model.doGenerate.bind(model);
        const shouldUseTools = !options.disableTools && this.supportsTools();
        const toolsRecord = shouldUseTools
            ? options.tools || {}
            : {};
        const v3Tools = Object.entries(toolsRecord).map(([name, t]) => {
            const tool = t;
            return {
                type: "function",
                name,
                description: tool.description ?? "",
                inputSchema: (tool.inputSchema
                    ? convertZodToJsonSchema(tool.inputSchema)
                    : { type: "object", properties: {} }),
            };
        });
        const hasTools = v3Tools.length > 0;
        // Two structured-output routes, and doGenerate implements both. With no
        // tools it replaces the tool list with one forced json tool; with tools it
        // APPENDS final_result so the real tools stay callable. Picking the wrong
        // one is what dropped structuredData to null on the first attempt.
        const schemaJson = options.schema
            ? convertZodToJsonSchema(options.schema)
            : undefined;
        const responseFormat = schemaJson && !hasTools
            ? { type: "json", schema: schemaJson }
            : undefined;
        const anthropicNamespace = {};
        if (schemaJson && hasTools) {
            anthropicNamespace.finalResultSchema = schemaJson;
        }
        if (options.thinkingConfig?.enabled &&
            options.thinkingConfig.budgetTokens) {
            anthropicNamespace.thinking = {
                type: "enabled",
                budget_tokens: options.thinkingConfig.budgetTokens,
            };
        }
        else if (options.thinkingConfig?.type === "disabled") {
            // Without this the request carries no `thinking` field at all, which on
            // a model that thinks by default means "think" — the explicit
            // `type: "disabled"` the type already offered was silently dropped.
            anthropicNamespace.thinking = { type: "disabled" };
        }
        // The per-call `timeout` keeps its per-MODEL-CALL meaning once
        // `turnTimeoutMs` owns the whole-turn deadline, and it reaches the model
        // layer only through providerOptions.neurolink. Without it each step fell
        // back to the provider default.
        const mergedProviderOptions = {};
        if (Object.keys(anthropicNamespace).length > 0) {
            mergedProviderOptions.anthropic = anthropicNamespace;
        }
        if (typeof options.timeout === "number") {
            mergedProviderOptions.neurolink = { timeoutMs: options.timeout };
        }
        const providerOptions = Object.keys(mergedProviderOptions).length > 0
            ? mergedProviderOptions
            : undefined;
        const conversation = (await this.buildMessagesForStream(options));
        const toolExecutionSummaries = [];
        const loop = await runNativeGenerateLoop({
            doGenerate,
            ...createNativeGenerateGuard({
                provider: "anthropic",
                availableInputTokens: getAvailableInputTokens("anthropic", modelId, options.maxTokens ?? undefined),
                // Runs on every step of the generate loop, so a cyclic or
                // oversized tool schema must not throw the turn away. Shares the
                // estimator's own serializer, matching the OpenAI-compatible
                // native guard's wiring: its fallback over-reports, which makes
                // the guard fire rather than silently size the overhead at
                // nothing.
                getFixedOverheadTokens: () => estimateTokens(serializeForEstimate({
                    tools: v3Tools,
                    responseFormat,
                    providerOptions,
                }), "anthropic"),
            }),
            conversation,
            ...(hasTools ? { tools: v3Tools } : {}),
            toolsRecord,
            // A caller's toolChoice was dropped here while the streaming path and
            // the OpenAI-compatible native path both forwarded it, so
            // `toolChoice: "required"` and named-tool choices silently degraded to
            // Anthropic's default `auto` on generate().
            ...(hasTools && options.toolChoice
                ? { toolChoice: resolveToolChoice(options, toolsRecord, true) }
                : {}),
            ...(responseFormat ? { responseFormat } : {}),
            ...(providerOptions ? { providerOptions } : {}),
            maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
            ...(options.maxTokens ? { maxOutputTokens: options.maxTokens } : {}),
            ...(options.temperature !== undefined
                ? { temperature: options.temperature }
                : {}),
            ...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),
            ...(options.toolTimeoutMs !== undefined
                ? { toolTimeoutMs: options.toolTimeoutMs }
                : {}),
            runStep: (call) => withProviderRetry(call, trace.getActiveSpan() ?? undefined, "anthropic generate").catch((err) => {
                throw this.handleProviderError(err);
            }),
        }, toolExecutionSummaries);
        const enhanced = {
            content: loop.text,
            provider: this.providerName,
            model: modelId,
            finishReason: loop.finishReason,
            ...(loop.rawFinishReason
                ? { rawFinishReason: loop.rawFinishReason }
                : {}),
            ...(loop.reasoning ? { reasoning: loop.reasoning } : {}),
            usage: {
                input: loop.inputTokens,
                output: loop.outputTokens,
                // `inputTokens` is now the disjoint (uncached) figure, so cache
                // tokens must be added back in to keep `total` billing-complete —
                // they are additive on top of input under Anthropic's convention.
                total: loop.inputTokens +
                    loop.outputTokens +
                    loop.cacheReadTokens +
                    loop.cacheWriteTokens,
                ...(loop.cacheReadTokens
                    ? { cacheReadTokens: loop.cacheReadTokens }
                    : {}),
                ...(loop.cacheWriteTokens
                    ? { cacheCreationTokens: loop.cacheWriteTokens }
                    : {}),
            },
            responseTime: Date.now() - startTime,
            toolsUsed: loop.toolsUsed,
            toolCalls: toolCallsFromSummaries(toolExecutionSummaries),
            toolExecutions: resolveToolExecutionRecords(options, transformToolExecutions(toolExecutionSummaries)),
            enhancedWithTools: loop.toolsUsed.length > 0,
        };
        return this.finalizeNativeGenerate(enhanced, options, startTime);
    }
    /**
     * Fold a captured snapshot into the provider's usage bookkeeping and log it.
     *
     * `updateResponseMetadata` had no callers before this — the metadata it
     * maintains, and the public `getLastResponseMetadata()` / `getUsageInfo()`
     * that read it, were never populated by anything.
     */
    recordLimitSnapshot(snapshot) {
        this.updateResponseMetadata(snapshot.rateLimit, snapshot.requestId);
        setLimitSpanAttributes(snapshot);
        logClaudeLimitSnapshot(snapshot, this.modelName);
    }
    /**
     * The native stream loop below is the one implementation of the
     * `executionControl` contract: it arms the lifetime policy, hands the
     * per-request deadline and the terminal-event requirement to the loop
     * adapter, and runs the step-boundary callback through the engine. Every
     * other provider rejects the option rather than ignoring it.
     */
    supportsExecutionControl() {
        return true;
    }
    async executeStream(options, analysisSchema) {
        // The capture scope must outlive this call: the SSE loop keeps running in
        // the background after executeStream returns, and its per-step HTTP
        // requests are what carry the limit headers. AsyncLocalStorage.run
        // propagates into every continuation started inside, so the whole stream
        // lifetime shares one slot.
        return runInLimitCaptureScope(() => this.executeStreamInCaptureScope(options, analysisSchema));
    }
    async executeStreamInCaptureScope(options, _analysisSchema) {
        // Refresh OAuth token if needed before making any API request.
        await this.refreshAuthIfNeeded();
        this.validateStreamOptions(options);
        // Validated in BaseProvider.stream() before this point, so the shape here
        // is known good: requestTimeoutMs finite positive, lifetimeTimeoutMs null
        // or finite positive or absent.
        const control = options.executionControl;
        // Same split the generate path already enforces in
        // `BaseProvider.withTurnTimeout`: an explicit, valid `turnTimeoutMs` is
        // the caller's whole-turn contract and owns this timer. Without it the
        // native stream path armed only the provider's own `timeout`, so a caller
        // asking for a 40-minute turn of 5-minute calls was killed at the shorter
        // value — and a caller asking for a 200ms turn was not bounded at all.
        const hasValidTurnTimeout = typeof options.turnTimeoutMs === "number" &&
            Number.isFinite(options.turnTimeoutMs) &&
            options.turnTimeoutMs > 0;
        // `lifetimeTimeoutMs: null` means NO lifetime timer, which is why this
        // resolves to undefined rather than to a large number:
        // createTimeoutController arms nothing for a falsy duration, so the turn
        // ends up bounded by its per-request deadline and its step cap alone.
        //
        // Tested with `!== undefined` rather than `"lifetimeTimeoutMs" in control`:
        // `in` is true for `{ lifetimeTimeoutMs: undefined }`, which is the shape
        // any programmatic construction produces — an optional field spread, a
        // JSON round trip, a config object assembled field by field. Under `in`
        // that shape removed the turn's ceiling entirely while the caller had said
        // nothing at all about it, and TypeScript could not warn because the field
        // is `?: number | null`. An absent value means "no opinion", and no
        // opinion inherits the legacy handling below.
        const lifetimeTimeoutMs = control && control.lifetimeTimeoutMs !== undefined
            ? (control.lifetimeTimeoutMs ?? undefined)
            : hasValidTurnTimeout
                ? options.turnTimeoutMs
                : this.getTimeout(options);
        const timeoutController = createTimeoutController(lifetimeTimeoutMs, this.providerName, "stream");
        // Consumer-driven abort: fires when the async iterator is closed early
        // (caller breaks out of `for await`) so the background loop stops
        // reading SSE and running tools.
        const consumerAbortController = new AbortController();
        const abortSignal = mergeAbortSignals([
            options.abortSignal,
            timeoutController?.controller.signal,
            consumerAbortController.signal,
        ]).signal;
        let toolsRecord;
        let anthropicTools;
        let payload;
        let shouldUseTools;
        // True once the additive `final_result` tool is in the request — the
        // streaming twin of the doGenerate path above.
        let finalResultActive = false;
        try {
            // options.tools is pre-merged by BaseProvider.stream() with base tools
            // (MCP/built-in) + user-provided tools (RAG, etc.)
            shouldUseTools = !options.disableTools && this.supportsTools();
            toolsRecord = shouldUseTools
                ? options.tools || (await this.getAllTools())
                : {};
            anthropicTools = shouldUseTools
                ? toNativeToolDeclarations(toolsRecord, "input_schema")
                : undefined;
            // Build message array from options with multimodal support, then
            // convert to the Anthropic Messages payload (system + content blocks).
            const built = await this.buildMessagesForStream(options);
            payload = messagesToAnthropic(built);
            // Schema + tools: append final_result rather than pinning tool_choice to
            // a json tool, so the real tools stay callable for the whole turn.
            // Unlike generate, no plumbing is needed — this is a native loop, so the
            // caller's Zod/JSON schema is right here on the options.
            if (options.schema && anthropicTools && anthropicTools.length > 0) {
                const appended = appendFinalResultTool(anthropicTools, convertZodToJsonSchema(options.schema));
                anthropicTools = appended.tools;
                finalResultActive = appended.applied;
                if (appended.applied) {
                    payload.system = appendFinalResultInstruction(payload.system);
                }
            }
        }
        catch (setupErr) {
            timeoutController?.cleanup();
            throw this.handleProviderError(setupErr);
        }
        const modelId = this.modelName || getDefaultAnthropicModel();
        const anthropicToolChoice = shouldUseTools && anthropicTools && anthropicTools.length > 0
            ? relaxForcedToolChoice(modelId, toolChoiceToAnthropic(resolveToolChoice(options, toolsRecord, shouldUseTools)))
            : undefined;
        // Extended thinking: enabled when the caller supplies an explicit token
        // budget (mirrors the previous experimental_thinking gating). Thinking
        // deltas stream out on the `reasoning` chunk channel.
        const thinking = options.thinkingConfig?.enabled && options.thinkingConfig.budgetTokens
            ? {
                type: "enabled",
                budget_tokens: options.thinkingConfig.budgetTokens,
            }
            : options.thinkingConfig?.type === "disabled"
                ? { type: "disabled" }
                : undefined;
        // See the generate path: `disabled` is sent, but only ON constrains sampling.
        const thinkingOn = thinking?.type === "enabled";
        // Wrap the native stream in an OTel span to capture provider-level
        // latency and token usage (same span name as the pre-migration path so
        // dashboards stay continuous).
        const streamSpan = streamTracer.startSpan("neurolink.provider.streamText", {
            kind: SpanKind.CLIENT,
            attributes: {
                "gen_ai.system": "anthropic",
                "gen_ai.request.model": modelId,
            },
        });
        const maxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
        const emitter = this.neurolink?.getEventEmitter();
        const channel = createStreamChannel();
        const { push: pushChunk } = channel;
        const { usagePromise, finishPromise, resolveUsage, resolveFinish } = createDeferredAnalytics();
        usagePromise
            .then((usage) => {
            streamSpan.setAttribute("gen_ai.usage.input_tokens", usage.promptTokens || 0);
            streamSpan.setAttribute("gen_ai.usage.output_tokens", usage.completionTokens || 0);
            if (usage.cacheReadTokens) {
                streamSpan.setAttribute("gen_ai.usage.cached_input_tokens", usage.cacheReadTokens);
            }
            const cost = calculateCost(this.providerName, this.modelName, {
                input: usage.promptTokens || 0,
                output: usage.completionTokens || 0,
                total: usage.totalTokens || 0,
                ...(usage.cacheReadTokens
                    ? { cacheReadTokens: usage.cacheReadTokens }
                    : {}),
                ...(usage.cacheCreationTokens
                    ? { cacheCreationTokens: usage.cacheCreationTokens }
                    : {}),
            });
            if (cost && cost > 0) {
                streamSpan.setAttribute("neurolink.cost", cost);
            }
        })
            .catch(() => {
            // usage may never resolve if the stream is aborted before completion
        });
        finishPromise
            .then((reason) => {
            streamSpan.setAttribute("gen_ai.response.finish_reason", reason || "unknown");
            streamSpan.end();
        })
            .catch(() => {
            streamSpan.end();
        });
        let capturedProviderError;
        const client = this.client;
        const toolsUsed = [];
        const streamStartTime = Date.now();
        // Mutable-reference contract from StreamResult.metadata: created before
        // the loop and filled once the turn drains, because wrapper spreads
        // snapshot top-level result fields before the loop resolves. This is how
        // the Gemini/Vertex native paths already report their resolved outcome;
        // without it a consumer (e.g. the SDK's no-output fallback gate) cannot
        // tell a step-capped turn from a failed one.
        const turnMetadata = {};
        // Hoisted out of runLoop so the error path can resolve the usage
        // accumulated by steps that completed BEFORE the failure — those steps
        // were billed and must not be reported as zero.
        let totalInput = 0;
        let totalOutput = 0;
        let totalCacheRead = 0;
        let totalCacheWrite = 0;
        let lastStop = null;
        const buildDeferredUsage = () => ({
            promptTokens: totalInput,
            completionTokens: totalOutput,
            totalTokens: totalInput + totalCacheRead + totalCacheWrite + totalOutput,
            ...(totalCacheRead > 0 ? { cacheReadTokens: totalCacheRead } : {}),
            ...(totalCacheWrite > 0 ? { cacheCreationTokens: totalCacheWrite } : {}),
        });
        // Structured-output turns are delivered as ONE chunk, not incrementally:
        // a caller that passed a schema needs parseable JSON, and text deltas
        // emitted before the model calls final_result would prefix the payload
        // with prose and break every JSON.parse on the consumer side. Same
        // contract as the native Vertex loops. Non-schema streams are untouched
        // and stay fully incremental.
        let bufferedText = "";
        let finalResultText;
        /** System prompt + tool definitions: they ride outside `messages`. */
        const estimateAnthropicFixedOverhead = (system, tools) => {
            const text = (value) => {
                if (typeof value === "string") {
                    return value;
                }
                try {
                    return JSON.stringify(value) ?? "";
                }
                catch {
                    return "";
                }
            };
            return (estimateTokens(text(system), "anthropic") +
                estimateTokens(text(tools), "anthropic"));
        };
        const runLoop = async () => {
            // The provider's REAL prompt-token count for the previous step,
            // calibrating the guard's char-based estimate for free, paired with the
            // guard's own estimate for that same request — a ratio between counts of
            // two different payloads would be meaningless.
            let lastObservedPromptTokens;
            let lastSentEstimate;
            /**
             * Reclaim, as a PURE function of the conversation.
             *
             * The hand-rolled loop this replaces rebuilt in place
             * (`conversation.length = 0; conversation.push(...rebuilt)`) because it
             * owned the array. The engine owns it now and assigns what this
             * returns, so mutating here would corrupt a retried or reclaimed step.
             */
            const planReclaim = (conversation) => {
                const reclaim = planAnthropicLoopReclaim({
                    conversation,
                    availableInputTokens: getAvailableInputTokens("anthropic", modelId, options.maxTokens ?? undefined),
                    fixedOverheadTokens: estimateAnthropicFixedOverhead(payload.system, anthropicTools),
                    provider: "anthropic",
                    observedPromptTokens: lastObservedPromptTokens,
                    // Both halves of the calibration ratio must describe the same
                    // request: the tokens the provider reported, and this guard's own
                    // estimate for what was sent to earn them.
                    previousSentEstimate: lastSentEstimate,
                    onSentEstimate: (tokens) => {
                        lastSentEstimate = tokens;
                    },
                });
                if (!reclaim) {
                    return undefined;
                }
                // Dropping an assistant tool_use message together with its user
                // tool_result message is what keeps blocks paired.
                const dropSet = new Set(reclaim.drop);
                const truncateSet = new Set(reclaim.truncate);
                const rebuilt = [];
                for (let i = 0; i < conversation.length; i++) {
                    if (dropSet.has(i)) {
                        continue;
                    }
                    const message = conversation[i];
                    if (truncateSet.has(i) && Array.isArray(message.content)) {
                        rebuilt.push({
                            ...message,
                            content: message.content.map((block) => block.type === "tool_result"
                                ? {
                                    ...block,
                                    content: previewAnthropicToolResultText(typeof block.content === "string"
                                        ? block.content
                                        : (JSON.stringify(block.content) ?? "")),
                                }
                                : block),
                        });
                        continue;
                    }
                    rebuilt.push(message);
                }
                if (dropSet.size > 0) {
                    // Anthropic requires user/assistant alternation around tool blocks;
                    // the note is a user turn placed immediately before the first
                    // surviving assistant tool_use turn, which preserves it.
                    let noteIndex = rebuilt.findIndex((m) => Array.isArray(m.content) &&
                        m.content.some((b) => b.type === "tool_use" || b.type === "tool_result"));
                    if (noteIndex < 0) {
                        noteIndex = Math.min(1, rebuilt.length);
                    }
                    rebuilt.splice(noteIndex, 0, {
                        role: "user",
                        content: [{ type: "text", text: ANTHROPIC_ELISION_NOTE }],
                    });
                }
                return { conversation: rebuilt };
            };
            const buildParams = (conversation) => {
                // Mid-turn discovery sync: search_tools (tools.discovery) hydrates
                // new tools into toolsRecord between steps; Claude only calls tools
                // declared in the request, so advertise them now.
                if (anthropicTools) {
                    const declared = new Set(anthropicTools.map((t) => t.name));
                    const hydrated = Object.fromEntries(Object.entries(toolsRecord).filter(([name]) => !declared.has(name)));
                    if (Object.keys(hydrated).length > 0) {
                        const extra = toNativeToolDeclarations(hydrated, "input_schema");
                        if (extra && extra.length > 0) {
                            anthropicTools = [...anthropicTools, ...extra];
                        }
                    }
                }
                // Close the stable prefix with a breakpoint on the last tool, exactly
                // as the generate path does. This was missing: the marker was applied
                // only in doGenerate, so a streaming turn re-billed the entire tools
                // block every step and could not reuse the prefix generate() cached.
                const cachedTools = withLastToolCacheBreakpoint(anthropicTools);
                // Prompt-cache parity with the native Vertex+Claude path — rolling
                // history breakpoints, re-applied per step so the stable prefix stays
                // byte-identical while the breakpoint follows the growing tail.
                const cacheMarkersUsed = countAnthropicCacheMarkers({
                    system: payload.system,
                    tools: cachedTools,
                    messages: conversation,
                });
                const cachedConversation = applyAnthropicHistoryCacheBreakpoints(conversation, ANTHROPIC_MAX_CACHE_BREAKPOINTS - cacheMarkersUsed);
                const streamSamplingParams = resolveSamplingParams("anthropic", modelId, options.temperature !== undefined && options.temperature !== null
                    ? { temperature: options.temperature }
                    : {}, "anthropic.executeStream");
                if (thinkingOn && streamSamplingParams.temperature !== undefined) {
                    logger.debug("[anthropic] extended thinking is enabled, so temperature is omitted on the stream path — Anthropic rejects any temperature but 1 while thinking is set");
                }
                return {
                    model: modelId,
                    messages: cachedConversation,
                    max_tokens: resolveClaudeMaxTokens(modelId, options.maxTokens),
                    // No `stream: true` here: executeStep sets it when it calls
                    // messages.create, so declaring it made the caller assert a literal
                    // the adapter immediately overwrites — and forced this whole params
                    // object into the streaming variant for a field it does not own.
                    ...(payload.system ? { system: payload.system } : {}),
                    // Same constraint on the streaming path: a temperature alongside
                    // `thinking` is rejected outright.
                    ...(!thinkingOn && streamSamplingParams.temperature !== undefined
                        ? { temperature: streamSamplingParams.temperature }
                        : {}),
                    ...(cachedTools && cachedTools.length > 0
                        ? { tools: cachedTools }
                        : {}),
                    ...(anthropicToolChoice ? { tool_choice: anthropicToolChoice } : {}),
                    ...(thinking ? { thinking } : {}),
                };
            };
            const baseAdapter = createAnthropicLoopAdapter({
                client,
                maxSteps,
                toolsRecord,
                buildParams,
                planReclaim,
                // Both are opt-in with the control and absent without it, so a caller
                // that never passed executionControl sees the turn it saw before.
                ...(control
                    ? {
                        requestTimeoutMs: control.requestTimeoutMs,
                        requireTerminalEvent: true,
                    }
                    : {}),
                noteObservedPromptTokens: (tokens) => {
                    lastObservedPromptTokens = tokens;
                },
                ...(finalResultActive
                    ? {
                        finalResultToolName: FINAL_RESULT_TOOL_NAME,
                        onTerminalResult: (text) => {
                            finalResultText = text;
                            logger.debug("[Anthropic] Extracted structured output from final_result tool (stream)", { chars: text.length });
                        },
                    }
                    : {}),
            });
            // Wrapped rather than folded into the adapter: analytics emission and
            // tool-execution storage fire ONCE PER STEP in the loop this replaces,
            // and buildToolResultMessages is the only per-step hook — it receives
            // exactly that step's results. Doing this from the turn's final result
            // instead would batch every step's tools into one late write.
            const adapter = {
                ...baseAdapter,
                buildToolResultMessages: (conversation, stepResult, toolResults, engineStep) => {
                    for (const result of toolResults) {
                        toolsUsed.push(result.name);
                    }
                    const toolCallsForStorage = toolResults.map((result) => ({
                        type: "tool-call",
                        toolCallId: result.id,
                        toolName: result.name,
                        args: result.args,
                    }));
                    const toolResultsForStorage = toolResults.map((result) => result.error
                        ? {
                            type: "tool-result",
                            toolCallId: result.id,
                            toolName: result.name,
                            error: result.error,
                        }
                        : {
                            type: "tool-result",
                            toolCallId: result.id,
                            toolName: result.name,
                            result: result.output,
                        });
                    emitToolEndFromStepFinish(emitter, toolResultsForStorage.map((tr) => ({
                        toolName: tr.toolName,
                        result: "result" in tr ? tr.result : undefined,
                        error: "error" in tr ? tr.error : undefined,
                    })));
                    this.handleToolExecutionStorage(toolCallsForStorage, toolResultsForStorage, options, new Date()).catch((storageErr) => {
                        logger.warn("[AnthropicProvider] Failed to store tool executions", {
                            provider: this.providerName,
                            error: storageErr instanceof Error
                                ? storageErr.message
                                : String(storageErr),
                        });
                    });
                    return baseAdapter.buildToolResultMessages(conversation, stepResult, toolResults, engineStep);
                },
            };
            // Presented in the shape the engine dispatches through. The engine
            // supplies `{ toolCallId, abortSignal }`; the loop this replaces also
            // supplied `messages: []`, so it is kept — a tool that reads it would
            // otherwise see undefined where it used to see an empty array.
            const engineTools = {};
            for (const [name, tool] of Object.entries(toolsRecord)) {
                const execute = tool.execute;
                if (!execute) {
                    continue;
                }
                engineTools[name] = {
                    execute: async (args, opts) => {
                        const ctx = opts;
                        return execute(args, {
                            toolCallId: ctx.toolCallId ?? "",
                            ...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
                            messages: [],
                        });
                    },
                };
            }
            // The active span goes with it. Before this loop moved onto the shared
            // engine it called
            //   withProviderRetry(fn, trace.getActiveSpan() ?? undefined, label)
            // and that span is where gen_ai.provider.total_attempts is recorded.
            // The engine passed `undefined` in its place, so the attribute silently
            // stopped being emitted for every native Anthropic turn.
            const activeSpan = trace.getActiveSpan();
            // The caller's step-boundary callback, made finite and cancellable
            // before the engine ever sees it. The engine's contract is "already
            // bounded", and this is the layer that knows the budget, because the
            // budget is a field on the public option this layer validated.
            //
            // A callback that throws or outlives its budget declines the renewal
            // rather than failing the turn: the cap it did not raise still stands,
            // so the turn ends at the step limit the caller originally set. Failing
            // instead would let a flaky budget service kill work already done.
            const callerBeforeStep = control?.beforeStep;
            const beforeStep = callerBeforeStep
                ? async (context) => {
                    const budgetMs = control?.beforeStepTimeoutMs ?? DEFAULT_BEFORE_STEP_TIMEOUT_MS;
                    const callbackTimeout = createTimeoutController(budgetMs, this.providerName, "stream");
                    const composed = composeAbortSignalsScoped(context.signal, callbackTimeout?.controller.signal);
                    try {
                        // One timer, not two. `callbackTimeout` already aborts the
                        // composed signal at `budgetMs`, so the second `withTimeout`
                        // that used to sit here armed a duplicate timer for the same
                        // deadline. Racing the composed signal instead keeps the budget
                        // COMPULSORY — a callback that ignores its signal must not be
                        // able to park the turn at a step boundary, which is the one
                        // place no other timer is watching — while arming nothing new.
                        // It also ends the wait the moment the TURN is cancelled, which
                        // the old duplicate timer did not do.
                        return await raceWithAbort(Promise.resolve(callerBeforeStep({
                            ...context,
                            signal: composed.signal ?? context.signal,
                        })), composed.signal ?? context.signal);
                    }
                    catch (callbackError) {
                        logger.warn("[Anthropic] executionControl.beforeStep failed or timed out; the step cap stands", {
                            error: callbackError instanceof Error
                                ? callbackError.message
                                : String(callbackError),
                            stepsCompleted: context.stepsCompleted,
                        });
                        return undefined;
                    }
                    finally {
                        composed.dispose();
                        callbackTimeout?.cleanup();
                    }
                }
                : undefined;
            const { stream, resultPromise } = runAgenticLoop(adapter, payload.messages.slice(), {
                tools: engineTools,
                ...(abortSignal ? { abortSignal } : {}),
                ...(activeSpan ? { span: activeSpan } : {}),
                ...(beforeStep ? { beforeStep } : {}),
                // `engineTools` above only fixes the context object; it adds no
                // deadline. The engine's per-tool bound is therefore the only thing
                // watching a wedged tool on this path — which matters most when the
                // caller asked for no lifetime ceiling at all.
                ...(options.toolTimeoutMs !== undefined
                    ? { toolTimeoutMs: options.toolTimeoutMs }
                    : {}),
            });
            // Structured turns buffer their text rather than streaming it: a caller
            // that passed a schema needs parseable JSON, and deltas emitted before
            // the model calls final_result would prefix the payload with prose.
            // Reasoning is forwarded either way — it is not part of the payload.
            const pump = (async () => {
                for await (const chunk of stream) {
                    if (chunk.reasoning) {
                        pushChunk({ content: "", reasoning: chunk.reasoning });
                    }
                    if (chunk.content) {
                        if (finalResultActive) {
                            bufferedText += chunk.content;
                        }
                        else {
                            pushChunk({ content: chunk.content });
                        }
                    }
                }
            })();
            // `pump` is detached: it starts draining the engine's channel the moment
            // it is created, and `await pump` below is the only thing that adopts its
            // rejection. When resultPromise rejects, that line is never reached, so
            // pump's rejection stays unhandled — and an unhandled rejection
            // TERMINATES the consumer's process. Measured: a caller that correctly
            // try/catches a streaming error still died with ERR_UNHANDLED_REJECTION,
            // exit code 1, with no way to defend against it from outside this
            // library. The rejection carried the raw SDK error, distinct from the
            // formatted one the caller received, which is why the existing
            // `loopPromise.catch` guard below does not cover it.
            //
            // Every detached-drain site in the codebase now goes through
            // drainDetachedPump(), which adopts the rejection and logs the reason at
            // debug instead of discarding it silently.
            let result;
            try {
                result = await resultPromise;
            }
            catch (error) {
                await drainDetachedPump(pump, "Anthropic");
                throw error;
            }
            await pump;
            totalInput += result.usage.inputTokens;
            totalOutput += result.usage.outputTokens;
            totalCacheRead += result.usage.cacheReadTokens ?? 0;
            totalCacheWrite += result.usage.cacheWriteTokens ?? 0;
            lastStop = result.rawStopReason ?? lastStop;
            // An interrupted turn is not a stop. The Anthropic SDK's stream
            // iterator exits WITHOUT throwing when its request is aborted, so a
            // turn killed by the caller's signal or by the turn deadline drains
            // through here carrying no terminal event — and the ordinary path below
            // would report it with the same `finishReason` and the same resolved
            // stop reason as a model that answered and stopped.
            //
            // The merged signal's reason is what separates the two causes: NeuroLink's
            // own timers abort with a TimeoutError, and nothing else does.
            // Everything the completed steps produced is still reported — the text
            // was already pushed to the consumer, and the tokens were billed.
            if (result.aborted) {
                const reason = abortSignal?.aborted ? abortSignal.reason : undefined;
                turnMetadata.stopReason =
                    reason instanceof TimeoutError ? "time-limit" : "aborted";
                turnMetadata.finishReason = "other";
                if (result.rawStopReason) {
                    turnMetadata.rawFinishReason = result.rawStopReason;
                }
                resolveUsage(buildDeferredUsage());
                // Returns before the step-cap branch below: a turn aborted while the
                // model still wanted tools carries stop_reason "tool_use", which that
                // branch would read as the caller's own maxSteps bound.
                resolveFinish("other");
                return;
            }
            turnMetadata.finishReason = result.finishReason;
            if (result.rawStopReason) {
                turnMetadata.rawFinishReason = result.rawStopReason;
            }
            // "tool-calls" after a drained turn means the model still wanted tools
            // when the step budget ran out — the engine breaks at maxSteps, and a
            // model turn that finished normally maps to "stop". The one other
            // producer of stop_reason "tool_use" at turn end is a final_result
            // call (structured output), which `finalResultText` identifies, so it
            // must not read as a capped turn.
            if (result.finishReason === "tool-calls" &&
                finalResultText === undefined) {
                turnMetadata.stopReason = "step-cap";
            }
            resolveUsage(buildDeferredUsage());
            resolveFinish(lastStop ?? "stop");
        };
        const loopPromise = runLoop()
            // Parameter named `error` so the compiled `capturedProviderError = error`
            // assignment matches the regression-grep in test:context 6.14.
            .catch((error) => {
            capturedProviderError = error;
            logger.error("Anthropic: Stream error", {
                error: error instanceof Error ? error.message : String(error),
            });
            // Report whatever the completed steps accumulated — they were billed
            // — and unblock any consumer awaiting the usage promise.
            resolveUsage(buildDeferredUsage());
            resolveFinish("error");
            throw this.formatProviderError(error);
        })
            .finally(() => {
            // Deliver the buffered structured-output turn: `finalResultText` when
            // the model called final_result, otherwise the prose it produced
            // instead — never nothing, so a model that ignores the instruction
            // degrades to today's plain-text behaviour rather than an empty
            // stream. In `finally` so a turn that dies mid-loop still surfaces
            // the text it had already buffered, exactly as the unbuffered path
            // surfaces its partial deltas.
            if (finalResultActive) {
                const output = finalResultText ?? bufferedText;
                if (output.length > 0) {
                    pushChunk({ content: output });
                }
            }
            timeoutController?.cleanup();
            channel.close();
        });
        loopPromise.catch(() => {
            // Swallowed by design: the generator below surfaces loop errors after
            // draining the channel; this guard only prevents an unhandled-rejection
            // crash when the consumer abandons the stream early.
        });
        const providerName = this.providerName;
        const transformedStream = async function* () {
            let contentYielded = 0;
            try {
                for await (const chunk of channel.iterable) {
                    if ("content" in chunk &&
                        typeof chunk.content === "string" &&
                        chunk.content.length > 0) {
                        contentYielded++;
                    }
                    yield chunk;
                }
                // Surface any error the loop threw after draining the channel.
                await loopPromise;
                // No-output path: stream completed normally but yielded zero text.
                if (contentYielded === 0 && toolsUsed.length === 0) {
                    logger.warn(`${providerName}: Stream produced no output — emitting enriched sentinel`);
                    const fauxNoOutput = new NoOutputGeneratedError({
                        message: "Stream produced no output",
                    });
                    const sentinel = await buildNoOutputSentinel(fauxNoOutput, undefined, capturedProviderError);
                    stampNoOutputSpan(sentinel);
                    yield sentinel;
                }
            }
            catch (streamError) {
                const sentinel = await buildNoOutputSentinel(streamError, undefined, capturedProviderError);
                stampNoOutputSpan(sentinel);
                yield sentinel;
                if (!NoOutputGeneratedError.isInstance(streamError)) {
                    throw streamError;
                }
            }
            finally {
                if (!consumerAbortController.signal.aborted) {
                    consumerAbortController.abort();
                }
            }
        };
        return {
            stream: transformedStream(),
            provider: this.providerName,
            // Cell-3 identity fix: `this.modelName` is the raw, possibly-unset
            // constructor arg (undefined whenever a caller relies on the default
            // model). `modelId` (resolved above as
            // `this.modelName || getDefaultAnthropicModel()`) is what was actually
            // put on the wire for this turn — the same value `buildParams` uses to
            // build the request. Reporting the raw field previously let a
            // default-model turn's StreamResult claim an undefined/empty model.
            model: modelId,
            toolCalls: [],
            toolResults: [],
            metadata: turnMetadata,
            // Wire the deferred usage/finish promises into the analytics collector
            // (mirrors openaiChatCompletionsBase). Without this the loop computed a
            // fully correct aggregate that was consumed only by the OTel span —
            // stream consumers and session cost tracking saw no usage at all.
            // Chained off finishPromise so requestDuration reflects the DRAINED
            // stream, not the milliseconds it took to construct this result object.
            analytics: finishPromise.then(async () => {
                const analytics = await streamAnalyticsCollector.createAnalytics(this.providerName, modelId, {
                    textStream: (async function* () { })(),
                    usage: usagePromise,
                    finishReason: finishPromise,
                }, Date.now() - streamStartTime, {
                    requestId: options.requestId ??
                        `${this.providerName}-stream-${Date.now()}`,
                    streamingMode: true,
                });
                // Still inside the capture scope opened by executeStream, so this sees
                // the limits reported by the last upstream step of the stream.
                const snapshot = getCapturedLimitSnapshot();
                if (snapshot && analytics) {
                    this.recordLimitSnapshot(snapshot);
                    analytics.limits = snapshot;
                }
                return analytics;
            }),
        };
    }
    async isAvailable() {
        try {
            // Check OAuth token first
            const oauthToken = getOAuthToken();
            if (oauthToken) {
                return true;
            }
            // Fall back to API key check
            getAnthropicApiKey();
            return true;
        }
        catch {
            return false;
        }
    }
    getModel() {
        return this.getAISDKModel();
    }
}
// Re-export types and utilities for convenience
export { getModelCapabilities, getRecommendedModelForTier, isModelAvailableForTier, ModelAccessError, } from "../../models/anthropicModels.js";