Spaces:
Running
Running
File size: 5,539 Bytes
2415446 61bb677 0a54372 2415446 c817fe8 2415446 61bb677 2415446 61bb677 2415446 61bb677 2415446 61bb677 2415446 61bb677 2415446 0a54372 2415446 0a54372 2415446 c817fe8 0a54372 2415446 c817fe8 2415446 61bb677 2415446 61bb677 2415446 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | """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
|