1use oino_ai::{JsonValue, Message, Model, ThinkingLevel};
5use oino_models::resolver::{
6 ThinkingSwitch, next_available_model, next_scoped_model, thinking_level_for_model_switch,
7};
8use oino_models::virtual_models::{
9 BranchEntryRef, ModelRoute, RouteReason, RouteResult, VIRTUAL_MODEL_STATE_ENTRY,
10 VirtualModelStateData, branch_selection, find_latest_response, is_virtual_api,
11 should_store_route_state, virtual_model_state,
12};
13use oino_session::{EntryKind, SessionEntry};
14use serde::{Deserialize, Serialize};
15use serde_json::json;
16use tokio_util::sync::CancellationToken;
17
18use super::{AgentSession, SessionOpError, lock};
19use crate::events::{AgentSessionEvent, ModelChangeSource};
20use crate::extensions::api::CallKind;
21
22pub use oino_models::resolver::CycleDirection;
23
24#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct CycleModelResult {
28 pub model: Model,
30 pub thinking_level: ThinkingLevel,
32 pub is_scoped: bool,
34}
35
36pub(crate) fn branch_refs(branch: &[SessionEntry]) -> Vec<BranchEntryRef<'_>> {
38 branch
39 .iter()
40 .map(|entry| match &entry.kind {
41 EntryKind::ModelChange { provider, model_id } => {
42 BranchEntryRef::ModelChange { provider, model_id }
43 }
44 EntryKind::Message {
45 message: oino_ai::AgentMessage::Assistant(assistant),
46 } => BranchEntryRef::Assistant {
47 provider: &assistant.provider,
48 model: &assistant.model,
49 api: &assistant.api,
50 },
51 EntryKind::Custom { custom_type, data } => BranchEntryRef::Custom {
52 custom_type,
53 data: data.as_ref(),
54 },
55 _ => BranchEntryRef::Other,
56 })
57 .collect()
58}
59
60impl AgentSession {
61 #[must_use]
63 pub fn routed_model(&self) -> Option<Model> {
64 let model = self.model()?;
65 if !is_virtual_api(&model.api) {
66 return None;
67 }
68 let llm = oino_session::convert_to_llm(&self.messages());
69 let latest = find_latest_response(&llm)?;
70 self.inner
71 .models
72 .find_physical(&latest.provider, &latest.model)
73 }
74
75 #[must_use]
77 pub fn available_thinking_levels(&self) -> Vec<ThinkingLevel> {
78 self.model().map_or_else(
79 || ThinkingLevel::ALL.to_vec(),
80 |model| model.available_thinking_levels(),
81 )
82 }
83
84 fn thinking_for_switch(
85 &self,
86 target: Option<&Model>,
87 explicit: Option<ThinkingLevel>,
88 ) -> ThinkingLevel {
89 let settings = lock(&self.inner.settings).snapshot();
90 let levels = settings.model_thinking_levels();
91 thinking_level_for_model_switch(
92 target,
93 &ThinkingSwitch {
94 explicit,
95 model_thinking_levels: Some(&levels),
96 default_thinking_level: settings.default_thinking_level(),
97 current: Some(self.thinking_level()),
98 },
99 )
100 }
101
102 async fn emit_model_select(&self, next: &Model, previous: Option<&Model>, source: &str) {
103 if previous.is_some_and(|p| p.provider == next.provider && p.id == next.id) {
104 return;
105 }
106 let runner = self.runner();
107 if runner.has_handlers("model_select") {
108 let mut payload = json!({"type": "model_select", "model": next, "source": source});
109 if let Some(previous) = previous {
110 payload["previousModel"] = json!(previous);
111 }
112 runner.emit(payload).await;
113 }
114 }
115
116 fn apply_model(&self, model: &Model, persist: bool) -> Result<(), SessionOpError> {
117 self.inner.agent.set_model(model.clone());
118 lock(&self.inner.manager)
119 .append_model_change(&model.provider, &model.id)
120 .map_err(|e| SessionOpError::Failed(e.to_string()))?;
121 if persist {
122 let result = self
123 .with_settings(|s| s.set_default_model_and_provider(&model.provider, &model.id));
124 if let Err(error) = result {
125 tracing::warn!("saving default model failed: {error}");
126 }
127 self.add_persisted_default_to_scope(model);
128 }
129 Ok(())
130 }
131
132 fn add_persisted_default_to_scope(&self, model: &Model) {
133 let mut scope = lock(&self.inner.scoped_models);
134 if scope.is_empty()
135 || scope
136 .iter()
137 .any(|s| s.model.provider == model.provider && s.model.id == model.id)
138 {
139 return;
140 }
141 scope.push(oino_models::resolver::ScopedModel {
142 model: model.clone(),
143 thinking_level: None,
144 });
145 drop(scope);
146 let enabled = lock(&self.inner.settings).settings().enabled_models();
147 let Some(mut enabled) = enabled.filter(|e| !e.is_empty()) else {
148 return;
149 };
150 let reference = format!("{}/{}", model.provider, model.id);
151 if enabled.iter().any(|p| p.eq_ignore_ascii_case(&reference)) {
152 return;
153 }
154 enabled.push(reference);
155 if let Err(error) = self.with_settings(|s| s.set_enabled_models(Some(&enabled))) {
156 tracing::warn!("saving enabledModels failed: {error}");
157 }
158 }
159
160 pub async fn set_model(&self, model: Model, persist: bool) -> Result<Model, SessionOpError> {
166 if self.inner.models.check_auth(&model.provider).is_none() {
167 return Err(SessionOpError::Failed(format!(
168 "No API key for {}/{}",
169 model.provider, model.id
170 )));
171 }
172 let previous = self.model();
173 let thinking = self.thinking_for_switch(Some(&model), None);
174 self.apply_model(&model, persist)?;
175 self.set_thinking_level(thinking, false).await;
176 self.emit_model_select(&model, previous.as_ref(), "set")
177 .await;
178 self.emit_model_changed(ModelChangeSource::Set);
179 Ok(model)
180 }
181
182 pub async fn cycle_model(
188 &self,
189 direction: CycleDirection,
190 persist: bool,
191 ) -> Result<Option<CycleModelResult>, SessionOpError> {
192 let available = self.inner.models.available();
193 let current = self.model();
194 let scope = self.scoped_models();
195 let (next, explicit, is_scoped) = if scope.is_empty() {
196 match next_available_model(&available, current.as_ref(), direction) {
197 Some(model) => (model, None, false),
198 None => return Ok(None),
199 }
200 } else {
201 match next_scoped_model(&scope, &available, current.as_ref(), direction) {
202 Some(scoped) => (scoped.model, scoped.thinking_level, true),
203 None => return Ok(None),
204 }
205 };
206 let thinking = self.thinking_for_switch(Some(&next), explicit);
207 self.apply_model(&next, persist)?;
208 self.set_thinking_level(thinking, false).await;
209 self.emit_model_select(&next, current.as_ref(), "cycle")
210 .await;
211 self.emit_model_changed(ModelChangeSource::Cycle);
212 Ok(Some(CycleModelResult {
213 model: next,
214 thinking_level: self.thinking_level(),
215 is_scoped,
216 }))
217 }
218
219 pub async fn set_thinking_level(&self, level: ThinkingLevel, persist: bool) -> ThinkingLevel {
221 let available = self.available_thinking_levels();
222 let effective = if available.contains(&level) {
223 level
224 } else if self.model().is_some() {
225 level.clamp(&available)
226 } else {
227 ThinkingLevel::Off
228 };
229 let previous = self.thinking_level();
230 self.inner.agent.set_thinking_level(effective);
231 if persist && let Err(error) = self.with_settings(|s| s.set_default_thinking_level(level)) {
232 tracing::warn!("saving defaultThinkingLevel failed: {error}");
233 }
234 if effective != previous {
235 if let Err(error) =
236 lock(&self.inner.manager).append_thinking_level_change(&effective.to_string())
237 {
238 tracing::error!("persisting thinking level failed: {error}");
239 }
240 self.emit(AgentSessionEvent::ThinkingLevelChanged { level: effective });
241 let runner = self.runner();
242 if runner.has_handlers("thinking_level_select") {
243 runner
244 .emit(json!({"type": "thinking_level_select", "level": effective, "previousLevel": previous}))
245 .await;
246 }
247 }
248 effective
249 }
250
251 pub async fn cycle_thinking_level(&self, persist: bool) -> Option<ThinkingLevel> {
253 if !self.model().is_some_and(|m| m.reasoning) {
254 return None;
255 }
256 let levels = self.available_thinking_levels();
257 let index = levels.iter().position(|l| *l == self.thinking_level());
258 let next = levels[index.map_or(0, |i| (i + 1) % levels.len())];
259 self.set_thinking_level(next, persist).await;
260 Some(next)
261 }
262
263 pub(crate) fn record_selection(&self) {
265 let Some(model) = self.model() else {
266 return;
267 };
268 let models = &self.inner.models;
269 let recorded = {
270 let manager = lock(&self.inner.manager);
271 let branch: Vec<SessionEntry> = manager.branch(None).into_iter().cloned().collect();
272 branch_selection(&branch_refs(&branch), |provider, id| {
273 models.virtual_model(provider, id).is_some()
274 })
275 };
276 let Some(recorded) = recorded else {
277 return;
278 };
279 if recorded.provider == model.provider && recorded.model_id == model.id {
280 return;
281 }
282 let recorded_virtual = models
283 .find(&recorded.provider, &recorded.model_id)
284 .is_some_and(|m| is_virtual_api(&m.api));
285 if !is_virtual_api(&model.api) && !recorded_virtual {
286 return;
287 }
288 if let Err(error) =
289 lock(&self.inner.manager).append_model_change(&model.provider, &model.id)
290 {
291 tracing::error!("recording model selection failed: {error}");
292 }
293 }
294
295 pub(crate) async fn route_virtual(
298 &self,
299 model: &Model,
300 messages: Vec<Message>,
301 reason: RouteReason,
302 thinking: ThinkingLevel,
303 failed: Option<oino_ai::AssistantMessage>,
304 _cancel: &CancellationToken,
305 ) -> Result<ModelRoute, String> {
306 let state: Option<JsonValue> = {
307 let manager = lock(&self.inner.manager);
308 let branch: Vec<SessionEntry> = manager.branch(None).into_iter().cloned().collect();
309 virtual_model_state(&branch_refs(&branch), &model.provider, &model.id).cloned()
310 };
311 let request = self
312 .inner
313 .models
314 .route_request(model, messages, reason, thinking, failed, state.clone())
315 .map_err(|e| e.to_string())?;
316 let registered = self
317 .inner
318 .models
319 .virtual_model(&model.provider, &model.id)
320 .ok_or_else(|| {
321 format!(
322 "Virtual model {}/{} is not registered.",
323 model.provider, model.id
324 )
325 })?;
326 let runner = self.runner();
327 let extension = runner
328 .set()
329 .get(®istered.extension_id)
330 .cloned()
331 .ok_or_else(|| format!("Extension {} is not loaded", registered.extension_id))?;
332 let cx = runner.call_context().with_kind(CallKind::Service);
333 let response = extension
334 .instance
335 .route_model(json!(request), cx)
336 .await
337 .map_err(|e| e.message)?;
338 let result: RouteResult = serde_json::from_value(response).map_err(|e| e.to_string())?;
339 let route = self
340 .inner
341 .models
342 .validate_route(model, result)
343 .map_err(|e| e.to_string())?;
344 if should_store_route_state(reason, state.as_ref(), route.state.as_ref()) {
345 let data = VirtualModelStateData {
346 provider: model.provider.clone(),
347 model_id: model.id.clone(),
348 state: route.state.clone().unwrap_or(JsonValue::Null),
349 };
350 let appended = lock(&self.inner.manager)
351 .append_custom_entry(VIRTUAL_MODEL_STATE_ENTRY, serde_json::to_value(data).ok());
352 if let Ok(id) = appended {
353 self.publish_entry(&id);
354 }
355 }
356 Ok(route)
357 }
358}