Add native LLM calling functions (llm_call, llm_parallel, llm_configure) to El VM

This commit is contained in:
Will Anderson
2026-04-29 04:04:11 -05:00
parent 8d4d9ed786
commit 7bdfb9bbcd
+267
View File
@@ -5996,6 +5996,110 @@ fn dispatch_builtin(
stack.push(Value::Nil); BuiltinResult::Handled
}
// ── LLM native functions ──────────────────────────────────────────────
// llm_call(model, prompt) -> String
"llm_call" => {
let prompt = match stack.pop().unwrap_or(Value::Str(String::new())) {
Value::Str(s) => s,
_ => String::new(),
};
let model = match stack.pop().unwrap_or(Value::Str("claude".to_string())) {
Value::Str(s) => s,
_ => "claude".to_string(),
};
let result = call_llm_blocking(&model, None, &prompt);
stack.push(Value::Str(result));
BuiltinResult::Handled
}
// llm_call_system(model, system, prompt) -> String
"llm_call_system" => {
let prompt = match stack.pop().unwrap_or(Value::Str(String::new())) {
Value::Str(s) => s,
_ => String::new(),
};
let system = match stack.pop().unwrap_or(Value::Str(String::new())) {
Value::Str(s) => s,
_ => String::new(),
};
let model = match stack.pop().unwrap_or(Value::Str("claude".to_string())) {
Value::Str(s) => s,
_ => "claude".to_string(),
};
let result = call_llm_blocking(&model, Some(&system), &prompt);
stack.push(Value::Str(result));
BuiltinResult::Handled
}
// llm_stream(model, prompt) -> String (blocking for now; streaming is future work)
"llm_stream" => {
let prompt = match stack.pop().unwrap_or(Value::Str(String::new())) {
Value::Str(s) => s,
_ => String::new(),
};
let model = match stack.pop().unwrap_or(Value::Str("claude".to_string())) {
Value::Str(s) => s,
_ => "claude".to_string(),
};
let result = call_llm_blocking(&model, None, &prompt);
stack.push(Value::Str(result));
BuiltinResult::Handled
}
// llm_parallel(models: List, prompt: String) -> Map
"llm_parallel" => {
let prompt = match stack.pop().unwrap_or(Value::Str(String::new())) {
Value::Str(s) => s,
_ => String::new(),
};
let models: Vec<String> = match stack.pop().unwrap_or(Value::List(vec![])) {
Value::List(l) => l.into_iter().filter_map(|v| {
if let Value::Str(s) = v { Some(s) } else { None }
}).collect(),
_ => vec![],
};
let mut pairs: Vec<(String, Value)> = Vec::new();
for model in models {
let response = call_llm_blocking(&model, None, &prompt);
pairs.push((model, Value::Str(response)));
}
stack.push(Value::Map(pairs));
BuiltinResult::Handled
}
// llm_models() -> List
"llm_models" => {
let mut model_names: Vec<String> = LLM_CONFIG.with(|c| {
c.borrow().keys().cloned().collect()
});
model_names.sort();
let list = model_names.into_iter().map(Value::Str).collect();
stack.push(Value::List(list));
BuiltinResult::Handled
}
// llm_configure(name, endpoint, api_key) -> Bool
"llm_configure" => {
let api_key = match stack.pop().unwrap_or(Value::Str(String::new())) {
Value::Str(s) => s,
_ => String::new(),
};
let endpoint = match stack.pop().unwrap_or(Value::Str(String::new())) {
Value::Str(s) => s,
_ => String::new(),
};
let name = match stack.pop().unwrap_or(Value::Str(String::new())) {
Value::Str(s) => s,
_ => String::new(),
};
LLM_CONFIG.with(|c| {
c.borrow_mut().insert(name, (endpoint, api_key));
});
stack.push(Value::Bool(true));
BuiltinResult::Handled
}
_ => BuiltinResult::NotBuiltin,
}
}
@@ -6132,6 +6236,167 @@ thread_local! {
static OBSERVER_ID_COUNTER: std::cell::Cell<i64> = std::cell::Cell::new(1);
}
thread_local! {
/// LLM model name → (endpoint_url, api_key)
static LLM_CONFIG: std::cell::RefCell<std::collections::HashMap<String, (String, String)>> =
std::cell::RefCell::new({
let mut m = std::collections::HashMap::new();
// Claude via Anthropic API
m.insert("claude".to_string(), (
"https://api.anthropic.com/v1/messages".to_string(),
std::env::var("ANTHROPIC_API_KEY").unwrap_or_default(),
));
m.insert("claude-3-5-sonnet".to_string(), (
"https://api.anthropic.com/v1/messages".to_string(),
std::env::var("ANTHROPIC_API_KEY").unwrap_or_default(),
));
m.insert("claude-opus".to_string(), (
"https://api.anthropic.com/v1/messages".to_string(),
std::env::var("ANTHROPIC_API_KEY").unwrap_or_default(),
));
// OpenAI
m.insert("gpt4o".to_string(), (
"https://api.openai.com/v1/chat/completions".to_string(),
std::env::var("OPENAI_API_KEY").unwrap_or_default(),
));
m.insert("gpt4".to_string(), (
"https://api.openai.com/v1/chat/completions".to_string(),
std::env::var("OPENAI_API_KEY").unwrap_or_default(),
));
// Local Ollama
m.insert("ollama".to_string(), (
"http://localhost:11434/api/chat".to_string(),
String::new(),
));
// Neuron (the platform itself)
m.insert("neuron".to_string(), (
std::env::var("NEURON_ENDPOINT")
.unwrap_or_else(|_| "https://api.neurontechnologies.ai/v1/chat/completions".to_string()),
std::env::var("NEURON_API_KEY").unwrap_or_default(),
));
m
});
}
// ── LLM helper functions ──────────────────────────────────────────────────────
/// Map a short model name to the actual Anthropic model id string.
fn get_anthropic_model(name: &str) -> &'static str {
match name {
"claude-opus" | "claude-3-opus" => "claude-opus-4-5",
"claude-haiku" => "claude-haiku-4-5",
_ => "claude-sonnet-4-5",
}
}
/// Call an LLM synchronously. Returns the response text (or an error string).
/// Supports Anthropic, Ollama, and OpenAI-compatible endpoints based on URL.
fn call_llm_blocking(model: &str, system: Option<&str>, prompt: &str) -> String {
let (endpoint, api_key) = LLM_CONFIG.with(|c| {
c.borrow()
.get(model)
.cloned()
.unwrap_or_else(|| ("http://localhost:11434/api/chat".to_string(), String::new()))
});
let client = match reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(120))
.build()
{
Ok(c) => c,
Err(e) => return format!("llm_call error: {}", e),
};
if endpoint.contains("anthropic.com") {
// Anthropic Messages API
let body = if let Some(sys) = system {
serde_json::json!({
"model": get_anthropic_model(model),
"max_tokens": 4096,
"system": sys,
"messages": [{"role": "user", "content": prompt}]
})
} else {
serde_json::json!({
"model": get_anthropic_model(model),
"max_tokens": 4096,
"messages": [{"role": "user", "content": prompt}]
})
};
match client
.post(&endpoint)
.header("x-api-key", &api_key)
.header("anthropic-version", "2023-06-01")
.header("content-type", "application/json")
.json(&body)
.send()
{
Ok(r) => match r.json::<serde_json::Value>() {
Ok(v) => v["content"][0]["text"].as_str().unwrap_or("").to_string(),
Err(e) => format!("llm_call error: {}", e),
},
Err(e) => format!("llm_call error: {}", e),
}
} else if endpoint.contains("ollama") || endpoint.contains("11434") {
// Ollama chat API
let model_name = model.trim_start_matches("ollama:").to_string();
let messages_arr = if let Some(sys) = system {
serde_json::json!([
{"role": "system", "content": sys},
{"role": "user", "content": prompt}
])
} else {
serde_json::json!([{"role": "user", "content": prompt}])
};
let body = serde_json::json!({
"model": if model_name == "ollama" { "llama3.2".to_string() } else { model_name },
"messages": messages_arr,
"stream": false
});
match client.post(&endpoint).json(&body).send() {
Ok(r) => match r.json::<serde_json::Value>() {
Ok(v) => v["message"]["content"].as_str().unwrap_or("").to_string(),
Err(e) => format!("llm_call error: {}", e),
},
Err(e) => format!("llm_call error: {}", e),
}
} else {
// OpenAI-compatible format (OpenAI, Neuron, and most others)
let messages_arr = if let Some(sys) = system {
serde_json::json!([
{"role": "system", "content": sys},
{"role": "user", "content": prompt}
])
} else {
serde_json::json!([{"role": "user", "content": prompt}])
};
let model_id = model
.replace("gpt4o", "gpt-4o")
.replace("gpt4", "gpt-4-turbo");
let body = serde_json::json!({
"model": model_id,
"messages": messages_arr,
"max_tokens": 4096
});
match client
.post(&endpoint)
.header("Authorization", format!("Bearer {}", api_key))
.header("content-type", "application/json")
.json(&body)
.send()
{
Ok(r) => match r.json::<serde_json::Value>() {
Ok(v) => v["choices"][0]["message"]["content"]
.as_str()
.unwrap_or("")
.to_string(),
Err(e) => format!("llm_call error: {}", e),
},
Err(e) => format!("llm_call error: {}", e),
}
}
}
/// Compare two runtime values for ordering (used by Lt/Gt/LtEq/GtEq).
fn cmp_values(a: &el_compiler::Value, b: &el_compiler::Value) -> std::cmp::Ordering {
use el_compiler::Value;
@@ -6192,6 +6457,8 @@ fn is_builtin(name: &str) -> bool {
| "time_add" | "time_diff" | "time_start_of" | "time_tz_offset" | "time_to_tz"
// Observer
| "observe" | "unobserve"
// LLM native functions
| "llm_call" | "llm_call_system" | "llm_stream" | "llm_parallel" | "llm_models" | "llm_configure"
)
}