GigaChat LLM provider + GigaAM (LiteLLM) dev ASR#14
Conversation
- Replace ASR_URL/ASR_MODEL (Mistral) with LITELLM_URL/LITELLM_MODEL (GigaAM via LiteLLM proxy)
- Add env_file (.env.dev) for secrets instead of inline ${VAR:-} which overrides with empty host values
- Remove MISTRAL_API_KEY, LLM_API_KEY, JWT_SECRET, API_KEY from environment section (provided by .env.dev)
Co-authored-by: Saint <saintdemon1337@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Switch summary/insights/chat/live-hints/autoflow from the OpenAI-compatible
client to GigaChat via create_llm_client(). Replacing every direct LLMClient()
instantiation fixes the live-mode 401 (stale OpenAI LLM_API_KEY) and adds an
explicit GigaChat SDK timeout (GIGACHAT_TIMEOUT, default 60s) so the initial
TLS+OAuth handshake no longer times out ("Failed to fetch" on summary).
Also move GIGACHAT_API_KEY out of docker-compose.dev.yaml into the gitignored
.env.dev to keep the credential out of version control.
Co-authored-by: Saint <saintdemon1337@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
- LLMCascade._build_client and live_hints router use LLMClient (Mistral via LLM_BASE_URL/LLM_MODEL/LLM_API_KEY) for the Live Advisor, not GigaChat. Transcription stays on GigaAM; summary/insights/chat stay on GigaChat. - New GET /api/live-hints/info: introspects active providers (ASR / advisor / analysis) so the frontend can show a non-intrusive model badge. Co-authored-by: Saint <saintdemon1337@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
Добавил ещё один коммит в эту ветку: Live Advisor back to Mistral (OpenAI-compatible) + /api/live-hints/info (#6). Live Advisor осознанно использует OpenAI-совместимый \LLMClient\ (Mistral) вместо GigaChat; транскрибация остаётся на GigaAM, а summary/insights/chat — на GigaChat. Также добавлен \GET /api/live-hints/info\ для интроспекции активных провайдеров (нужен фронту для бейджа модели). |
…closed) (#7) The transcriber drives ASR via repeated asyncio.run() calls, each creating and closing a fresh event loop. LiteLLMASRClient created its httpx.AsyncClient in __init__, binding to the first loop -> 'Event loop is closed' on the next call, silently falling back to Mistral (whose intermittent SSL errors surfaced as 502 on /api/transcribe). Now each request opens its own client bound to the current loop; close() is a no-op. GigaAM (LiteLLM) works directly without fallback. Co-authored-by: Saint <saintdemon1337@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a pluggable LLM provider layer (OpenAI-compatible vs GigaChat) and updates dev ASR configuration to support GigaAM via a LiteLLM proxy, along with UI introspection for active ASR/LLM models.
Changes:
- Introduces
GigaChatLLMClientpluscreate_llm_client()factory and routes several API paths through it. - Adds
/api/live-hints/infoto surface currently active ASR + LLM models/providers for the UI. - Updates dev tooling/config: LiteLLM-based ASR wiring,
gigachatdev dependency, and gitignore for.env.dev.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
gigaam_transcriber/summarizer.py |
Adds GigaChat client implementation and factory selection logic. |
gigaam_transcriber/chat.py |
Switches default chat client creation path to the factory. |
gigaam_transcriber/llm_cascade.py |
Refactors Live Advisor client construction into a helper method. |
gigaam_transcriber/litellm_client.py |
Uses per-request httpx.AsyncClient to avoid cross-event-loop reuse issues. |
routers/analysis.py |
Uses the factory for the analysis router’s global llm_client. |
routers/saved_transcriptions.py |
Uses the factory for LLM client initialization. |
routers/meeting_prep.py |
Uses the factory for LLM client initialization. |
routers/autoflow.py |
Uses the factory and gates model override to OpenAI-compatible client. |
routers/live_hints.py |
Adds /info endpoint + provider labeling; clarifies Live Advisor still uses OpenAI-compatible client. |
docker-compose.dev.yaml |
Switches dev env to .env.dev + LiteLLM/GigaChat defaults. |
.gitignore |
Adds .env.dev to keep dev secrets out of VCS. |
requirements.dev.txt |
Adds gigachat dependency for dev installs. |
Comments suppressed due to low confidence (3)
routers/saved_transcriptions.py:31
- After introducing
create_llm_client(),llm_client.config.api_keymay be coming fromGIGACHAT_API_KEY, but the error message still saysLLM_API_KEY not configured, which is misleading for GigaChat users. Update the message to be provider-agnostic and mention both env vars.
def _ensure_llm() -> None:
if not llm_client.config.api_key:
raise HTTPException(status_code=503, detail="LLM_API_KEY not configured")
routers/meeting_prep.py:23
- After introducing
create_llm_client(),llm_client.config.api_keymay be coming fromGIGACHAT_API_KEY, but the error message still saysLLM_API_KEY not configured, which is misleading for GigaChat users. Update the message to be provider-agnostic and mention both env vars.
def _ensure_llm() -> None:
if not llm_client.config.api_key:
raise HTTPException(status_code=503, detail="LLM_API_KEY not configured")
routers/analysis.py:24
LLMClientis no longer referenced in this module after switching tocreate_llm_client(), so the import is now unused.
from gigaam_transcriber.summarizer import (
LLMClient,
create_llm_client,
generate_summary,
get_available_models,
summary_to_html,
)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self._client = GigaChatSDK( | ||
| credentials=self._credentials, | ||
| scope=self._scope, | ||
| model=self._model, | ||
| verify_ssl_certs=False, | ||
| timeout=self._timeout, | ||
| ) |
| # Дефолтный таймаут httpx в gigachat SDK слишком мал для начального | ||
| # TLS handshake + OAuth — увеличиваем, иначе ConnectTimeout. | ||
| self._timeout = float(os.getenv("GIGACHAT_TIMEOUT", "60")) | ||
| self._client = None |
| def create_llm_client() -> LLMClient | GigaChatLLMClient: | ||
| """Создать LLM-клиент на основе env-конфигурации.""" | ||
| gigachat_key = os.getenv("GIGACHAT_API_KEY", "") | ||
| if gigachat_key and HAS_GIGACHAT: | ||
| logger.info("LLM provider: GigaChat") | ||
| return GigaChatLLMClient(credentials=gigachat_key) | ||
| logger.info("LLM provider: OpenAI-compatible (%s)", _default_base_url()) | ||
| return LLMClient() |
| class GigaChatLLMClient: | ||
| """LLM-клиент для GigaChat API (Sber).""" | ||
|
|
||
| def __init__(self, credentials: str = "", scope: str = "GIGACHAT_API_CORP", | ||
| model: str = "GigaChat-Pro"): | ||
| self._credentials = credentials or os.getenv("GIGACHAT_API_KEY", "") | ||
| self._scope = scope or os.getenv("GIGACHAT_SCOPE", "GIGACHAT_API_CORP") | ||
| self._model = model or os.getenv("GIGACHAT_MODEL", "GigaChat-Pro") | ||
| # Дефолтный таймаут httpx в gigachat SDK слишком мал для начального | ||
| # TLS handshake + OAuth — увеличиваем, иначе ConnectTimeout. | ||
| self._timeout = float(os.getenv("GIGACHAT_TIMEOUT", "60")) | ||
| self._client = None | ||
|
|
||
| @property | ||
| def config(self) -> LLMClientConfig: | ||
| return LLMClientConfig( | ||
| base_url="gigachat", | ||
| api_key=self._credentials, | ||
| model=self._model, | ||
| ) | ||
|
|
||
| def _get_client(self): | ||
| if self._client is None: | ||
| if not HAS_GIGACHAT: | ||
| raise ImportError("Пакет 'gigachat' не установлен: pip install gigachat") | ||
| if not self._credentials: | ||
| raise ValueError("GIGACHAT_API_KEY не задан") | ||
| self._client = GigaChatSDK( | ||
| credentials=self._credentials, | ||
| scope=self._scope, | ||
| model=self._model, | ||
| verify_ssl_certs=False, | ||
| timeout=self._timeout, | ||
| ) | ||
| return self._client | ||
|
|
||
| def update_config(self, base_url: str, api_key: str, model: str) -> None: | ||
| self._credentials = api_key | ||
| self._model = model or "GigaChat-Pro" | ||
| self._client = None | ||
|
|
||
| def call(self, system_prompt: str, user_text: str, max_tokens: int = 4096) -> str: | ||
| client = self._get_client() | ||
| try: | ||
| payload = Chat( | ||
| messages=[ | ||
| Messages(role=MessagesRole.SYSTEM, content=system_prompt), | ||
| Messages(role=MessagesRole.USER, content=user_text), | ||
| ], | ||
| max_tokens=max_tokens, | ||
| temperature=0.3, | ||
| ) | ||
| response = client.chat(payload) | ||
| return response.choices[0].message.content or "" | ||
| except Exception as e: | ||
| raise RuntimeError(f"GigaChat error: {e}") from e | ||
|
|
||
| def test_connection(self) -> tuple[bool, str]: | ||
| if not self._credentials: | ||
| return False, "GIGACHAT_API_KEY не задан" | ||
| try: | ||
| client = self._get_client() | ||
| payload = Chat( | ||
| messages=[Messages(role=MessagesRole.USER, content="Hi")], | ||
| max_tokens=5, | ||
| ) | ||
| response = client.chat(payload) | ||
| if response.choices: | ||
| return True, f"GigaChat подключён (модель: {self._model})" | ||
| return False, "Пустой ответ" | ||
| except Exception as e: | ||
| return False, f"Ошибка GigaChat: {e}" | ||
|
|
||
|
|
||
| def create_llm_client() -> LLMClient | GigaChatLLMClient: | ||
| """Создать LLM-клиент на основе env-конфигурации.""" | ||
| gigachat_key = os.getenv("GIGACHAT_API_KEY", "") | ||
| if gigachat_key and HAS_GIGACHAT: | ||
| logger.info("LLM provider: GigaChat") | ||
| return GigaChatLLMClient(credentials=gigachat_key) | ||
| logger.info("LLM provider: OpenAI-compatible (%s)", _default_base_url()) | ||
| return LLMClient() |
| asr = get_asr_provider() | ||
| primary = getattr(asr, "_primary", asr) | ||
| asr_model = getattr(primary, "_model", "") or "" | ||
| info["asr"] = { | ||
| "model": asr_model, | ||
| "label": "GigaAM" if "gigaam" in asr_model.lower() else (asr_model or "ASR"), | ||
| "fallback": getattr(asr, "_secondary_name", None), |
| from gigaam_transcriber.mindmap import generate_mindmap_markdown | ||
| from gigaam_transcriber.models import SavedTranscription, User | ||
| from gigaam_transcriber.summarizer import LLMClient, generate_summary | ||
| from gigaam_transcriber.summarizer import LLMClient, create_llm_client, generate_summary |
| from gigaam_transcriber.limits import check_limit | ||
| from gigaam_transcriber.models import User | ||
| from gigaam_transcriber.summarizer import LLMClient | ||
| from gigaam_transcriber.summarizer import LLMClient, create_llm_client |
|
|
||
| from gigaam_transcriber.autoflow import run_autoflow | ||
| from gigaam_transcriber.summarizer import LLMClient, LLMClientConfig | ||
| from gigaam_transcriber.summarizer import LLMClient, LLMClientConfig, create_llm_client |
| env_file: | ||
| - .env.dev | ||
| environment: | ||
| # Transcription via Mistral Voxtral | ||
| - MISTRAL_API_KEY=${MISTRAL_API_KEY:-} | ||
| - ASR_URL=https://api.mistral.ai | ||
| - ASR_MODEL=voxtral-mini-latest | ||
| # LLM for summary / insights / chat (Mistral OpenAI-compat) | ||
| - LLM_API_KEY=${LLM_API_KEY:-} | ||
| - LLM_BASE_URL=https://api.mistral.ai/v1 | ||
| - LLM_MODEL=mistral-small-latest | ||
| # Auth | ||
| - JWT_SECRET=${JWT_SECRET:-dev-secret-change-in-prod} | ||
| - API_KEY=${API_KEY:-} | ||
| # Transcription via GigaAM (LiteLLM proxy) | ||
| - LITELLM_URL=https://litellm.komolov.synology.me | ||
| - LITELLM_MODEL=gigaamv3-generation | ||
| # LLM for summary / insights / chat (GigaChat) | ||
| # GIGACHAT_API_KEY is injected from .env.dev (gitignored — keep secrets out of VCS) | ||
| - GIGACHAT_SCOPE=GIGACHAT_API_CORP | ||
| - GIGACHAT_MODEL=GigaChat-Pro |
| """ | ||
| if llm_client is None: | ||
| llm_client = LLMClient() | ||
| llm_client = create_llm_client() | ||
|
|
Contributing changes from the SaintDemon25 fork (2 commits).
What's included
create_llm_client()insummarizer.pyreturnsGigaChatLLMClientwhenGIGACHAT_API_KEYis set, else the existing OpenAI-compatibleLLMClient. All call sites (summary, insights, chat, live-hints cascade, autoflow) route through the factory instead of instantiatingLLMClientdirectly — fixes401s where live/background paths picked up a stale key.GIGACHAT_TIMEOUT, default 60s) so the initial TLS+OAuth handshake doesn'tConnectTimeout.docker-compose.dev.yaml,LITELLM_URL/MODEL)..env.devgitignored; no credentials in the diff or commit history.Notes for the maintainer
🤖 Generated with Claude Code