Files
server-26/drb-c2-core/app/internal/ai_clients.py
T
Logan CusanoandClaude Opus 5.5 d485eae41a Transcription: config-driven OpenAI-compatible STT with OpenAI fallback (server-26#297, server-26#83)
STT_PROVIDER=auto picks Groq whisper-large-v3-turbo only when GROQ_API_KEY is
set (else OpenAI, unchanged); compat covers any OpenAI-compatible server via
STT_BASE_URL/STT_API_KEY. A failed primary falls back to OpenAI STT_MODEL on
the same downloaded audio; the fallback reports to its own ai_health tier so
a dead primary stays red. The non-OpenAI provider gets a token-budgeted
glossary prompt (area places, local_knowledge, vocabulary; never ten-codes),
guarded by a narrow prompt-echo check. A per-30s-window zlib compression
check (Whisper's 2.4) adds a noise gate that survives correction being off
(#83, partial). Call docs record stt_provider/model/audio_seconds/confidence.
The sweep hold-back counts the fallback attempt. gate_chatter added to
chatter_classifier (used by transcription to skip correction when
CHATTER_GATE_ENABLED). GROQ_API_KEY templated with default ''.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:28:30 -04:00

63 lines
2.2 KiB
Python

"""
One place every OpenAI client is built, so none of them runs on SDK-default
timeouts (server-26#153).
The OpenAI SDK's defaults are a 600s read timeout and 2 retries, which it also
applies to timeouts: one hung Whisper request could hold a call's pipeline for
half an hour, twice over with stt_retry_on_degenerate. That unbounded tail is
what recorrelation_sweep.py's MIN_MINUTES_SINCE_PIPELINE_START had to guess
against. With these settings it is derived instead (see
recorrelation_sweep.pipeline_worst_case_seconds).
A timeout raises openai.APITimeoutError, an ordinary Exception, so each call
site's existing failure path handles it unchanged: ai_health.classify() reads
"Request timed out." as transient.
The Gemini client is built in app/internal/gemini.py with the same intent.
"""
from typing import Optional
from app.config import settings
def _timeout(timeout: Optional[float]) -> float:
return settings.openai_timeout_seconds if timeout is None else timeout
def openai_client(timeout: Optional[float] = None):
"""Synchronous OpenAI client. `timeout` overrides openai_timeout_seconds (STT passes its own)."""
from openai import OpenAI # lazy, like every call site before this module
return OpenAI(
api_key=settings.openai_api_key,
timeout=_timeout(timeout),
max_retries=settings.openai_max_retries,
)
def stt_client(api_key: Optional[str], base_url: Optional[str] = None):
"""
Synchronous client for an OpenAI-compatible transcription endpoint
(server-26#297) -- OpenAI itself, Groq, or a self-hosted server. Same
timeout/retry limits as every other client here, with STT's own timeout.
"""
from openai import OpenAI
kwargs = {}
if base_url:
kwargs["base_url"] = base_url
return OpenAI(
api_key=api_key,
timeout=settings.stt_timeout_seconds,
max_retries=settings.openai_max_retries,
**kwargs,
)
def async_openai_client(timeout: Optional[float] = None):
"""Async OpenAI client, same limits as openai_client()."""
from openai import AsyncOpenAI
return AsyncOpenAI(
api_key=settings.openai_api_key,
timeout=_timeout(timeout),
max_retries=settings.openai_max_retries,
)