| 1 | use axum::Json; |
| 2 | use axum::extract::{Path, State}; |
| 3 | use serde::Serialize; |
| 4 | |
| 5 | use super::{ApiError, RuntimeApiState, map_thread_err}; |
| 6 | |
| 7 | /// Live context-window posture for one thread — the facts the GPUI usage |
| 8 | /// panel cannot reconstruct from turn receipts. `input_tokens` is the same |
| 9 | /// conservative estimate the in-app context meter shows; `billed_input_tokens` |
| 10 | /// is the last provider-counted prompt size when one exists. |
| 11 | /// |
| 12 | /// All numeric fields are nullable: a route that cannot express a bounded |
| 13 | /// window (unknown model, no catalog or configured limits) reports `null` |
| 14 | /// rather than an invented number, and `live: false` marks responses where |
| 15 | /// the engine could not be loaded and only the static route window resolved. |
| 16 | #[derive(Debug, Serialize)] |
| 17 | pub(super) struct ThreadContextResponse { |
| 18 | thread_id: String, |
| 19 | model: String, |
| 20 | provider: Option<String>, |
| 21 | model_provider_id: Option<String>, |
| 22 | /// `true` when the numbers came from the loaded engine (live estimate + |
| 23 | /// route limits); `false` when only the store-recorded route resolved. |
| 24 | live: bool, |
| 25 | window_tokens: Option<u64>, |
| 26 | input_tokens: Option<u64>, |
| 27 | billed_input_tokens: Option<u64>, |
| 28 | output_cap_tokens: Option<u64>, |
| 29 | input_budget_ceiling: Option<u64>, |
| 30 | available_input_tokens: Option<u64>, |
| 31 | compaction_trigger_tokens: Option<u64>, |
| 32 | usage_percent: Option<f64>, |
| 33 | pressure: Option<&'static str>, |
| 34 | } |
| 35 | |
| 36 | pub(super) async fn get_thread_context( |
| 37 | State(state): State<RuntimeApiState>, |
| 38 | Path(thread_id): Path<String>, |
| 39 | ) -> Result<Json<ThreadContextResponse>, ApiError> { |
| 40 | let thread = state |
| 41 | .runtime_threads |
| 42 | .get_thread(&thread_id) |
| 43 | .await |
| 44 | .map_err(map_thread_err)?; |
| 45 | |
| 46 | if let Ok(engine) = state.runtime_threads.get_engine(&thread_id).await |
| 47 | && let Ok(Some(snapshot)) = engine.get_context_budget().await |
| 48 | { |
| 49 | return Ok(Json(ThreadContextResponse { |
| 50 | thread_id, |
| 51 | model: snapshot.model, |
| 52 | provider: Some(snapshot.provider), |
| 53 | model_provider_id: snapshot.model_provider_id, |
| 54 | live: true, |
| 55 | window_tokens: Some(snapshot.window_tokens), |
| 56 | input_tokens: Some(snapshot.input_tokens), |
| 57 | billed_input_tokens: snapshot.billed_input_tokens, |
| 58 | output_cap_tokens: Some(snapshot.output_cap_tokens), |
| 59 | input_budget_ceiling: Some(snapshot.input_budget_ceiling), |
| 60 | available_input_tokens: Some(snapshot.available_input_tokens), |
| 61 | compaction_trigger_tokens: Some(snapshot.compaction_trigger_tokens), |
| 62 | usage_percent: Some(snapshot.usage_percent), |
| 63 | pressure: Some(snapshot.pressure), |
| 64 | })); |
| 65 | } |
| 66 | |
| 67 | // Engine unavailable or the route cannot bound a window: still answer |
| 68 | // with whatever the thread record and the static route catalog can prove. |
| 69 | let provider = thread |
| 70 | .model_provider |
| 71 | .as_deref() |
| 72 | .and_then(crate::config::ApiProvider::parse); |
| 73 | let window_tokens = provider.map(|provider| { |
| 74 | u64::from(crate::route_budget::route_context_window_tokens( |
| 75 | provider, |
| 76 | &thread.model, |
| 77 | None, |
| 78 | )) |
| 79 | }); |
| 80 | Ok(Json(ThreadContextResponse { |
| 81 | thread_id, |
| 82 | model: thread.model, |
| 83 | provider: thread.model_provider, |
| 84 | model_provider_id: thread.model_provider_id, |
| 85 | live: false, |
| 86 | window_tokens, |
| 87 | input_tokens: None, |
| 88 | billed_input_tokens: None, |
| 89 | output_cap_tokens: None, |
| 90 | input_budget_ceiling: None, |
| 91 | available_input_tokens: None, |
| 92 | compaction_trigger_tokens: None, |
| 93 | usage_percent: None, |
| 94 | pressure: None, |
| 95 | })) |
| 96 | } |
| 97 |