{"record":{"id":"8fbe081f65abe254","repo":"BerriAI/litellm","slug":"advisor-tool-definition-sets-api-base-without-a","errorCode":null,"errorMessage":"advisor tool definition sets 'api_base' without 'api_key'. A caller-supplied api_base is only honored alongside a caller-supplied api_key, so the proxy's own credentials are never sent to a caller-chosen destination.","messagePattern":"advisor tool definition sets 'api_base' without 'api_key'\\. A caller-supplied api_base is only honored alongside a caller-supplied api_key, so the proxy's own credentials are never sent to a caller-chosen destination\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py","lineNumber":214,"sourceCode":"    the caller-chosen ``api_base``. A caller-supplied ``api_base`` is also\n    required to be https with TLS verification on, and SSRF-validated so it\n    can't target a private/internal/cloud-metadata address, mirroring\n    ``proxy.auth.auth_utils.check_complete_credentials``. https with TLS\n    verification is required because ``validate_url`` only rewrites the\n    connection to a DNS-pinned IP for http, or for https with\n    ``litellm.ssl_verify`` disabled; otherwise it returns the URL unchanged\n    and relies on certificate validation to block DNS rebinding, so this\n    closes the same gap without threading the pinned URL through the whole\n    ``anthropic_messages()`` call chain.\n    \"\"\"\n    if not _allow_client_side_advisor_credentials():\n        return None, None\n    api_key: Final[str | None] = advisor_tool.get(\"api_key\")\n    api_base: Final[str | None] = advisor_tool.get(\"api_base\")\n    if api_base is None:\n        return api_key, None\n    if not api_key:\n        raise ValueError(\n            \"advisor tool definition sets 'api_base' without 'api_key'. A \"\n            \"caller-supplied api_base is only honored alongside a \"\n            \"caller-supplied api_key, so the proxy's own credentials are \"\n            \"never sent to a caller-chosen destination.\"\n        )\n    if not api_base.startswith(\"https://\"):\n        raise ValueError(f\"advisor tool definition sets 'api_base'={api_base!r}, which must use the https scheme.\")\n    if getattr(litellm, \"ssl_verify\", True) is False:\n        raise ValueError(\n            \"advisor tool definition sets 'api_base' but the proxy has TLS verification \"\n            \"disabled (litellm.ssl_verify=False), so a caller-supplied api_base can't be \"\n            \"safely validated against DNS rebinding.\"\n        )\n    if getattr(litellm, \"user_url_validation\", True):\n        validate_url(api_base)\n    return api_key, api_base\n\n","sourceCodeStart":196,"sourceCodeEnd":232,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py#L196-L232","documentation":"Security guard in the advisor credential resolver. A caller may point the advisor at a custom api_base only when the caller also supplies the api_key for that destination. Supplying api_base without api_key is rejected so the proxy never attaches its own credentials to a caller-chosen URL (credential-exfiltration / SSRF-to-credential-theft protection). Only active when client-side advisor credentials are allowed.","triggerScenarios":"Sending an advisor tool with {\"type\": \"advisor\", \"model\": ..., \"api_base\": \"https://my-llm.example.com\"} but no 'api_key' key (or api_key=None/\"\"). The resolver sees api_base set, api_key falsy, and raises.","commonSituations":"Teams pointing the advisor at an internal vLLM endpoint but expecting the proxy's key to be reused; half-finished config where the base was migrated to the tool definition but the key was left in an env var; attempts (accidental or malicious) to make the gateway forward its Anthropic key to an attacker-controlled host.","solutions":["Supply both fields together: api_key and api_base on the advisor tool definition.","If the destination should use the gateway's configured credentials, remove 'api_base' and configure that model in the proxy's model_list instead.","If you truly need gateway credentials at a custom base, register that model+credentials server-side rather than client-side."],"exampleFix":"# before\ntools = [{\"type\": \"advisor\", \"model\": \"claude-haiku-4-5\", \"api_base\": \"https://internal.example.com\"}]\n\n# after\ntools = [{\n    \"type\": \"advisor\",\n    \"model\": \"claude-haiku-4-5\",\n    \"api_base\": \"https://internal.example.com\",\n    \"api_key\": \"sk-internal-key\",\n}]","handlingStrategy":"validation","validationCode":"def validate_advisor_credentials(advisor_tool: dict) -> None:\n    api_base = advisor_tool.get(\"api_base\")\n    api_key = advisor_tool.get(\"api_key\")\n    if api_base is not None and not api_key:\n        raise ValueError(\"advisor api_base requires a matching caller-supplied api_key\")","typeGuard":"def advisor_credentials_are_paired(tool: dict) -> bool:\n    api_base = tool.get(\"api_base\")\n    api_key = tool.get(\"api_key\")\n    return api_base is None or bool(api_key)","tryCatchPattern":"try:\n    resp = litellm.anthropic_messages(tools=tools, ...)\nexcept ValueError as e:\n    if \"api_base\" in str(e) and \"api_key\" in str(e):\n        log_security_event(\"advisor_credential_mismatch\", str(e))\n        return http_error(400, \"advisor api_base must be paired with api_key\")\n    raise","preventionTips":["Treat api_key and api_base as an atomic pair in advisor tool config.","Prefer server-side model registration over caller-supplied bases when using gateway credentials.","Never expect the proxy's own key to follow a caller-chosen destination."],"tags":["anthropic","advisor","security","ssrf","credentials"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}