dheraingoud's picture
feat: sync proxy with upstream commits up to a4a0fb8 (model catalog refactor, desktop launcher, telemetry feature flags)
c817fe8
Raw
History Blame Contribute Delete
5.54 kB
"""Cloudflare Workers AI provider using OpenAI-compatible chat completions."""
from collections.abc import Iterator, Mapping
from dataclasses import replace
from typing import Any
from urllib.parse import quote
import httpx
from free_claude_code.application.errors import ApplicationUnavailableError
from free_claude_code.application.model_metadata import ProviderModelInfo
from free_claude_code.config.provider_catalog import CLOUDFLARE_AI_REST_ROOT
from free_claude_code.core.anthropic import ReasoningReplayMode
from free_claude_code.providers.admission import ProviderAdmissionController
from free_claude_code.providers.base import ProviderConfig
from free_claude_code.providers.http import maybe_await_aclose
from free_claude_code.providers.model_listing import (
ModelListResponseError,
extract_openai_model_infos,
)
from free_claude_code.providers.openai_chat import (
ChatTemplateReasoning,
OpenAIChatProfile,
OpenAIChatProvider,
OpenAIChatRequestPolicy,
validate_extra_body_does_not_override_canonical_fields,
)
_REQUEST_POLICY = OpenAIChatRequestPolicy(
provider_name="CLOUDFLARE",
reasoning_replay=ReasoningReplayMode.REASONING_CONTENT,
include_extra_body=True,
extra_body_validator=validate_extra_body_does_not_override_canonical_fields,
max_tokens_field="max_completion_tokens",
)
_PROFILE = OpenAIChatProfile(_REQUEST_POLICY, ChatTemplateReasoning())
def cloudflare_ai_base_url(api_root: str | None, account_id: str) -> str:
"""Return the account-scoped Cloudflare Workers AI OpenAI-compatible base URL."""
return f"{_cloudflare_account_api_url(api_root, account_id)}/ai/v1"
def _cloudflare_model_search_url(api_root: str | None, account_id: str) -> str:
"""Return the Cloudflare account model-search endpoint URL."""
return f"{_cloudflare_account_api_url(api_root, account_id)}/ai/models/search"
def _cloudflare_account_api_url(api_root: str | None, account_id: str) -> str:
"""Return the account-scoped Cloudflare API root URL."""
stripped_account = account_id.strip()
if not stripped_account:
raise ApplicationUnavailableError(
"CLOUDFLARE_ACCOUNT_ID is not set. Add it to your .env file."
)
root = (api_root or CLOUDFLARE_AI_REST_ROOT).rstrip("/")
encoded_account = quote(stripped_account, safe="")
return f"{root}/accounts/{encoded_account}"
class CloudflareProvider(OpenAIChatProvider):
"""Cloudflare Workers AI OpenAI-compatible chat provider."""
def __init__(
self,
config: ProviderConfig,
*,
account_id: str,
admission: ProviderAdmissionController,
):
base_url = cloudflare_ai_base_url(config.base_url, account_id)
self._model_search_url = _cloudflare_model_search_url(
config.base_url, account_id
)
self._model_list_client = httpx.AsyncClient(
proxy=config.proxy or None,
timeout=httpx.Timeout(
config.http_read_timeout,
connect=config.http_connect_timeout,
read=config.http_read_timeout,
write=config.http_write_timeout,
),
)
super().__init__(
replace(config, base_url=base_url),
profile=_PROFILE,
admission=admission,
)
async def cleanup(self) -> None:
"""Release provider client resources."""
await super().cleanup()
await self._model_list_client.aclose()
async def list_model_infos(self) -> frozenset[ProviderModelInfo]:
"""Return Cloudflare Workers AI metadata from account model search."""
async def request() -> httpx.Response:
response = await self._model_list_client.get(
self._model_search_url,
params={"format": "openrouter"},
headers=self._model_list_headers(),
)
try:
response.raise_for_status()
except Exception:
await maybe_await_aclose(response)
raise
return response
response = await self._admission.run_with_retry(request)
try:
try:
payload = response.json()
except ValueError as exc:
raise ModelListResponseError(
"CLOUDFLARE model-list response is malformed: invalid JSON"
) from exc
return extract_openai_model_infos(payload, provider_name="CLOUDFLARE")
finally:
await maybe_await_aclose(response)
def _handle_extra_reasoning(
self, delta: Any, ledger: Any, *, output_reasoning: bool
) -> Iterator[str]:
"""Map Cloudflare's ``reasoning`` delta field to Anthropic thinking."""
reasoning = _cloudflare_reasoning(delta)
if not output_reasoning or not reasoning:
return
yield from ledger.ensure_thinking_block()
yield ledger.emit_thinking_delta(reasoning)
def _model_list_headers(self) -> dict[str, str]:
return {"Authorization": f"Bearer {self._api_key}"}
def _cloudflare_reasoning(delta: Any) -> str | None:
reasoning = getattr(delta, "reasoning", None)
if isinstance(reasoning, str) and reasoning:
return reasoning
model_extra = getattr(delta, "model_extra", None)
if isinstance(model_extra, Mapping):
reasoning = model_extra.get("reasoning")
if isinstance(reasoning, str) and reasoning:
return reasoning
return None