# Claude (claude.ai) provider configuration, driven entirely by this file. # Selectors change when the web UI is updated: edit them here (reloaded live), # never in the Rust source. Validate with: llm-gateway selftest claude # # The values below come from the public claude.ai DOM observed in 2026 and from # published selector lists (May 2026). They are a starting point, not a # guarantee: run 'llm-gateway selftest claude' before relying on them. [provider] name = "claude" display_name = "Anthropic Claude" web_url = "https://claude.ai/new" new_conversation_url = "https://claude.ai/new" # Captures the web conversation id from the current URL (one capture group). conversation_url_pattern = '^https://claude\.ai/chat/([0-9a-zA-Z-]{8,})' default_models = ["claude-sonnet-4", "claude-opus-4", "claude-haiku-4"] enabled = true [selectors] # Claude uses ProseMirror, so the editor is a contenteditable div rather than a # textarea. The fallbacks cover the composer variants seen behind A/B tests. input_field = "div.ProseMirror[contenteditable='true'], div[contenteditable='true'][data-placeholder], [data-testid='composer-input']" send_button = "button[aria-label='Send Message'], button[aria-label='Send message'], button[data-testid='send-button'], button[type='submit']" # One node per assistant answer, in document order. response_container = "div.font-claude-response, div[data-is-streaming], div.font-claude-message" streaming_indicator = "button[aria-label='Stop response'], button[aria-label='Stop Response']" file_input = "input[type='file']" new_chat_button = "a[href='/new'], button[aria-label='New chat']" login_page_indicator = "a[href*='/login'], button[data-testid='login-button'], form[action*='login']" captcha_indicator = "iframe[src*='challenges.cloudflare.com'], iframe[title*='challenge'], div#challenge-form" error_banner = "[role='alert'], div[data-testid='error']" # Relative to the answer node: the hover toolbar button that copies the answer # as Markdown. copy_button = "button[aria-label='Copy'], button[data-testid='copy-button']" [login] url_patterns = ["/login", "/magic-link", "accounts.google.com"] # Only consulted when the prompt field is missing, so a chat page that happens # to mention "log in" is never misclassified. text_patterns = ["sign in", "log in", "continue with google"] [rate_limit] text_patterns = [ "message limit", "rate limit", "too many requests", "you've reached", "limite de messages", "trop de requêtes", "réessayez plus tard", ] [timeouts] page_load = 30 response_generation = 120 response_timeout = 180 [input] poll_interval_ms = 400 quiet_ms = 1500 inject_method = "auto" [options] supports_images = true supports_streaming = true supports_new_chat = true # Multi-account: each account is a separate browser profile, so several # subscriptions can be driven side by side. Declare them like this and address # them with model = "claude@work". # # [[accounts]] # id = "perso" # email = "bruno.charest@gmail.com" # default = true # # [[accounts]] # id = "work" # email = "brunocharest.bc@gmail.com"