"""Shared OpenAI-compatible client factory for the Modal endpoints. All three models (LFM2.5 text, Higgs TTS, Qwen3-VL OCR) speak the OpenAI HTTP API, so a single ``OpenAI`` client talks to each — only the base URL differs. The one project-specific concern lives here: **Modal cold starts.** An idle container answers the first request with a 502/503/504 while it boots (~30-60 s, see app.py COLD_START_HINT). The SDK already retries 5xx/connection errors with exponential backoff; we just raise ``max_retries`` and ``timeout`` well past the SDK defaults (2 retries) so a cold boot is absorbed rather than surfaced. This module is intentionally self-contained (no intra-package imports) so it can be imported both as ``openai_client`` (from the Space root) and ``space.openai_client`` (from the repo root) — the same dual-path trick the ``ocr`` package relies on. """ import os from openai import OpenAI TIMEOUT = 900 # seconds; headroom for a Modal cold start MAX_RETRIES = 15 # SDK backoff caps each sleep at ~8 s -> covers a ~1-2 min boot # Modal endpoints don't authenticate the OpenAI API key, but the SDK requires a # non-empty one. (Proxy auth, below, is a separate Modal-Key/Modal-Secret pair.) DUMMY_API_KEY = "EMPTY" # Modal proxy auth: when an endpoint is deployed with requires_proxy_auth=True, # every request must carry these two headers, sourced from a proxy auth token. MODAL_KEY_ENV = "MODAL_KEY" MODAL_SECRET_ENV = "MODAL_SECRET" def proxy_auth_headers() -> dict[str, str]: """Modal proxy-auth headers from the environment, or empty if not configured. Both halves are required, so a lone key/secret yields nothing — the client stays unauthenticated and keeps working against unprotected endpoints until both ``MODAL_KEY`` and ``MODAL_SECRET`` are set.""" key = os.environ.get(MODAL_KEY_ENV) secret = os.environ.get(MODAL_SECRET_ENV) if key and secret: return {"Modal-Key": key, "Modal-Secret": secret} return {} def make_client( base_url: str, *, timeout: float = TIMEOUT, max_retries: int = MAX_RETRIES, ) -> OpenAI: """Build an OpenAI client pointed at a Modal server. ``base_url`` may be the server root or already include the ``/v1`` suffix; both are accepted (and a trailing slash is fine). Modal proxy-auth headers are attached automatically when configured (see ``proxy_auth_headers``). """ base = base_url.rstrip("/") if not base.endswith("/v1"): base = f"{base}/v1" headers = proxy_auth_headers() return OpenAI( base_url=base, api_key=DUMMY_API_KEY, timeout=timeout, max_retries=max_retries, default_headers=headers or None, )