Skip to content

Commit dd2be9a

Browse files
committed
feat: add Ollama native API support to inference proxy
Add pattern detection, provider profile, and validation probe for Ollama's native /api/chat, /api/tags, and /api/show endpoints. Proxy changes (l7/inference.rs): - POST /api/chat -> ollama_chat protocol - GET /api/tags -> ollama_model_discovery protocol - POST /api/show -> ollama_model_discovery protocol Provider profile (openshell-core/inference.rs): - New 'ollama' provider type with default endpoint http://host.openshell.internal:11434 - Supports ollama_chat, ollama_model_discovery, and OpenAI-compatible protocols (openai_chat_completions, openai_completions, model_discovery) - Credential lookup via OLLAMA_API_KEY, base URL via OLLAMA_BASE_URL Validation (backend.rs): - Ollama validation probe sends minimal /api/chat request with stream:false Tests: 4 new tests for pattern detection (ollama chat, tags, show, and GET /api/chat rejection). Signed-off-by: Lyle Hopkins <lyle@cosmicnetworks.com>
1 parent 0e5ebb6 commit dd2be9a

3 files changed

Lines changed: 82 additions & 0 deletions

File tree

crates/openshell-core/src/inference.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ const OPENAI_PROTOCOLS: &[&str] = &[
5656

5757
const ANTHROPIC_PROTOCOLS: &[&str] = &["anthropic_messages", "model_discovery"];
5858

59+
const OLLAMA_PROTOCOLS: &[&str] = &[
60+
"ollama_chat",
61+
"ollama_model_discovery",
62+
"openai_chat_completions",
63+
"openai_completions",
64+
"model_discovery",
65+
];
66+
5967
static OPENAI_PROFILE: InferenceProviderProfile = InferenceProviderProfile {
6068
provider_type: "openai",
6169
default_base_url: "https://api.openai.com/v1",
@@ -86,6 +94,16 @@ static NVIDIA_PROFILE: InferenceProviderProfile = InferenceProviderProfile {
8694
default_headers: &[],
8795
};
8896

97+
static OLLAMA_PROFILE: InferenceProviderProfile = InferenceProviderProfile {
98+
provider_type: "ollama",
99+
default_base_url: "http://host.openshell.internal:11434",
100+
protocols: OLLAMA_PROTOCOLS,
101+
credential_key_names: &["OLLAMA_API_KEY"],
102+
base_url_config_keys: &["OLLAMA_BASE_URL", "OLLAMA_HOST"],
103+
auth: AuthHeader::Bearer,
104+
default_headers: &[],
105+
};
106+
89107
/// Look up the inference provider profile for a given provider type.
90108
///
91109
/// Returns `None` for provider types that don't support inference routing
@@ -95,6 +113,7 @@ pub fn profile_for(provider_type: &str) -> Option<&'static InferenceProviderProf
95113
"openai" => Some(&OPENAI_PROFILE),
96114
"anthropic" => Some(&ANTHROPIC_PROFILE),
97115
"nvidia" => Some(&NVIDIA_PROFILE),
116+
"ollama" => Some(&OLLAMA_PROFILE),
98117
_ => None,
99118
}
100119
}

crates/openshell-router/src/backend.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,20 @@ fn validation_probe(route: &ResolvedRoute) -> Result<ValidationProbe, Validation
223223
});
224224
}
225225

226+
if route
227+
.protocols
228+
.iter()
229+
.any(|protocol| protocol == "ollama_chat")
230+
{
231+
return Ok(ValidationProbe {
232+
path: "/api/chat",
233+
protocol: "ollama_chat",
234+
body: bytes::Bytes::from_static(
235+
br#"{"model":"test","messages":[{"role":"user","content":"ping"}],"stream":false}"#,
236+
),
237+
});
238+
}
239+
226240
Err(ValidationFailure {
227241
kind: ValidationFailureKind::RequestShape,
228242
details: format!(

crates/openshell-sandbox/src/l7/inference.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,24 @@ pub fn default_patterns() -> Vec<InferenceApiPattern> {
4343
protocol: "anthropic_messages".to_string(),
4444
kind: "messages".to_string(),
4545
},
46+
InferenceApiPattern {
47+
method: "POST".to_string(),
48+
path_glob: "/api/chat".to_string(),
49+
protocol: "ollama_chat".to_string(),
50+
kind: "ollama_chat".to_string(),
51+
},
52+
InferenceApiPattern {
53+
method: "GET".to_string(),
54+
path_glob: "/api/tags".to_string(),
55+
protocol: "ollama_model_discovery".to_string(),
56+
kind: "ollama_tags".to_string(),
57+
},
58+
InferenceApiPattern {
59+
method: "POST".to_string(),
60+
path_glob: "/api/show".to_string(),
61+
protocol: "ollama_model_discovery".to_string(),
62+
kind: "ollama_show".to_string(),
63+
},
4664
InferenceApiPattern {
4765
method: "GET".to_string(),
4866
path_glob: "/v1/models".to_string(),
@@ -372,6 +390,37 @@ mod tests {
372390
assert!(result.is_none());
373391
}
374392

393+
#[test]
394+
fn detect_ollama_chat() {
395+
let patterns = default_patterns();
396+
let result = detect_inference_pattern("POST", "/api/chat", &patterns);
397+
assert!(result.is_some());
398+
assert_eq!(result.unwrap().protocol, "ollama_chat");
399+
}
400+
401+
#[test]
402+
fn detect_ollama_tags() {
403+
let patterns = default_patterns();
404+
let result = detect_inference_pattern("GET", "/api/tags", &patterns);
405+
assert!(result.is_some());
406+
assert_eq!(result.unwrap().protocol, "ollama_model_discovery");
407+
}
408+
409+
#[test]
410+
fn detect_ollama_show() {
411+
let patterns = default_patterns();
412+
let result = detect_inference_pattern("POST", "/api/show", &patterns);
413+
assert!(result.is_some());
414+
assert_eq!(result.unwrap().protocol, "ollama_model_discovery");
415+
}
416+
417+
#[test]
418+
fn no_match_ollama_chat_get() {
419+
let patterns = default_patterns();
420+
let result = detect_inference_pattern("GET", "/api/chat", &patterns);
421+
assert!(result.is_none());
422+
}
423+
375424
#[test]
376425
fn detect_get_models() {
377426
let patterns = default_patterns();

0 commit comments

Comments
 (0)