feat: add local/ routing prefix for model IDs with embedded slashes
Some backends (e.g., gpt.uio.no running Ollama) have model IDs containing slashes like Qwen/Qwen3.6-27B-FP8. The existing routing prefix logic would strip 'Qwen/' as a provider prefix, sending only the remainder on the wire and causing 403 Forbidden from the server. The new local/ prefix is an escape hatch: it strips just 'local/' and sends everything after it verbatim, preserving embedded slashes in model IDs. Usage: --model local/Qwen/Qwen3.6-27B-FP8 This patch was developed with assistance from OpenCode and local Qwen 3.6 API server.
This commit is contained in:
parent
87b7e74770
commit
95bf18e5cc
|
|
@ -915,14 +915,17 @@ pub fn model_requires_reasoning_content_in_history(model: &str) -> bool {
|
|||
|
||||
/// Strip routing prefix (e.g., "openai/gpt-4" → "gpt-4") for the wire.
|
||||
/// The prefix is used only to select transport; the backend expects the
|
||||
/// bare model id.
|
||||
/// bare model id. Use `"local/"` as an escape hatch when the server's model
|
||||
/// ID itself contains a slash (e.g., `local/Qwen/Qwen3.6-27B-FP8`).
|
||||
#[allow(dead_code)]
|
||||
fn strip_routing_prefix(model: &str) -> &str {
|
||||
if let Some(pos) = model.find('/') {
|
||||
let prefix = &model[..pos];
|
||||
// Only strip if the prefix before "/" is a known routing prefix,
|
||||
// not if "/" appears in the middle of the model name for other reasons.
|
||||
if matches!(prefix, "openai" | "xai" | "grok" | "qwen" | "kimi") {
|
||||
// "local" is an escape hatch: strips just "local/" and sends everything
|
||||
// after it verbatim (including embedded slashes) on the wire.
|
||||
if matches!(prefix, "openai" | "xai" | "grok" | "qwen" | "kimi" | "local") {
|
||||
&model[pos + 1..]
|
||||
} else {
|
||||
model
|
||||
|
|
@ -965,6 +968,12 @@ fn wire_model_for_base_url<'a>(
|
|||
return Cow::Borrowed(&model[pos + 1..]);
|
||||
}
|
||||
|
||||
// "local/" escape hatch: strips just the prefix and sends everything after it
|
||||
// verbatim on the wire (including embedded slashes in model IDs).
|
||||
if lowered_prefix == "local" {
|
||||
return Cow::Borrowed(&model[pos + 1..]);
|
||||
}
|
||||
|
||||
Cow::Borrowed(model)
|
||||
}
|
||||
|
||||
|
|
@ -2730,4 +2739,24 @@ mod tests {
|
|||
assert_eq!(super::strip_routing_prefix("kimi-k2.5"), "kimi-k2.5"); // no prefix, unchanged
|
||||
assert_eq!(super::strip_routing_prefix("kimi/kimi-k1.5"), "kimi-k1.5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_routing_prefix_preserves_embedded_slash_with_local_prefix() {
|
||||
// "local/" escape hatch: strips just the prefix, preserves everything after it verbatim
|
||||
assert_eq!(
|
||||
super::strip_routing_prefix("local/Qwen/Qwen3.6-27B-FP8"),
|
||||
"Qwen/Qwen3.6-27B-FP8"
|
||||
);
|
||||
assert_eq!(super::strip_routing_prefix("local/mistralai/Mistral-Large-3"), "mistralai/Mistral-Large-3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wire_model_preserves_embedded_slash_with_local_prefix() {
|
||||
let config = OpenAiCompatConfig::openai();
|
||||
// "local/" prefix: strips just "local/", sends rest verbatim on the wire
|
||||
assert_eq!(
|
||||
super::wire_model_for_base_url("local/Qwen/Qwen3.6-27B-FP8", config.clone(), ""),
|
||||
"Qwen/Qwen3.6-27B-FP8"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1943,7 +1943,10 @@ fn validate_model_syntax(model: &str) -> Result<(), String> {
|
|||
}
|
||||
// Check provider/model format: provider_id/model_id
|
||||
let parts: Vec<&str> = trimmed.split('/').collect();
|
||||
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
|
||||
if (parts.len() != 2 && !trimmed.starts_with("local/"))
|
||||
|| parts[0].is_empty()
|
||||
|| parts[1].is_empty()
|
||||
{
|
||||
// #154: hint if the model looks like it belongs to a different provider
|
||||
let mut err_msg = format!(
|
||||
"invalid model syntax: '{}'.\nExpected provider/model (e.g., anthropic/claude-opus-4-7)",
|
||||
|
|
|
|||
Loading…
Reference in New Issue