Add the OpenAI-compatible translation backend #191

Closed
opened 2026-08-24 21:28:32 +01:00 by naps62-yolo · 1 comment
Owner

Implement the OpenAI-compatible translation backend behind its own cargo
feature, against the trait from #190.

One HTTP shape reaches the largest number of options: OpenAI itself,
OpenRouter, Groq, a self-hosted gateway, or a local llama.cpp server. Base
URL and model are settings; the API key is config or environment per §10.

The prompt instructs a subtitle-appropriate translation into the exact target
variant — pt-PT is not pt-BR — and requires the reply to carry the same cue
numbering it was given, which #190 then validates. A refusal, a truncated reply
or a rate-limit response is an error the caller can distinguish, not a silent
partial translation.

Fixture responses in tests. No live endpoint.

Depends on: #190

Implement the OpenAI-compatible translation backend behind its own cargo feature, against the trait from #190. One HTTP shape reaches the largest number of options: OpenAI itself, OpenRouter, Groq, a self-hosted gateway, or a local `llama.cpp` server. Base URL and model are settings; the API key is config or environment per §10. The prompt instructs a subtitle-appropriate translation into the exact target variant — pt-PT is not pt-BR — and requires the reply to carry the same cue numbering it was given, which #190 then validates. A refusal, a truncated reply or a rate-limit response is an error the caller can distinguish, not a silent partial translation. Fixture responses in tests. No live endpoint. Depends on: #190
naps62-yolo added this to the Subtitles milestone 2026-08-24 21:28:32 +01:00
naps62-yolo added the difficulty/easyarea/subsphase/9-subtitlestype/feature labels 2026-08-24 21:28:32 +01:00
naps62-yolo added a new dependency 2026-08-24 21:29:27 +01:00
Author
Owner

Implemented the OpenAI-compatible translation backend behind the existing translate-openai feature: OpenAi in crates/arr-subs/src/openai.rs implements the translate::Backend trait from #190 against any /v1/chat/completions-shaped endpoint (OpenAI, OpenRouter, Groq, a self-hosted gateway, llama.cpp). The base URL and model are constructor arguments — settings, per the issue — while OpenAiConfig carries only the optional API key (bootstrap config/env, §10). Each batch travels as a JSON array of {number, text} objects and the reply is asked for in the same shape, so a multi-line cue's embedded newlines round-trip cleanly (a line-oriented reply format could not tell a multi-line cue from two cues). The system prompt names the exact target variant and, for pt-PT/pt-BR, gives concrete vocabulary the model must not reach for. A refusal, a content-filtered reply, or one cut short by the token limit each fail with a distinct Error::Malformed before reaching validation, rather than being read as an empty or partial translation; the shared translate() step still catches a mismatched cue count or numbering on its own, since this backend does not validate its own replies. 11 wiremock-backed integration tests cover the happy path, bearer auth, a markdown-fenced JSON reply, 401/429, refusal, content-filter, truncation, and the shared mismatch/unsupported-target paths; unit tests cover the prompt and code-fence stripping. just ci passed.

Branch: subtitles/191-openai-backend
just ci: passed (556 tests, 0 failed)

Findings

  • The push gate (Justfile's test and lint recipes) runs with default features only — no --features / --all-features. Every translation backend lands behind its own default-off feature (translate-openai, and #192/#193 to come), so cargo clippy and cargo nextest run in just ci never compile or run this code, or DeepL's/Google's once they land. I verified this backend separately with cargo check/clippy/test -p arr-subs --features translate-openai (all clean), but the shared gate gives no signal on any backend crate by itself. Whoever wires backend selection (#198) should also update the lint/test recipes to add --all-features (or an explicit feature list) so the gate actually exercises compiled-in backends.
Implemented the OpenAI-compatible translation backend behind the existing `translate-openai` feature: `OpenAi` in `crates/arr-subs/src/openai.rs` implements the `translate::Backend` trait from #190 against any `/v1/chat/completions`-shaped endpoint (OpenAI, OpenRouter, Groq, a self-hosted gateway, `llama.cpp`). The base URL and model are constructor arguments — settings, per the issue — while `OpenAiConfig` carries only the optional API key (bootstrap config/env, §10). Each batch travels as a JSON array of `{number, text}` objects and the reply is asked for in the same shape, so a multi-line cue's embedded newlines round-trip cleanly (a line-oriented reply format could not tell a multi-line cue from two cues). The system prompt names the exact target variant and, for pt-PT/pt-BR, gives concrete vocabulary the model must not reach for. A refusal, a content-filtered reply, or one cut short by the token limit each fail with a distinct `Error::Malformed` before reaching validation, rather than being read as an empty or partial translation; the shared `translate()` step still catches a mismatched cue count or numbering on its own, since this backend does not validate its own replies. 11 wiremock-backed integration tests cover the happy path, bearer auth, a markdown-fenced JSON reply, 401/429, refusal, content-filter, truncation, and the shared mismatch/unsupported-target paths; unit tests cover the prompt and code-fence stripping. `just ci` passed. Branch: `subtitles/191-openai-backend` `just ci`: passed (556 tests, 0 failed) ## Findings - The push gate (`Justfile`'s `test` and `lint` recipes) runs with default features only — no `--features` / `--all-features`. Every translation backend lands behind its own default-off feature (`translate-openai`, and #192/#193 to come), so `cargo clippy` and `cargo nextest run` in `just ci` never compile or run this code, or DeepL's/Google's once they land. I verified this backend separately with `cargo check/clippy/test -p arr-subs --features translate-openai` (all clean), but the shared gate gives no signal on any backend crate by itself. Whoever wires backend selection (#198) should also update the `lint`/`test` recipes to add `--all-features` (or an explicit feature list) so the gate actually exercises compiled-in backends.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: yolo/arr#191