fix: prevent hybrid mode from leaking OpenRouter model to local llamacpp client

When backend=hybrid with LLM_BACKEND=llamacpp, the user-selected model
(an OpenRouter id like "google/gemini-3-flash-preview") was being applied
to the local LlamaCppClient's primary_model and vision_model. This caused
describe_image to send the OpenRouter model name to llama-swap, which
returned 400 because it has no such slot.

Guard the local-client model override with !is_hybrid so it only applies
in local-only mode (where the user is selecting a different local model).
Bump to v1.2.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 4.7 committed 2026-05-26 09:55:16 -04:00
1 parent 0a627f4880
commit b03ee60342
7 files changed
+172 -79

No files matched your search

+27 -5
View File
@@ -409,10 +409,7 @@ impl LlmClient for LlamaCppClient {
tools.len()
);
let mut body = serde_json::Map::new();
body.insert(
"model".into(),
Value::String(self.primary_model.clone()),
);
body.insert("model".into(), Value::String(self.primary_model.clone()));
body.insert(
"messages".into(),
Value::Array(Self::messages_to_openai(&messages)),
@@ -1071,6 +1068,28 @@ mod tests {
assert_eq!(local.embedding_model, "embed");
}
#[test]
fn hybrid_mode_local_client_preserves_vision_model() {
// In hybrid mode, overrides.model is an OpenRouter model id
// (e.g. "google/gemini-3-flash-preview"). The local llamacpp
// client must NOT adopt that as its vision_model — it should
// keep the configured LLAMA_SWAP_VISION_MODEL so describe_image
// hits the correct local slot instead of sending an unknown
// model name to llama-swap.
let mut base = LlamaCppClient::new(None, Some("chat".into()));
base.set_vision_model("vision".into());
base.set_embedding_model("embed".into());
// Simulate what resolve_backend SHOULD do for hybrid mode:
// clone but do NOT override primary_model / vision_model.
let local = base.clone();
// The local client keeps its configured slots.
assert_eq!(local.primary_model, "chat");
assert_eq!(local.vision_model, "vision");
assert_eq!(local.embedding_model, "embed");
}
#[test]
fn assistant_tool_calls_emit_null_content() {
let msg = ChatMessage {
@@ -1086,7 +1105,10 @@ mod tests {
images: None,
};
let wire = LlamaCppClient::messages_to_openai(&[msg]);
assert!(wire[0]["content"].is_null(), "empty content + tool_calls should emit null");
assert!(
wire[0]["content"].is_null(),
"empty content + tool_calls should emit null"
);
}
#[test]