fix(cloudflare): guard missing account id and migrate legacy /ai/run base

Centralize the OpenAI-compatible api_base default in get_complete_url so it
is built in one place instead of being duplicated in main.py. When neither
api_base nor CLOUDFLARE_ACCOUNT_ID is set the call now fails fast with a clear
error rather than sending a request to a URL containing the literal 'None'.

An api_base still pinned to the legacy Workers AI '/ai/run' path is rewritten
to the '/ai/v1' OpenAI-compatible endpoint with a deprecation warning, so
users who hardcoded the previous default migrate gracefully instead of hitting
a silently broken '/ai/run/chat/completions' URL.
This commit is contained in:
mateo-berri 2026-06-23 02:52:30 +00:00
parent 1815cc9bee
commit 93add6a43b
No known key found for this signature in database
3 changed files with 52 additions and 13 deletions

View file

@ -2,6 +2,7 @@ from typing import List, Optional, Union
import httpx
from litellm._logging import verbose_logger
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
from litellm.secret_managers.main import get_secret_str
@ -32,13 +33,8 @@ class CloudflareChatConfig(OpenAIGPTConfig):
litellm_params: dict,
stream: Optional[bool] = None,
) -> str:
if api_base is None:
account_id = get_secret_str("CLOUDFLARE_ACCOUNT_ID")
api_base = (
f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1"
)
return super().get_complete_url(
api_base=api_base,
api_base=self._resolve_api_base(api_base),
api_key=api_key,
model=model,
optional_params=optional_params,
@ -46,6 +42,23 @@ class CloudflareChatConfig(OpenAIGPTConfig):
stream=stream,
)
@staticmethod
def _resolve_api_base(api_base: Optional[str]) -> str:
if api_base is None:
account_id = get_secret_str("CLOUDFLARE_ACCOUNT_ID")
if account_id is None:
raise ValueError(
"Missing CLOUDFLARE_ACCOUNT_ID - set CLOUDFLARE_ACCOUNT_ID in the environment or pass api_base explicitly"
)
return f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1"
trimmed = api_base.rstrip("/")
if trimmed.endswith("/ai/run"):
verbose_logger.warning(
"Cloudflare api_base ending in '/ai/run' is the legacy Workers AI path and no longer serves OpenAI-compatible requests; rewriting to the '/ai/v1' endpoint"
)
return f"{trimmed[: -len('/ai/run')]}/ai/v1"
return api_base
def validate_environment(
self,
headers: dict,

View file

@ -4236,13 +4236,7 @@ def completion( # type: ignore
or litellm.api_key
or get_secret("CLOUDFLARE_API_KEY")
)
account_id = get_secret("CLOUDFLARE_ACCOUNT_ID")
api_base = (
api_base
or litellm.api_base
or get_secret("CLOUDFLARE_API_BASE")
or f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1"
)
api_base = api_base or litellm.api_base or get_secret("CLOUDFLARE_API_BASE")
custom_prompt_dict = custom_prompt_dict or litellm.custom_prompt_dict
response = base_llm_http_handler.completion(

View file

@ -68,6 +68,38 @@ def test_get_complete_url_is_idempotent_for_full_base():
)
def test_get_complete_url_raises_when_account_id_and_base_missing(monkeypatch):
monkeypatch.delenv("CLOUDFLARE_ACCOUNT_ID", raising=False)
config = CloudflareChatConfig()
with pytest.raises(ValueError, match="Missing CLOUDFLARE_ACCOUNT_ID"):
config.get_complete_url(
api_base=None,
api_key="cf-key",
model="@cf/meta/llama-2-7b-chat-int8",
optional_params={},
litellm_params={},
)
def test_get_complete_url_migrates_legacy_ai_run_base():
config = CloudflareChatConfig()
url = config.get_complete_url(
api_base="https://api.cloudflare.com/client/v4/accounts/acct/ai/run/",
api_key="cf-key",
model="@cf/meta/llama-2-7b-chat-int8",
optional_params={},
litellm_params={},
)
assert (
url
== "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1/chat/completions"
)
assert "/ai/run" not in url
def test_transform_request_passes_tools_through_in_openai_format():
config = CloudflareChatConfig()
tools = [