feat(arr): make the OpenAI endpoint a live setting

The base URL and model move into a cell the backend re-reads per request,
so an operator can repoint it without a restart. Migration 0028 adds the
two columns; DESIGN.md §15 calls both database rows. Construction never
depends on the API key — llama.cpp serves without one.
This commit is contained in:
Miguel Palhas
2026-08-25 08:29:45 +01:00
parent ab001b512f
commit c6906bffae
5 changed files with 318 additions and 27 deletions
+53 -2
View File
@@ -16,7 +16,7 @@ use std::time::Duration;
use arr_core::Language;
use arr_subs::translate::{translate, Backend, Error};
use arr_subs::{OpenAi, OpenAiConfig};
use arr_subs::{OpenAi, OpenAiConfig, OpenAiEndpoint};
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
@@ -319,9 +319,60 @@ async fn an_unsupported_target_is_refused_before_any_request() {
assert!(matches!(error, Error::UnsupportedTarget { .. }));
}
/// #220: an edit of `openai_base_url` / `openai_model` reaches the running
/// backend without a restart — the next batch goes to the new endpoint and
/// names the new model, with no reconstruction in between.
#[tokio::test]
async fn repointing_the_endpoint_moves_the_next_batch() {
let first = MockServer::start().await;
let second = MockServer::start().await;
mount_reply(
&first,
ResponseTemplate::new(200).set_body_string(chat_response(
r#""[{\"number\": 1, \"text\": \"do primeiro\"}]""#,
)),
)
.await;
mount_reply(
&second,
ResponseTemplate::new(200).set_body_string(chat_response(
r#""[{\"number\": 1, \"text\": \"do segundo\"}]""#,
)),
)
.await;
let endpoint = OpenAiEndpoint::new(Some(&format!("{}/v1/", first.uri())), Some("first-model"))
.expect("endpoint resolves");
let backend =
OpenAi::with_endpoint(OpenAiConfig { api_key: None }, endpoint.clone()).expect("builds");
let cues = vec![arr_subs::translate::BatchCue {
number: 1,
text: "from the first".to_owned(),
}];
let batch = arr_subs::translate::Batch {
source: Language::Other("en".to_owned()),
target: Language::PortuguesePortugal,
cues: cues.clone(),
};
let out = backend.translate(&batch).await.expect("first batch");
assert_eq!(out[0].text, "do primeiro");
endpoint
.set(Some(&format!("{}/v1/", second.uri())), Some("second-model"))
.expect("repoint");
let out = backend.translate(&batch).await.expect("second batch");
assert_eq!(out[0].text, "do segundo");
let sent = second.received_requests().await.expect("requests recorded");
let body: serde_json::Value =
serde_json::from_slice(&sent.last().expect("one request").body).expect("json body");
assert_eq!(body["model"], "second-model");
}
#[test]
fn the_backend_answers_to_openai() {
let id = OpenAi::new("gpt-4o-mini", OpenAiConfig { api_key: None })
let id = OpenAi::new(OpenAiConfig { api_key: None })
.expect("client builds")
.id();
assert_eq!(id.as_str(), "openai");