Skip to content

feat(llm): add Opencode provider with mandatory x-opencode-session header - #278

Open
frank-lar wants to merge 3 commits into
hydropix:mainfrom
frank-lar:feat/opencode-provider
Open

frank-lar wants to merge 3 commits into
hydropix:mainfrom
frank-lar:feat/opencode-provider

Conversation

@frank-lar

Copy link
Copy Markdown

Add Opencode provider (OpenAI-compatible + mandatory session header)

Summary

Adds an Opencode LLM provider. The endpoint is fully OpenAI-compatible
(chat/completions, models), with one requirement: every request must
carry an x-opencode-session header holding a stable id for the conversation
.

The provider is wired end-to-end: adapter, factory, config/.env, web API,
CLI, frontend UI and all 7 UI locales.

Motivation

Opencode rejects calls that omit x-opencode-session, so it cannot be used
through the generic openai provider. Rather than special-case it at each call
site, this introduces a thin OpenAICompatibleProvider subclass that owns the
header, plus a small extra_headers extension point on the base provider.

Design

  • What is a "conversation"? This codebase has no conversation abstraction.
    One provider instance is created per translation job and reused for every
    chunk (see GenericTranslator.translateLLMClient), so the session id is
    minted once at provider construction and reused for the whole job. Parallel
    workers and retries share it; a separate job (or the auto-prep/sample probe)
    gets its own.
  • Value: a random UUIDv4, prefixed opencode-session-. It is never
    derived from the machine, the install or user data — this keeps the existing
    install-fingerprint regression guard meaningful.
  • Scope: the header is sent on every provider request — chat
    completions, model listing, and context-detection probes — and is set
    per-request, never on the shared httpx client defaults.
  • Endpoint: fixed cloud URL, overridable via OPENCODE_API_ENDPOINT (read at
    server startup). The per-request llm_api_endpoint field is ignored, like the
    other cloud providers, so a stale Ollama value in the UI can't hijack it.
  • No pricing entry is added, because no real OpenCode prices are known;
    get_default_pricing returns None and the cost estimator degrades to "no
    estimate". A test pins this as intentional.

What changed

Provider core

  • src/core/llm/providers/opencode.py (new): OpencodeProvider subclass;
    generates/reuses the session id; implements get_available_models().
  • src/core/llm/providers/openai.py: optional extra_headers on
    OpenAICompatibleProvider, merged into every request (base
    Content-Type/Authorization win over caller-supplied extras).
  • src/core/llm/utils/context_detection.py: optional extra_headers so the
    context probe carries the session too.

Registration & config

  • src/core/llm/factory.py: opencode branch (key + model required; explicit
    session_id/conversation_id passthrough).
  • src/core/llm/__init__.py, src/core/llm_client.py: export / client wiring.
  • src/config.py: OPENCODE_API_KEY, OPENCODE_MODEL,
    OPENCODE_API_ENDPOINT, TranslationConfig fields.
  • src/api/api_keys.py: OPENCODE_API_KEY env mapping.
  • .env.example: Opencode block.

API & CLI

  • src/api/blueprints/config_routes.py: /api/models listing,
    /api/config masks, /api/settings GET flag, and the /api/settings POST
    allow-list (see review fixes).
  • src/api/blueprints/translation_routes.py, src/api/handlers.py: key
    plumbing through every pipeline (txt/srt/epub/docx + refine).
  • src/core/adapters/{translate_file,refine_file}.py, src/core/translator.py,
    src/core/epub/translator.py, src/core/refine/*: forward
    opencode_api_key.
  • translate.py: --provider opencode, --opencode_api_key, OPENCODE_MODEL
    default, and fail-fast guards for a missing key/model.

Frontend & i18n

  • Provider dropdown, logo/metadata, key field, model loading, API-key maps,
    settings persistence and provider labels.
  • 6 new settings:* keys + 1 errors:* key, added to all 7 locales
    (en, fr, es, de, zh-CN, ja, ko) — test_frontend_i18n.py green.

Docs

  • docs/PROVIDERS.md (setup), docs/CLI.md, docs/TROUBLESHOOTING.md,
    docs/API_KEY_ROTATION.md, README.md.

Tests

  • tests/unit/test_opencode_provider.py: header present/stable/distinct, not on
    the shared client, UUIDv4/machine-independence, header precedence,
    context-probe header, model-listing parse + edge cases, factory guards and
    endpoint-ignore.
  • tests/unit/test_opencode_integration.py: settings allow-list + endpoint not
    UI-editable, create_llm_client kwargs forwarding, intentional no-pricing.

Review follow-ups (commit 9f8cf48)

An independent review requested changes; all findings are addressed:

  • B1 (blocker): /api/settings silently dropped OPENCODE_API_KEY /
    OPENCODE_MODEL; added to the allow-list (lifted to a module-level constant
    so it is regression-tested).
  • M1: create_llm_client('opencode', ...) dropped context_window /
    log_callback; now forwarded like the openai branch.
  • m1: extra_headers could clobber Content-Type/Authorization; base
    headers now win.
  • m2: removed misleading "using fallback list" wording (no fallback exists).
  • m3/m4: documented endpoint startup semantics; fail fast on empty model.
  • Minor: canonical key placeholders, docs indentation/wording.

Testing

python3 -m venv venv && ./venv/bin/pip install -r requirements-dev.txt
./venv/bin/python -m pytest tests/unit/test_opencode_provider.py tests/unit/test_opencode_integration.py tests/test_frontend_i18n.py -q
./venv/bin/python -m pytest -q
  • Targeted suites: green.
  • Full suite: 2287 passed, 2 skipped, 10 failed.
  • The 10 failures are pre-existing on a clean origin/main (0a26cec) in
    this environment and unrelated to this PR:
    • 9 × tests/characterization/test_progress_baseline.py (stale progress
      goldens),
    • 1 × tests/unit/test_path_validator.py::test_reject_path_separator_backslash
      (platform-specific).
      No new failures were introduced.

Notes for reviewers

  • x-opencode-session is not X-Session-Token/X-Client-Agent/X-Request-ID;
    tests/unit/test_no_install_fingerprint.py still passes and the shared client
    defaults are unchanged.
  • No secrets are committed; examples use inert placeholders.
  • refine runs create a new client, hence a new session id — a distinct
    conversation, which is the intended semantics.
  • Not included: no default pricing (intentional), OPENCODE_API_ENDPOINT is not
    UI-editable (consistent with other cloud endpoints).

Checklist

  • Feature works end-to-end (provider selectable in UI + CLI)
  • Adapter sends the mandatory header on every request
  • Session id is stable per conversation, random and not machine-derived
  • No secrets / no API keys committed
  • All new i18n keys exist in all 7 locales
  • Unit + integration tests added; targeted suites green
  • Docs updated (providers, CLI, troubleshooting, rotation, README)

Frengo added 3 commits September 13, 2026 22:07
Opencode is OpenAI-compatible but requires an x-opencode-session header on
every call. Add OpencodeProvider (subclass of OpenAICompatibleProvider) that
generates a random, stable-per-conversation id and sends it on chat,
model-listing and context-detection requests.

Add optional extra_headers support to OpenAICompatibleProvider and
ContextDetector, then wire the provider through the factory, config, API
routes, handlers, translate/refine pipelines, CLI, frontend UI and all 7
locales. Add unit tests covering the header behaviour.
B1: allow OPENCODE_API_KEY/OPENCODE_MODEL through /api/settings by lifting
the allow-list to a module-level frozenset (previously the UI silently
dropped the key and model on save).

M1: forward context_window/log_callback in the create_llm_client opencode
branch, matching the openai branch, so the EPUB/refine paths keep the job's
context size and log callback.

m1: apply extra_headers before the base headers so a caller-supplied dict
cannot clobber Content-Type or Authorization (openai provider + context
detector).

m2: stop showing the DeepSeek 'using fallback list' wording for Opencode,
which has no fallback list.

m3/m4: document that OPENCODE_API_ENDPOINT is read at startup, and fail fast
when no model is configured instead of minting an unusable provider.

n1-n3: canonical key placeholders, restore TROUBLESHOOTING indentation and
correct the cloud-endpoint comparison.

Add regression tests for the settings allow-list, kwargs forwarding, header
precedence, context-detection probe, factory guards/endpoint-ignore, model
listing edge cases and the absence of default pricing.
The provider was missing from several enumerations: the README logo row,
the .env.example and docker-compose option lists, the API-key rotation
syntax list, the CLI example set, the Docker provider list, the translator
skill, and a few code docstrings/comments that enumerate providers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant