mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
701 lines
23 KiB
Rust
701 lines
23 KiB
Rust
//! Round 15 raw/E2E coverage for inference provider and local runtime paths.
|
|
//!
|
|
//! These tests use only loopback HTTP mocks and temp workspaces. They do not
|
|
//! require real Ollama, LM Studio, Piper, Whisper, Python, or model binaries.
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use axum::body::Body;
|
|
use axum::extract::State;
|
|
use axum::http::{header, HeaderMap, Response, StatusCode};
|
|
use axum::response::IntoResponse;
|
|
use axum::routing::{get, post};
|
|
use axum::{Json, Router};
|
|
use serde_json::{json, Value};
|
|
use tempfile::{tempdir, TempDir};
|
|
|
|
use openhuman_core::openhuman::config::schema::cloud_providers::{
|
|
AuthStyle as CloudAuthStyle, CloudProviderCreds,
|
|
};
|
|
use openhuman_core::openhuman::config::Config;
|
|
use openhuman_core::openhuman::credentials::{
|
|
AuthService, APP_SESSION_PROVIDER, DEFAULT_AUTH_PROFILE_NAME,
|
|
};
|
|
use openhuman_core::openhuman::inference::local::LocalAiService;
|
|
use openhuman_core::openhuman::inference::provider::compatible::{
|
|
AuthStyle as CompatibleAuthStyle, OpenAiCompatibleProvider,
|
|
};
|
|
use openhuman_core::openhuman::inference::provider::factory::{
|
|
auth_key_for_slug, create_chat_provider_from_string, provider_for_role,
|
|
};
|
|
use openhuman_core::openhuman::inference::provider::{
|
|
list_configured_models, sanitize_api_error, ChatMessage, ChatRequest, Provider, ProviderDelta,
|
|
};
|
|
use openhuman_core::openhuman::tools::ToolSpec;
|
|
|
|
#[derive(Clone, Default)]
|
|
struct MockState {
|
|
requests: Arc<Mutex<Vec<(String, Option<String>, Value)>>>,
|
|
}
|
|
|
|
struct EnvVarGuard {
|
|
key: &'static str,
|
|
previous: Option<String>,
|
|
}
|
|
|
|
impl EnvVarGuard {
|
|
fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {
|
|
let previous = std::env::var(key).ok();
|
|
// SAFETY: this test binary is run with --test-threads=1 in validation.
|
|
unsafe { std::env::set_var(key, value) };
|
|
Self { key, previous }
|
|
}
|
|
}
|
|
|
|
impl Drop for EnvVarGuard {
|
|
fn drop(&mut self) {
|
|
match &self.previous {
|
|
Some(value) => {
|
|
// SAFETY: this test binary is run with --test-threads=1 in validation.
|
|
unsafe { std::env::set_var(self.key, value) }
|
|
}
|
|
None => {
|
|
// SAFETY: this test binary is run with --test-threads=1 in validation.
|
|
unsafe { std::env::remove_var(self.key) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn compatible_provider_covers_chat_responses_streaming_tools_and_errors() {
|
|
let (base, state) = serve_mock().await;
|
|
let provider = OpenAiCompatibleProvider::new_with_user_agent(
|
|
"custom_openai",
|
|
&format!("{base}/v1"),
|
|
Some("sk-test-secret"),
|
|
CompatibleAuthStyle::Bearer,
|
|
"round15-agent",
|
|
)
|
|
.with_temperature_unsupported_models(vec!["cold-*".to_string()])
|
|
.with_temperature_override(Some(0.7));
|
|
|
|
let simple = provider
|
|
.chat_with_system(Some("system"), "hello", "demo-chat", 0.2)
|
|
.await
|
|
.expect("chat_with_system");
|
|
assert_eq!(simple, "chat:demo-chat");
|
|
|
|
let history = provider
|
|
.chat_with_history(
|
|
&[ChatMessage::system("rules"), ChatMessage::user("history")],
|
|
"responses-only",
|
|
0.3,
|
|
)
|
|
.await
|
|
.expect("responses fallback");
|
|
assert_eq!(history, "responses fallback text");
|
|
|
|
let tools = vec![ToolSpec {
|
|
name: "lookup".to_string(),
|
|
description: "lookup a thing".to_string(),
|
|
parameters: json!({
|
|
"type": "object",
|
|
"properties": { "query": { "type": "string" } },
|
|
"required": ["query"]
|
|
}),
|
|
}];
|
|
let native = provider
|
|
.chat(
|
|
ChatRequest {
|
|
messages: &[ChatMessage::user("use a tool")],
|
|
tools: Some(&tools),
|
|
stream: None,
|
|
max_tokens: None,
|
|
},
|
|
"tool-model",
|
|
0.1,
|
|
)
|
|
.await
|
|
.expect("native tools");
|
|
assert_eq!(native.text.as_deref(), Some("tool response"));
|
|
assert_eq!(native.tool_calls.len(), 1);
|
|
assert_eq!(native.tool_calls[0].name, "lookup");
|
|
assert_eq!(native.tool_calls[0].arguments, r#"{"query":"openhuman"}"#);
|
|
let usage = native.usage.expect("usage");
|
|
assert_eq!(usage.input_tokens, 11);
|
|
assert_eq!(usage.output_tokens, 7);
|
|
assert_eq!(usage.cached_input_tokens, 3);
|
|
assert!((usage.charged_amount_usd - 0.0042).abs() < f64::EPSILON);
|
|
|
|
let (tx, mut rx) = tokio::sync::mpsc::channel::<ProviderDelta>(16);
|
|
let streamed = provider
|
|
.chat(
|
|
ChatRequest {
|
|
messages: &[ChatMessage::user("stream it")],
|
|
tools: Some(&tools),
|
|
stream: Some(&tx),
|
|
max_tokens: None,
|
|
},
|
|
"stream-model",
|
|
0.1,
|
|
)
|
|
.await
|
|
.expect("streaming native chat");
|
|
drop(tx);
|
|
assert_eq!(streamed.text.as_deref(), Some("hello world"));
|
|
assert_eq!(streamed.reasoning_content.as_deref(), Some("thinking"));
|
|
assert_eq!(streamed.tool_calls.len(), 1);
|
|
|
|
let deltas = collect_deltas(&mut rx).await;
|
|
assert!(deltas
|
|
.iter()
|
|
.any(|d| matches!(d, ProviderDelta::TextDelta { delta } if delta == "hello ")));
|
|
assert!(deltas
|
|
.iter()
|
|
.any(|d| matches!(d, ProviderDelta::ThinkingDelta { delta } if delta == "thinking")));
|
|
assert!(deltas.iter().any(
|
|
|d| matches!(d, ProviderDelta::ToolCallStart { tool_name, .. } if tool_name == "lookup")
|
|
));
|
|
|
|
let err = provider
|
|
.chat_with_system(None, "boom", "budget-model", 0.2)
|
|
.await
|
|
.expect_err("budget error");
|
|
assert!(err.to_string().contains("budget exhausted"));
|
|
assert_eq!(
|
|
sanitize_api_error("leaked sk-abcdef ghp_secret-token"),
|
|
"leaked [REDACTED] [REDACTED]"
|
|
);
|
|
|
|
let cold = provider
|
|
.chat_with_system(None, "no temperature", "cold-no-temp", 0.2)
|
|
.await
|
|
.expect("temperature omitted");
|
|
assert_eq!(cold, "chat:cold-no-temp");
|
|
|
|
let seen = state.requests.lock().expect("requests");
|
|
assert!(seen.iter().any(|(path, auth, _)| {
|
|
path == "/v1/chat/completions" && auth.as_deref() == Some("Bearer sk-test-secret")
|
|
}));
|
|
assert!(seen
|
|
.iter()
|
|
.any(|(path, _, body)| path == "/v1/responses" && body["instructions"] == "rules"));
|
|
assert!(seen.iter().any(|(_, _, body)| {
|
|
body.get("temperature").is_none() && body["model"] == "cold-no-temp"
|
|
}));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn provider_factory_and_model_listing_cover_cloud_local_and_invalid_shapes() {
|
|
let (base, _state) = serve_mock().await;
|
|
let tmp = tempdir().expect("tempdir");
|
|
let mut config = temp_config(&tmp);
|
|
let _workspace_env = EnvVarGuard::set(
|
|
"OPENHUMAN_WORKSPACE",
|
|
config.config_path.parent().expect("config parent"),
|
|
);
|
|
seed_session(&config);
|
|
config.cloud_providers = vec![
|
|
CloudProviderCreds {
|
|
id: "custom-id".to_string(),
|
|
slug: "custom".to_string(),
|
|
label: "Custom".to_string(),
|
|
endpoint: format!("{base}/v1"),
|
|
auth_style: CloudAuthStyle::Bearer,
|
|
legacy_type: None,
|
|
default_model: Some("demo-chat".to_string()),
|
|
},
|
|
CloudProviderCreds {
|
|
id: "openrouter-id".to_string(),
|
|
slug: "openrouter".to_string(),
|
|
label: "OpenRouter".to_string(),
|
|
endpoint: format!("{base}/openrouter"),
|
|
auth_style: CloudAuthStyle::Bearer,
|
|
legacy_type: None,
|
|
default_model: None,
|
|
},
|
|
CloudProviderCreds {
|
|
id: "missing-id".to_string(),
|
|
slug: "missing".to_string(),
|
|
label: "Missing".to_string(),
|
|
endpoint: format!("{base}/missing"),
|
|
auth_style: CloudAuthStyle::None,
|
|
legacy_type: None,
|
|
default_model: None,
|
|
},
|
|
CloudProviderCreds {
|
|
id: "html-id".to_string(),
|
|
slug: "html".to_string(),
|
|
label: "HTML".to_string(),
|
|
endpoint: format!("{base}/html"),
|
|
auth_style: CloudAuthStyle::None,
|
|
legacy_type: None,
|
|
default_model: None,
|
|
},
|
|
CloudProviderCreds {
|
|
id: "wrong-data-id".to_string(),
|
|
slug: "wrong-data".to_string(),
|
|
label: "Wrong Data".to_string(),
|
|
endpoint: format!("{base}/wrong-data"),
|
|
auth_style: CloudAuthStyle::None,
|
|
legacy_type: None,
|
|
default_model: None,
|
|
},
|
|
CloudProviderCreds {
|
|
id: "error-payload-id".to_string(),
|
|
slug: "error-payload".to_string(),
|
|
label: "Error Payload".to_string(),
|
|
endpoint: format!("{base}/error-payload"),
|
|
auth_style: CloudAuthStyle::None,
|
|
legacy_type: None,
|
|
default_model: None,
|
|
},
|
|
];
|
|
config.chat_provider = Some("custom:demo-chat@0.4".to_string());
|
|
config.reasoning_provider = None;
|
|
config.local_ai.base_url = Some(base.clone());
|
|
|
|
AuthService::from_config(&config)
|
|
.store_provider_token(
|
|
&auth_key_for_slug("custom"),
|
|
DEFAULT_AUTH_PROFILE_NAME,
|
|
"custom-key",
|
|
HashMap::new(),
|
|
true,
|
|
)
|
|
.expect("store custom key");
|
|
AuthService::from_config(&config)
|
|
.store_provider_token(
|
|
&auth_key_for_slug("openrouter"),
|
|
DEFAULT_AUTH_PROFILE_NAME,
|
|
"openrouter-key",
|
|
HashMap::new(),
|
|
true,
|
|
)
|
|
.expect("store openrouter key");
|
|
config.save().await.expect("save temp config");
|
|
|
|
assert_eq!(provider_for_role("chat", &config), "custom:demo-chat@0.4");
|
|
assert_eq!(
|
|
provider_for_role("reasoning", &config),
|
|
"custom:demo-chat@0.4"
|
|
);
|
|
|
|
let (_provider, model) =
|
|
create_chat_provider_from_string("chat", "custom:demo-chat@0.4", &config)
|
|
.expect("cloud provider");
|
|
assert_eq!(model, "demo-chat");
|
|
|
|
let (_local_provider, local_model) =
|
|
create_chat_provider_from_string("chat", "ollama:gemma3:1b-it-qat@0.1", &config)
|
|
.expect("ollama provider");
|
|
assert_eq!(local_model, "gemma3:1b-it-qat");
|
|
|
|
let empty_model = match create_chat_provider_from_string("chat", "ollama:", &config) {
|
|
Ok(_) => panic!("expected empty model error"),
|
|
Err(err) => err,
|
|
};
|
|
assert!(empty_model.to_string().contains("empty model"));
|
|
|
|
let listed = list_configured_models("custom")
|
|
.await
|
|
.expect("list models")
|
|
.value;
|
|
assert_eq!(listed["models"][0]["id"], "demo-chat");
|
|
assert_eq!(listed["models"][1]["context_window"], 8192);
|
|
|
|
let local_listed = list_configured_models("ollama")
|
|
.await
|
|
.expect("synthetic ollama list")
|
|
.value;
|
|
assert_eq!(local_listed["models"][0]["id"], "demo-chat");
|
|
|
|
// PR #2959 reverted the list_models 404 suppression: a 404 from /models
|
|
// now surfaces as a real error instead of a synthetic `unsupported: true`
|
|
// success, so the failure fires to Sentry for a root-cause fix.
|
|
let missing_err = list_configured_models("missing")
|
|
.await
|
|
.expect_err("404 list_models now surfaces as an error");
|
|
assert!(
|
|
missing_err.contains("provider returned 404"),
|
|
"404 list_models error should surface the status: {missing_err:?}"
|
|
);
|
|
|
|
let openrouter = list_configured_models("openrouter")
|
|
.await
|
|
.expect("openrouter key validation and list")
|
|
.value;
|
|
assert_eq!(openrouter["models"][0]["owned_by"], "test-suite");
|
|
|
|
for provider_id in ["html", "wrong-data", "error-payload", ""] {
|
|
let err = list_configured_models(provider_id)
|
|
.await
|
|
.expect_err("invalid model listing");
|
|
assert!(
|
|
err.contains("provider")
|
|
|| err.contains("provider_id")
|
|
|| err.contains("OpenRouter")
|
|
|| err.contains("parse JSON")
|
|
);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn local_service_public_inference_assets_and_shutdown_use_loopback_ollama() {
|
|
let (base, _state) = serve_mock().await;
|
|
let _ollama_env = EnvVarGuard::set("OPENHUMAN_OLLAMA_BASE_URL", &base);
|
|
let tmp = tempdir().expect("tempdir");
|
|
let mut config = temp_config(&tmp);
|
|
config.local_ai.runtime_enabled = true;
|
|
config.local_ai.base_url = Some(base);
|
|
config.local_ai.chat_model_id = "gemma3:1b-it-qat".to_string();
|
|
config.local_ai.vision_model_id = "llava:mock".to_string();
|
|
config.local_ai.embedding_model_id = "bge-m3".to_string();
|
|
config.local_ai.preload_embedding_model = true;
|
|
config.local_ai.preload_vision_model = false;
|
|
config.local_ai.preload_stt_model = false;
|
|
config.local_ai.preload_tts_voice = false;
|
|
config.local_ai.whisper_in_process = false;
|
|
|
|
let service = LocalAiService::new(&config);
|
|
let prompt = service
|
|
.prompt(&config, "Say hi", Some(8), true)
|
|
.await
|
|
.expect("prompt");
|
|
assert_eq!(prompt, "generated final");
|
|
|
|
let summarized = service
|
|
.summarize(&config, "one two three", Some(16))
|
|
.await
|
|
.expect("summarize");
|
|
assert_eq!(summarized, "generated final");
|
|
|
|
let completion = service
|
|
.inline_complete_interactive(
|
|
&config,
|
|
"OpenHuman is",
|
|
"concise",
|
|
Some("short"),
|
|
&["OpenHuman is useful".to_string()],
|
|
Some(6),
|
|
)
|
|
.await
|
|
.expect("inline");
|
|
assert_eq!(completion, "generated final");
|
|
|
|
let assets = service.assets_status(&config).await.expect("assets");
|
|
assert!(assets.ollama_available);
|
|
assert_eq!(assets.chat.state, "ready");
|
|
assert_eq!(assets.embedding.state, "ready");
|
|
assert!(matches!(assets.stt.state.as_str(), "ondemand" | "missing"));
|
|
assert!(matches!(
|
|
assets.tts.state.as_str(),
|
|
"ready" | "ondemand" | "missing"
|
|
));
|
|
|
|
let diagnostics = service.diagnostics(&config).await.expect("diagnostics");
|
|
assert_eq!(diagnostics["ollama_running"], true);
|
|
assert_eq!(diagnostics["expected"]["chat_found"], true);
|
|
assert!(
|
|
diagnostics["installed_models"]
|
|
.as_array()
|
|
.expect("installed_models")
|
|
.len()
|
|
>= 4
|
|
);
|
|
|
|
let progress = service.downloads_progress(&config).await.expect("progress");
|
|
assert_eq!(progress.chat.id, "gemma3:1b-it-qat");
|
|
assert_eq!(progress.embedding.id, "bge-m3");
|
|
|
|
let disabled_config = Config::default();
|
|
let disabled_err = service
|
|
.prompt(&disabled_config, "disabled", None, false)
|
|
.await
|
|
.expect_err("disabled prompt");
|
|
assert_eq!(disabled_err, "local ai is disabled");
|
|
|
|
service.shutdown_owned_ollama(&config).await;
|
|
assert!(!service.has_owned_ollama());
|
|
}
|
|
|
|
async fn collect_deltas(rx: &mut tokio::sync::mpsc::Receiver<ProviderDelta>) -> Vec<ProviderDelta> {
|
|
let mut out = Vec::new();
|
|
while let Some(delta) = rx.recv().await {
|
|
out.push(delta);
|
|
}
|
|
out
|
|
}
|
|
|
|
fn temp_config(tmp: &TempDir) -> Config {
|
|
let root = tmp.path().join(".openhuman");
|
|
std::fs::create_dir_all(root.join("workspace")).expect("workspace dir");
|
|
let mut config = Config::default();
|
|
config.config_path = root.join("config.toml");
|
|
config.workspace_dir = root.join("workspace");
|
|
config.secrets.encrypt = false;
|
|
config.api_url = Some("http://127.0.0.1:9".to_string());
|
|
config
|
|
}
|
|
|
|
fn seed_session(config: &Config) {
|
|
AuthService::from_config(config)
|
|
.store_provider_token(
|
|
APP_SESSION_PROVIDER,
|
|
DEFAULT_AUTH_PROFILE_NAME,
|
|
"session-token",
|
|
HashMap::new(),
|
|
true,
|
|
)
|
|
.expect("seed session");
|
|
}
|
|
|
|
async fn serve_mock() -> (String, MockState) {
|
|
let state = MockState::default();
|
|
let app = Router::new()
|
|
.route("/v1/chat/completions", post(chat_completions))
|
|
.route("/v1/responses", post(responses))
|
|
.route("/v1/models", get(models))
|
|
.route("/missing/models", get(missing_models))
|
|
.route("/html/models", get(html_models))
|
|
.route("/wrong-data/models", get(wrong_data_models))
|
|
.route("/error-payload/models", get(error_payload_models))
|
|
.route("/openrouter/key", get(openrouter_key))
|
|
.route("/openrouter/models", get(models))
|
|
.route("/api/tags", get(ollama_tags))
|
|
.route("/api/show", post(ollama_show))
|
|
.route("/api/generate", post(ollama_generate))
|
|
.with_state(state.clone());
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
|
.await
|
|
.expect("bind mock");
|
|
let addr = listener.local_addr().expect("addr");
|
|
tokio::spawn(async move {
|
|
axum::serve(listener, app).await.expect("serve mock");
|
|
});
|
|
(format!("http://{addr}"), state)
|
|
}
|
|
|
|
async fn chat_completions(
|
|
State(state): State<MockState>,
|
|
headers: HeaderMap,
|
|
Json(body): Json<Value>,
|
|
) -> impl IntoResponse {
|
|
remember(&state, "/v1/chat/completions", &headers, body.clone());
|
|
let model = body["model"].as_str().unwrap_or_default();
|
|
if model == "responses-only" {
|
|
return (StatusCode::NOT_FOUND, Json(json!({"error": "not found"}))).into_response();
|
|
}
|
|
if model == "budget-model" {
|
|
return (
|
|
StatusCode::BAD_REQUEST,
|
|
Json(json!({"error": {"message": "budget exhausted for sk-very-secret"}})),
|
|
)
|
|
.into_response();
|
|
}
|
|
if model == "stream-model" && body["stream"] == true {
|
|
return sse_response();
|
|
}
|
|
if model == "tool-model" {
|
|
return Json(json!({
|
|
"choices": [{
|
|
"message": {
|
|
"content": "tool response",
|
|
"reasoning_content": "hidden reasoning",
|
|
"tool_calls": [{
|
|
"id": "call_1",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "lookup",
|
|
"arguments": { "query": "openhuman" }
|
|
}
|
|
}]
|
|
}
|
|
}],
|
|
"usage": {
|
|
"prompt_tokens": 1,
|
|
"completion_tokens": 2,
|
|
"prompt_tokens_details": { "cached_tokens": 1 }
|
|
},
|
|
"openhuman": {
|
|
"usage": {
|
|
"input_tokens": 11,
|
|
"output_tokens": 7,
|
|
"cached_input_tokens": 3
|
|
},
|
|
"billing": { "charged_amount_usd": 0.0042 }
|
|
}
|
|
}))
|
|
.into_response();
|
|
}
|
|
Json(json!({
|
|
"choices": [{
|
|
"message": {
|
|
"content": format!("chat:{model}"),
|
|
"function_call": {
|
|
"name": "legacy_tool",
|
|
"arguments": "{\"ok\":true}"
|
|
}
|
|
}
|
|
}]
|
|
}))
|
|
.into_response()
|
|
}
|
|
|
|
async fn responses(
|
|
State(state): State<MockState>,
|
|
headers: HeaderMap,
|
|
Json(body): Json<Value>,
|
|
) -> impl IntoResponse {
|
|
remember(&state, "/v1/responses", &headers, body);
|
|
Json(json!({
|
|
"output": [{
|
|
"content": [{ "type": "output_text", "text": "nested fallback text" }]
|
|
}],
|
|
"output_text": "responses fallback text"
|
|
}))
|
|
}
|
|
|
|
fn sse_response() -> Response<Body> {
|
|
let chunks = [
|
|
json!({"choices":[{"delta":{"content":"hello "}}]}).to_string(),
|
|
json!({"choices":[{"delta":{"reasoning_content":"thinking"}}]}).to_string(),
|
|
json!({"choices":[{"delta":{"content":"world","tool_calls":[{
|
|
"index":0,
|
|
"id":"call_stream",
|
|
"type":"function",
|
|
"function":{"name":"lookup","arguments":"{\"query\""}
|
|
}]}}]})
|
|
.to_string(),
|
|
json!({"choices":[{"delta":{"tool_calls":[{
|
|
"index":0,
|
|
"function":{"arguments":":\"stream\"}"}
|
|
}]}}]})
|
|
.to_string(),
|
|
json!({
|
|
"choices": [],
|
|
"usage": {
|
|
"prompt_tokens": 5,
|
|
"completion_tokens": 6,
|
|
"prompt_tokens_details": { "cached_tokens": 2 }
|
|
}
|
|
})
|
|
.to_string(),
|
|
];
|
|
let body = chunks
|
|
.into_iter()
|
|
.map(|chunk| format!("data: {chunk}\n\n"))
|
|
.chain(std::iter::once("data: [DONE]\n\n".to_string()))
|
|
.collect::<String>();
|
|
Response::builder()
|
|
.status(StatusCode::OK)
|
|
.header(header::CONTENT_TYPE, "text/event-stream")
|
|
.body(Body::from(body))
|
|
.expect("sse response")
|
|
}
|
|
|
|
async fn models(State(state): State<MockState>, headers: HeaderMap) -> impl IntoResponse {
|
|
remember(&state, "/v1/models", &headers, Value::Null);
|
|
Json(json!({
|
|
"object": "list",
|
|
"data": [
|
|
{ "id": "demo-chat", "owned_by": "test-suite" },
|
|
{ "id": "demo-coder", "owned_by": "test-suite", "context_window": 8192 },
|
|
{ "owned_by": "ignored-without-id" }
|
|
]
|
|
}))
|
|
}
|
|
|
|
async fn missing_models() -> impl IntoResponse {
|
|
(
|
|
StatusCode::NOT_FOUND,
|
|
Json(json!({"error": "models unsupported"})),
|
|
)
|
|
}
|
|
|
|
async fn html_models() -> impl IntoResponse {
|
|
(StatusCode::OK, "<html>login</html>")
|
|
}
|
|
|
|
async fn wrong_data_models() -> impl IntoResponse {
|
|
Json(json!({ "object": "error", "data": { "message": "wrong shape" } }))
|
|
}
|
|
|
|
async fn error_payload_models() -> impl IntoResponse {
|
|
Json(json!({ "error": { "message": "bad sk-secret-key" } }))
|
|
}
|
|
|
|
async fn openrouter_key(headers: HeaderMap) -> impl IntoResponse {
|
|
let auth = header_value(&headers, "authorization").unwrap_or_default();
|
|
if auth == "Bearer openrouter-key" {
|
|
Json(json!({"data": {"label": "ok"}})).into_response()
|
|
} else {
|
|
(
|
|
StatusCode::UNAUTHORIZED,
|
|
Json(json!({"error": "missing key"})),
|
|
)
|
|
.into_response()
|
|
}
|
|
}
|
|
|
|
async fn ollama_tags() -> impl IntoResponse {
|
|
Json(json!({
|
|
"models": [
|
|
{ "name": "gemma3:1b-it-qat", "model": "gemma3:1b-it-qat" },
|
|
{ "name": "llava:mock", "model": "llava:mock" },
|
|
{ "name": "bge-m3", "model": "bge-m3" },
|
|
{ "name": "demo-chat", "model": "demo-chat" }
|
|
]
|
|
}))
|
|
}
|
|
|
|
async fn ollama_show(Json(body): Json<Value>) -> impl IntoResponse {
|
|
let model = body
|
|
.get("model")
|
|
.or_else(|| body.get("name"))
|
|
.and_then(Value::as_str)
|
|
.unwrap_or_default();
|
|
if model == "___nonexistent_probe___" {
|
|
return (
|
|
StatusCode::NOT_FOUND,
|
|
Json(json!({"error": "model not found"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
Json(json!({
|
|
"model_info": {
|
|
"general.context_length": if model == "bge-m3" { 8192 } else { 4096 }
|
|
}
|
|
}))
|
|
.into_response()
|
|
}
|
|
|
|
async fn ollama_generate(Json(_body): Json<Value>) -> impl IntoResponse {
|
|
Json(json!({
|
|
"model": "gemma3:1b-it-qat",
|
|
"response": "generated final",
|
|
"done": true,
|
|
"prompt_eval_count": 8,
|
|
"prompt_eval_duration": 400000000,
|
|
"eval_count": 6,
|
|
"eval_duration": 300000000
|
|
}))
|
|
}
|
|
|
|
fn remember(state: &MockState, path: &str, headers: &HeaderMap, body: Value) {
|
|
state.requests.lock().expect("requests").push((
|
|
path.to_string(),
|
|
header_value(headers, "authorization"),
|
|
body,
|
|
));
|
|
}
|
|
|
|
fn header_value(headers: &HeaderMap, key: &str) -> Option<String> {
|
|
headers
|
|
.get(key)
|
|
.and_then(|value| value.to_str().ok())
|
|
.map(ToOwned::to_owned)
|
|
}
|