| """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 |
| MAX_RETRIES = 15 |
|
|
| |
| |
| DUMMY_API_KEY = "EMPTY" |
|
|
| |
| |
| 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, |
| ) |
|
|