use std::cell::RefCell;
use std::collections::BTreeMap;
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: crate::llm_config::model_family(provider, model),
capabilities,
}
}
pub fn to_vm_value(&self) -> VmValue {
let mut dict = BTreeMap::new();
dict.insert(
"provider".to_string(),
VmValue::String(std::sync::Arc::from(self.provider.as_str())),
);
dict.insert(
"model".to_string(),
VmValue::String(std::sync::Arc::from(self.model.as_str())),
);
dict.insert(
"family".to_string(),
VmValue::String(std::sync::Arc::from(self.family.as_str())),
);
dict.insert("capabilities".to_string(), self.capabilities.clone());
VmValue::Dict(std::sync::Arc::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();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn derive_family(provider: &str, model: &str) -> String {
crate::llm_config::model_family(provider, model)
}
#[test]
fn family_from_model_id_takes_precedence() {
assert_eq!(
derive_family("openrouter", "anthropic/claude-3-5-sonnet"),
"anthropic-claude"
);
assert_eq!(derive_family("openrouter", "openai/gpt-4o"), "openai-gpt");
assert_eq!(
derive_family("openrouter", "google/gemini-1.5-pro"),
"google-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"),
"anthropic-claude"
);
assert_eq!(derive_family("azure", "deployment-xyz"), "openai-gpt");
assert_eq!(derive_family("vertex", "model-xyz"), "google-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("anthropic-claude".to_string()),
);
push_llm_render_context(LlmRenderContext::resolve("openai", "gpt-4o"));
assert_eq!(
current_llm_render_context().map(|c| c.family),
Some("openai-gpt".to_string()),
);
pop_llm_render_context();
assert_eq!(
current_llm_render_context().map(|c| c.family),
Some("anthropic-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());
}
}