Repository navigation
feat: OpenAI-compatible LLM provider (#616) - #648
Merged
Merged
Conversation
Design approved by Judd 2026-10-31. Includes the live Ollama v1 probe results and the Unsloth, llama-server, OpenAI and OpenRouter research. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
Six persisted, env-guarded settings: llm_provider, openai_base_url, openai_api_key, openai_model, openai_vision, openai_server_type. One set of normalisers is shared by the env path and the persisted-override path: whitespace stripped, keyword values lower-cased, model ids kept as typed, and an unknown provider or server type falls back to the default with a warning instead of failing config load. Built by a DeepSeek flash worker (dspilot run 563207aa), reviewed here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
Built by the DeepSeek flash worker (dspilot run dbae24ba) from brief L4; reviewed and accepted. Three docstring/comment rationales reworded in review where the builder had invented a reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
A 400 naming an optional parameter drops, swaps or steps it down and retries, at most 3 times per call; the accepted shape is remembered per base URL, model and profile. An empty reply cut off by the cap while the model reasoned is retried once with a raised cap. Built by the DeepSeek flash worker (run a45279e0, brief L7). Review added three tests: raised cap never below the request, use_learned False overwrites the stored shape, no bare-word parameter match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
Built by DeepSeek run 7ead6df9 (brief L9), reviewed and re-verified here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
Built by the DeepSeek flash worker (dspilot run fa0222a5, brief L10), reviewed and accepted. Review fixup: dropped an unused noqa directive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
A 401 or 403 from /chat/completions now records a breaker failure, so a revoked hosted key stops being retried every call and the client cools down instead. 400 and 404 still count as success and models() stays outside the breaker. Built by the DeepSeek flash worker (dspilot run cbfbd23c, brief L12) and reviewed in the main session: full suite 3273 passed, 3 mutants caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
build_llm_client() picks Ollama or the OpenAI-compatible client from llm_provider; every caller uses app.state.llm and catches LLMError. GPU gate only for local providers, bulk enrichment stops on cooling down, unload and pull are provider-aware, health and probes list /models for the OpenAI-compatible client. DeepSeek flash run f0a7b2f7 (L15), reviewed and accepted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
…provider Adds the openai and llm credential integrations, skips masked secrets on save for every integration, and a step-by-step Test connection (reachable, key, model, JSON, thinking, vision) on a fresh client. Built by dspilot run 4d51def3 (brief L16). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
… script The title-language prompt now names the library a row is filed in and its path inside that library instead of the raw canonical path. A reply that is neither a JSON object nor a bare language code raises NotJSONReply and is stored as an error row; before, a prose reply was parsed by its first word, so "It is Japanese." was stored as Italian. OllamaClient.generate accepts schema_name so both clients take the same arguments. scripts/eval_title_language.py compares the old and new prompts against the arrs originalLanguage without writing to the enrichment store. Built by the DeepSeek flash worker (dspilot run eb2c0297, brief L18). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
Before #616 a prose or truncated JSON reply was stored as a guessed language code and cached forever. EnrichmentStore.requeue_unparseable re-parses every cached row at startup and clears the ones the current parser rejects, so they are asked again. Fail-soft at boot. Built by DeepSeek flash (dspilot run e5bee333, brief L19). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
The telemetry ingest drops unknown top-level keys, so llm_provider would never be stored. Judd chose openai_local and openai_hosted booleans in the integrations object, which the ingest already stores and counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
openai_local and openai_hosted booleans inside the integrations object, set only when the OpenAI-compatible provider is active with a base URL and a model. The ingest drops unknown top-level keys, so no new top-level field. No URL, key or model name enters the payload. Built by DeepSeek flash (dspilot run 3dcc7729, brief L20). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
Provider select on the LLM panel, the OpenAI-compatible credential editor (base URL, key, model, vision, server type), the hosted-profile disclosure line, per-step Test connection rows, and the Ollama-only vision card and unload row hidden for the OpenAI-compatible provider. Built by deepseek-flash, dspilot run 5bcbaa41 (brief L22). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
The absence check passed on a panel that never rendered. It now asserts the panel itself rendered first, and fails against the base. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
CHANGELOG Unreleased: Added entry for the OpenAI-compatible provider naming the servers tested live (Ollama /v1, OpenRouter gemini-2.5-flash and gpt-4o-mini) and those implemented from documentation only (OpenAI direct, Unsloth Studio, llama-server); Changed entry for the title-language prompt with the gemma4 eval result (old 33/60, new 33/60). README: provider section with a compose example, six env vars in the table. eval_title_language.py: the usage line piped into subarr-next, whose code predates 616 and lacks the modules the script imports; document the throwaway-container route used for the live run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi
| continue | ||
| if key in _SECRET_KEYS and _is_masked(raw): | ||
| skipped_masked.append(settings_attr) | ||
| log.info("creds: %s.%s masked value, kept the stored secret", svc, settings_attr) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Adds an OpenAI-compatible LLM provider next to Ollama. Settings → Integrations offers Provider: Ollama | OpenAI-compatible (base URL with
/v1, optional API key, text model, vision setting), also settable throughLLM_PROVIDER/OPENAI_*env vars. All three LLM features run on it: the title-language guess, Arena explanations and the vision check. One code path serves local servers (Ollama/v1, Unsloth Studio, llama-server, LM Studio, vLLM) and hosted APIs (OpenAI, OpenRouter). It works out per server and model which JSON and reasoning-off request formats are accepted and remembers them, and it never returns an all-reasoning reply as a silent empty answer. Test connection runs six named checks. The API key is masked on read, never logged, never in telemetry and never overwritten by its own mask. The title-language prompt now sends library name + path relative to the library root, for both providers.Why
Closes #616.
Testing
PYTHONPATH=src pytest -qpasses locally: 3435 passed, 9 skipped (after rebase onto 7c9e0b4)settings.jsx: 26 of 29 fail there. Full vitest 53 files / 366 tests;check:frontendrc 0 with bundles unchanged; ruff 0.15.15 clean onsrc/ tests/./v1,gemma4:12b-it-qat: test connection 6/6;/api/enrichment/lang"Shingeki no Kyojin" →ja;/api/vision/checkparsed a real Plex thumbnail; Arena explanation non-empty (explainer called on the live client with a synthetic result, since the container has no subgen).google/gemini-2.5-flash(reasoning) andopenai/gpt-4o-mini: the same four checks pass on both; neither needed a format fallback or a reasoning retry (no#616 ... retryinglines in the logs).scripts/eval_title_language.py --arm both, 60 titles scored against the arrs'originalLanguage, gemma4): old 33/60, new 33/60 (en 27/30, other 6/30). Acceptance was new ≥ old − 1.Migration impact
N/A: no schema change. New settings persist through the existing config store.
Telemetry impact
integrationsobject,openai_localandopenai_hosted, set only when the OpenAI-compatible provider is active with a base URL and model. No URL, key or model name enters the payload.Known limits
🤖 Generated with Claude Code
https://claude.ai/code/session_01ShzWm3urD55AUk5yrSgXsi