Spaces:
Sleeping
Sleeping
feat: sync proxy with upstream commits up to a4a0fb8 (model catalog refactor, desktop launcher, telemetry feature flags)
c817fe8 | """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 | |