返回 CodeWhale
context.rs
根目录 / crates / tui / src / runtime_api / context.rs
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
97 lines RUST