harn_vm/stdlib/template/
llm_context.rs1use crate::value::VmDictExt;
26use std::cell::RefCell;
27use std::collections::BTreeMap;
28
29use crate::value::VmValue;
30
31#[derive(Debug, Clone)]
35pub struct LlmRenderContext {
36 pub provider: String,
37 pub model: String,
38 pub family: String,
39 pub capabilities: VmValue,
42}
43
44impl LlmRenderContext {
45 pub fn resolve(provider: &str, model: &str) -> Self {
48 let caps = crate::llm::capabilities::lookup(provider, model);
49 let capabilities =
50 crate::llm::config_builtins::capabilities_to_vm_value(provider, model, &caps);
51 Self {
52 provider: provider.to_string(),
53 model: model.to_string(),
54 family: crate::llm_config::model_family(provider, model),
55 capabilities,
56 }
57 }
58
59 pub fn to_vm_value(&self) -> VmValue {
62 let mut dict = BTreeMap::new();
63 dict.put_str("provider", self.provider.as_str());
64 dict.put_str("model", self.model.as_str());
65 dict.put_str("family", self.family.as_str());
66 dict.insert("capabilities".to_string(), self.capabilities.clone());
67 VmValue::dict(dict)
68 }
69}
70
71thread_local! {
72 static LLM_RENDER_STACK: RefCell<Vec<LlmRenderContext>> = const { RefCell::new(Vec::new()) };
73}
74
75pub fn push_llm_render_context(ctx: LlmRenderContext) {
79 LLM_RENDER_STACK.with(|stack| stack.borrow_mut().push(ctx));
80}
81
82pub fn pop_llm_render_context() -> Option<LlmRenderContext> {
86 LLM_RENDER_STACK.with(|stack| stack.borrow_mut().pop())
87}
88
89pub fn current_llm_render_context() -> Option<LlmRenderContext> {
93 LLM_RENDER_STACK.with(|stack| stack.borrow().last().cloned())
94}
95
96pub(crate) fn reset_llm_render_stack() {
99 LLM_RENDER_STACK.with(|stack| stack.borrow_mut().clear());
100}
101
102pub struct LlmRenderContextGuard {
106 expected_depth: usize,
110}
111
112impl LlmRenderContextGuard {
113 pub fn enter(ctx: LlmRenderContext) -> Self {
114 push_llm_render_context(ctx);
115 let depth = LLM_RENDER_STACK.with(|stack| stack.borrow().len());
116 Self {
117 expected_depth: depth,
118 }
119 }
120}
121
122impl Drop for LlmRenderContextGuard {
123 fn drop(&mut self) {
124 let depth = LLM_RENDER_STACK.with(|stack| stack.borrow().len());
125 debug_assert_eq!(
126 depth, self.expected_depth,
127 "LlmRenderContextGuard nested-drop order violated",
128 );
129 pop_llm_render_context();
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 fn derive_family(provider: &str, model: &str) -> String {
138 crate::llm_config::model_family(provider, model)
139 }
140
141 #[test]
142 fn family_from_model_id_takes_precedence() {
143 assert_eq!(
144 derive_family("openrouter", "anthropic/claude-3-5-sonnet"),
145 "anthropic-claude"
146 );
147 assert_eq!(derive_family("openrouter", "openai/gpt-4o"), "openai-gpt");
148 assert_eq!(
149 derive_family("openrouter", "google/gemini-1.5-pro"),
150 "google-gemini"
151 );
152 assert_eq!(derive_family("llamacpp", "qwen3.6-35b-a3b"), "qwen");
153 }
154
155 #[test]
156 fn family_falls_back_to_provider_alias() {
157 assert_eq!(
158 derive_family("anthropic", "unknown-future-model"),
159 "anthropic-claude"
160 );
161 assert_eq!(derive_family("azure", "deployment-xyz"), "openai-gpt");
162 assert_eq!(derive_family("vertex", "model-xyz"), "google-gemini");
163 assert_eq!(derive_family("local", "anonymous-snapshot"), "local");
164 assert_eq!(derive_family("", ""), "unknown");
165 }
166
167 #[test]
168 fn push_pop_stack_round_trip() {
169 reset_llm_render_stack();
170 assert!(current_llm_render_context().is_none());
171 push_llm_render_context(LlmRenderContext::resolve("anthropic", "claude-3-5-sonnet"));
172 assert_eq!(
173 current_llm_render_context().map(|c| c.family),
174 Some("anthropic-claude".to_string()),
175 );
176 push_llm_render_context(LlmRenderContext::resolve("openai", "gpt-4o"));
177 assert_eq!(
178 current_llm_render_context().map(|c| c.family),
179 Some("openai-gpt".to_string()),
180 );
181 pop_llm_render_context();
182 assert_eq!(
183 current_llm_render_context().map(|c| c.family),
184 Some("anthropic-claude".to_string()),
185 );
186 pop_llm_render_context();
187 assert!(current_llm_render_context().is_none());
188 }
189
190 #[test]
191 fn guard_pops_on_drop() {
192 reset_llm_render_stack();
193 {
194 let _guard = LlmRenderContextGuard::enter(LlmRenderContext::resolve(
195 "anthropic",
196 "claude-3-5-sonnet",
197 ));
198 assert!(current_llm_render_context().is_some());
199 }
200 assert!(current_llm_render_context().is_none());
201 }
202}