use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
use crate::value::VmValue;
#[derive(Debug, Clone)]
pub struct LlmRenderContext {
pub provider: String,
pub model: String,
pub family: String,
pub capabilities: VmValue,
}
impl LlmRenderContext {
pub fn resolve(provider: &str, model: &str) -> Self {
let caps = crate::llm::capabilities::lookup(provider, model);
let capabilities =
crate::llm::config_builtins::capabilities_to_vm_value(provider, model, &caps);
Self {
provider: provider.to_string(),
model: model.to_string(),
family: derive_family(provider, model),
capabilities,
}
}
pub fn to_vm_value(&self) -> VmValue {
let mut dict = BTreeMap::new();
dict.insert(
"provider".to_string(),
VmValue::String(Rc::from(self.provider.as_str())),
);
dict.insert(
"model".to_string(),
VmValue::String(Rc::from(self.model.as_str())),
);
dict.insert(
"family".to_string(),
VmValue::String(Rc::from(self.family.as_str())),
);
dict.insert("capabilities".to_string(), self.capabilities.clone());
VmValue::Dict(Rc::new(dict))
}
}
thread_local! {
static LLM_RENDER_STACK: RefCell<Vec<LlmRenderContext>> = const { RefCell::new(Vec::new()) };
}
pub fn push_llm_render_context(ctx: LlmRenderContext) {
LLM_RENDER_STACK.with(|stack| stack.borrow_mut().push(ctx));
}
pub fn pop_llm_render_context() -> Option<LlmRenderContext> {
LLM_RENDER_STACK.with(|stack| stack.borrow_mut().pop())
}
pub fn current_llm_render_context() -> Option<LlmRenderContext> {
LLM_RENDER_STACK.with(|stack| stack.borrow().last().cloned())
}
pub(crate) fn reset_llm_render_stack() {
LLM_RENDER_STACK.with(|stack| stack.borrow_mut().clear());
}
pub struct LlmRenderContextGuard {
expected_depth: usize,
}
impl LlmRenderContextGuard {
pub fn enter(ctx: LlmRenderContext) -> Self {
push_llm_render_context(ctx);
let depth = LLM_RENDER_STACK.with(|stack| stack.borrow().len());
Self {
expected_depth: depth,
}
}
}
impl Drop for LlmRenderContextGuard {
fn drop(&mut self) {
let depth = LLM_RENDER_STACK.with(|stack| stack.borrow().len());
debug_assert_eq!(
depth, self.expected_depth,
"LlmRenderContextGuard nested-drop order violated",
);
pop_llm_render_context();
}
}
fn derive_family(provider: &str, model: &str) -> String {
let model_lc = model.to_ascii_lowercase();
const MARKERS: &[(&str, &[&str])] = &[
("claude", &["claude"]),
("gpt", &["gpt-", "gpt_", "o1-", "o3-", "o4-"]),
("gemini", &["gemini"]),
("qwen", &["qwen"]),
("llama", &["llama"]),
("mistral", &["mistral", "mixtral"]),
("deepseek", &["deepseek"]),
("phi", &["phi-", "phi_"]),
("grok", &["grok"]),
("command", &["command-", "command_"]),
];
for (family, needles) in MARKERS {
if needles.iter().any(|needle| model_lc.contains(needle)) {
return (*family).to_string();
}
}
match provider {
"anthropic" | "bedrock" | "vertex-anthropic" => "claude".to_string(),
"openai" | "azure" => "gpt".to_string(),
"gemini" | "vertex" | "google" => "gemini".to_string(),
other if !other.is_empty() => other.to_string(),
_ => "unknown".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn family_from_model_id_takes_precedence() {
assert_eq!(
derive_family("openrouter", "anthropic/claude-3-5-sonnet"),
"claude"
);
assert_eq!(derive_family("openrouter", "openai/gpt-4o"), "gpt");
assert_eq!(
derive_family("openrouter", "google/gemini-1.5-pro"),
"gemini"
);
assert_eq!(
derive_family("ollama", "qwen3.6:35b-a3b-coding-nvfp4"),
"qwen"
);
}
#[test]
fn family_falls_back_to_provider_alias() {
assert_eq!(derive_family("anthropic", "unknown-future-model"), "claude");
assert_eq!(derive_family("azure", "deployment-xyz"), "gpt");
assert_eq!(derive_family("vertex", "model-xyz"), "gemini");
assert_eq!(derive_family("local", "anonymous-snapshot"), "local");
assert_eq!(derive_family("", ""), "unknown");
}
#[test]
fn push_pop_stack_round_trip() {
reset_llm_render_stack();
assert!(current_llm_render_context().is_none());
push_llm_render_context(LlmRenderContext::resolve("anthropic", "claude-3-5-sonnet"));
assert_eq!(
current_llm_render_context().map(|c| c.family),
Some("claude".to_string()),
);
push_llm_render_context(LlmRenderContext::resolve("openai", "gpt-4o"));
assert_eq!(
current_llm_render_context().map(|c| c.family),
Some("gpt".to_string()),
);
pop_llm_render_context();
assert_eq!(
current_llm_render_context().map(|c| c.family),
Some("claude".to_string()),
);
pop_llm_render_context();
assert!(current_llm_render_context().is_none());
}
#[test]
fn guard_pops_on_drop() {
reset_llm_render_stack();
{
let _guard = LlmRenderContextGuard::enter(LlmRenderContext::resolve(
"anthropic",
"claude-3-5-sonnet",
));
assert!(current_llm_render_context().is_some());
}
assert!(current_llm_render_context().is_none());
}
}