Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5d0f9e5
feat: add token usage display
MyEcoria Sep 27, 2025
cd24d70
feat: token compression at 80 % of max_context_tokens
MyEcoria Sep 27, 2025
affa90e
Merge branch 'dev/pbuchez/tokens-usage' into dev/pbuchez/context-comp…
MyEcoria Sep 28, 2025
e99bc17
feat: update context compressor with token usage tracking
MyEcoria Sep 28, 2025
d5439cf
Merge branch 'ovh:main' into dev/pbuchez/context-compressor
MyEcoria Sep 29, 2025
f56419d
Merge branch 'ovh:main' into dev/pbuchez/context-compressor
MyEcoria Sep 29, 2025
6006184
feat: Increase threshold compression, switch manual config to automat…
MyEcoria Sep 30, 2025
38d1043
feat: context remaining
MyEcoria Sep 30, 2025
800b699
feat: manual compact trigger
MyEcoria Sep 30, 2025
b42fe6a
fix: gpt-oss context
MyEcoria Sep 30, 2025
736b5d1
dev in progress
MyEcoria Sep 30, 2025
39c02db
refactor: prompt, debug log and always use latest user prompt
MyEcoria Sep 30, 2025
f44eb10
Merge pull request #2 from MyEcoria/dev/pbuchez/context-dev
MyEcoria Sep 30, 2025
65ad427
Merge branch 'ovh:main' into dev/pbuchez/context-compressor
MyEcoria Sep 30, 2025
5ef91d1
feat: fix gpt-oss context & try to get ONLY output token without reas…
MyEcoria Sep 30, 2025
cfe82be
Merge pull request #3 from MyEcoria/dev/pbuchez/context-dev
MyEcoria Sep 30, 2025
b135c2e
Merge branch 'main' into dev/pbuchez/context-compressor
MyEcoria Oct 2, 2025
a5e478a
refactor(coder): refactor token usage extraction to if-let
MyEcoria Oct 2, 2025
21650f0
feat(compacter): retain tool and system messages in compressed log
MyEcoria Oct 2, 2025
152e6b7
Merge branch 'ovh:main' into dev/pbuchez/context-compressor
MyEcoria Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,50 @@ the `shai` binary will be installed in `$HOME/.local/bin`

## Configure a provider and Run!

### Configuration files

Shai can be configured via **configuration files** written in JSON. By default, the configuration file is `auth.config` located in `~/.config/shai/`. The file defines the list of LLM providers, the selected provider, model, and tool call method.

#### Example `.shai.config`
```json
{
"providers": [
{
"provider": "ovhcloud",
"env_vars": {
"OVH_BASE_URL": "https://gpt-oss-120b.endpoints.kepler.ai.cloud.ovh.net/api/openai_compat/v1"
},
"model": "gpt-oss-120b",
"tool_method": "FunctionCall",
"max_context_tokens": 8192
}
],
"selected_provider": 0
}
```

- **providers**: an array of provider definitions. Each provider can specify environment variables (`env_vars`), the model name, the tool call method (`FunctionCall` or `Chat`), and optionally `max_context_tokens` to limit the context size.
- **selected_provider**: the index of the provider to use (starting at `0`).
- **max_context_tokens** (optional, per provider): maximum number of tokens that can be sent in the context to the LLM. If omitted, the default for the model is used.

You can create multiple configuration files for different agents (see the *Custom Agent* section). To use a specific configuration, place the file in `~/.config/shai/agents/` and run the agent by its filename (without the `.config` extension):
```
shai my_custom_agent
```

Shai will automatically load the configuration, set the required environment variables, and use the selected provider for all subsequent interactions.

### Using the configuration

- **Automatic loading**: If a `.shai.config` file is present in the current directory, Shai will load it automatically.
- **Explicit loading**: Use the `--config <path>` flag to specify a custom configuration file:
```
shai --config ~/.config/shai/agents/example.config
```

The configuration system allows you to switch providers, models, or tool call methods without recompiling the binary.


By default `shai` uses OVHcloud as an anonymous user meaning you will be rate limited! If you want to sign in with your account or select another provider, run:

```
Expand Down
10 changes: 7 additions & 3 deletions shai-cli/src/headless/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,15 @@ impl AppHeadless {
.run().await;

match result {
Ok(AgentResult { success, message, trace: agent_trace }) => {
Ok(AgentResult { success, message, trace: full_trace, compressed_trace }) => {
if trace {
println!("{}", serde_json::to_string_pretty(&agent_trace)?);
#[cfg(debug_assertions)]
{
println!("{}", serde_json::to_string_pretty(&compressed_trace)?);
}
println!("{}", serde_json::to_string_pretty(&full_trace)?);
} else {
if let Some(message) = agent_trace.last() {
if let Some(message) = full_trace.last() {
match message {
ChatMessage::Assistant { content: Some(ChatMessageContent::Text(content)), .. } => {
println!("{}",content);
Expand Down
4 changes: 2 additions & 2 deletions shai-cli/src/headless/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ impl ToolName {
ToolName::Bash => "bash",
ToolName::Edit => "edit",
ToolName::Fetch => "fetch",
ToolName::Find => "find",
ToolName::Find => "search",
ToolName::Ls => "ls",
ToolName::MultiEdit => "multiedit",
ToolName::Read => "read",
Expand All @@ -54,7 +54,7 @@ impl ToolName {
"bash" => Some(ToolName::Bash),
"edit" => Some(ToolName::Edit),
"fetch" => Some(ToolName::Fetch),
"find" => Some(ToolName::Find),
"search" => Some(ToolName::Find),
"ls" => Some(ToolName::Ls),
"multiedit" => Some(ToolName::MultiEdit),
"read" => Some(ToolName::Read),
Expand Down
33 changes: 28 additions & 5 deletions shai-cli/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use shai_core::logging::LoggingConfig;
use shai_core::runners::coder::coder::coder;
use shai_core::tools::{ToolCall, ToolResult};
use shai_llm::{LlmClient, ToolCallMethod};
use shai_llm::tool::max_context::get_max_context;
use ratatui::{
layout::{Constraint, Direction, Layout},
style::{Color, Style},
Expand Down Expand Up @@ -72,6 +73,8 @@ pub struct App<'a> {

pub(crate) total_input_tokens: u32,
pub(crate) total_output_tokens: u32,
pub(crate) current_tokens: usize,
pub(crate) max_context: usize,
}


Expand All @@ -83,6 +86,7 @@ impl App<'_> {
let config = AgentConfig::load(agent_name)?;

println!("\x1b[2m░ agent {} - {} on {}\x1b[0m", agent_name, config.llm_provider.model, config.llm_provider.provider);
self.max_context = get_max_context(&config.llm_provider.model);

// Create agent from config
let agent_builder = AgentBuilder::from_config(config).await?;
Expand All @@ -91,7 +95,7 @@ impl App<'_> {
// Use default coder agent
let (llm, model) = ShaiConfig::get_llm().await?;
println!("\x1b[2m░ {} on {}\x1b[0m", model, llm.provider().name());

self.max_context = get_max_context(&model);
Box::new(coder(Arc::new(llm), model))
};

Expand Down Expand Up @@ -158,6 +162,13 @@ impl App<'_> {
if let AgentEvent::TokenUsage { input_tokens, output_tokens } = &event {
self.total_input_tokens += input_tokens;
self.total_output_tokens += output_tokens;
self.current_tokens += (input_tokens + output_tokens) as usize;
}
// Update current_tokens after context compression
if let AgentEvent::ContextCompressed { current_tokens, .. } = &event {
if let Some(ct) = current_tokens {
self.current_tokens = *ct as usize;
}
}

Ok(())
Expand All @@ -182,6 +193,8 @@ impl App<'_> {
permission_queue: VecDeque::new(),
total_input_tokens: 0,
total_output_tokens: 0,
current_tokens: 0,
max_context: 0,
}
}

Expand Down Expand Up @@ -385,7 +398,7 @@ impl App<'_> {
AppModalState::PermissionModal { widget } => widget.height(),
}.max(5);
let height = modal_height
+ 1
+ 2
+ self.running_tools.len() as u16;

if let Some(ref mut terminal) = self.terminal {
Expand All @@ -395,11 +408,13 @@ impl App<'_> {
}

terminal.draw(|frame| {
let [_, inprogress, modal] = Layout::vertical([
let [_, inprogress, modal, ctx_line] = Layout::vertical([
Constraint::Length(1), // padding
Constraint::Length(self.running_tools.len() as u16 + 1), // running tool (if any)
Constraint::Length(modal_height)]) // input or modal
.areas(frame.area());
Constraint::Length(modal_height),
Constraint::Length(1)
]).areas(frame.area());


// draw running tool
if !self.running_tools.is_empty() {
Expand All @@ -418,6 +433,14 @@ impl App<'_> {
widget.draw(frame, modal)
}
}
// Render context usage line
if self.max_context > 0 {
let used = self.current_tokens;
let remaining = if used > self.max_context { 0 } else { self.max_context - used };
let percent = (remaining as f64 / self.max_context as f64) * 100.0;
let ctx_text = format!("Context: {:.1}% remaining", percent);
frame.render_widget(Span::styled(ctx_text, Style::default().fg(Color::DarkGray)), ctx_line);
}
})?;
}
Ok(())
Expand Down
7 changes: 7 additions & 0 deletions shai-cli/src/tui/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ impl App<'_> {
(("/auth","select a provider"), vec![]),
(("/tc","set the tool call method: [fc | fc2 | so]"), vec!["method"]),
(("/tokens","display token usage (input/output)"), vec![]),
(("/compact","trigger context compaction"), vec![]),
])
.into_iter()
.map(|((cmd,desc),args)|((cmd.to_string(),desc.to_string()),args.into_iter().map(|s|s.to_string()).collect()))
Expand Down Expand Up @@ -65,6 +66,12 @@ impl App<'_> {
);
self.input.alert_msg(&msg, Duration::from_secs(5));
}
"/compact" => {
if let Some(ref agent) = self.agent {
let _ = agent.controller.trigger_context_compression().await;
self.input.alert_msg("Context compression triggered", Duration::from_secs(2));
}
}
_ => {
self.input.alert_msg("command unknown", Duration::from_secs(1));
}
Expand Down
5 changes: 3 additions & 2 deletions shai-cli/src/tui/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ impl HelpArea {
" Available Commands:",
" /exit exit from the tui",
" /tc <method> set tool call method: [auto | fc | fc2 | so]",
" /tokens display token usage"
" /tokens display token usage",
" /compact trigger context compaction"
].join("\n").to_string()
}
}

impl HelpArea {
pub fn height(&self) -> u16 {
8 // content (3 general help lines + 1 blank + 1 header + 3 command lines)
9 // content (3 general help lines + 1 blank + 1 header + 4 command lines)
}

pub fn draw(&self, f: &mut Frame, area: Rect) {
Expand Down
Loading