mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-10 22:43:37 +00:00
Remove ProfileCapabilities, rename ProviderProfile to AgentProfile, fix layer separation
Move model facts (knowledge_cutoff, context_window) to fabro-model catalog as source of truth. Move request-shaping (auto-thinking, 1M beta headers, Gemini safety settings) into fabro-llm adapters. Delete ProfileCapabilities struct and all dead code (supports_reasoning, supports_streaming, supports_parallel_tool_calls, OpenAiProfile.reasoning_effort). Fix "powered by OpenAI" mislabeling for Kimi/ZAI/Minimax/Inception providers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c7703fc9a0
commit
57d0317d7f
19 changed files with 224 additions and 346 deletions
|
|
@ -40,7 +40,7 @@ User Input
|
|||
### Key Components
|
||||
|
||||
- **`Session`** -- Manages the full agentic loop: LLM calls, tool execution, steering, follow-ups, abort handling, and event emission.
|
||||
- **`ProviderProfile`** (trait) -- Defines how to build system prompts, which tools to register, and what capabilities a provider supports. Ships with `AnthropicProfile`, `OpenAiProfile`, and `GeminiProfile`.
|
||||
- **`AgentProfile`** (trait) -- Defines how to build system prompts, which tools to register, and what capabilities a provider supports. Ships with `AnthropicProfile`, `OpenAiProfile`, and `GeminiProfile`.
|
||||
- **`Sandbox`** (trait) -- Abstracts filesystem, shell, grep, and glob operations. `LocalSandbox` provides a real implementation; the trait enables sandboxing and testing.
|
||||
- **`ToolRegistry`** -- Maps tool names to definitions and async executor functions. Tools are registered per-profile.
|
||||
- **`History`** -- Ordered list of `Turn` variants (`User`, `Assistant`, `ToolResults`, `System`, `Steering`) that converts to LLM messages.
|
||||
|
|
@ -54,10 +54,10 @@ User Input
|
|||
|
||||
The main entry point. Created with an LLM client, a provider profile, a sandbox, and a config.
|
||||
|
||||
### `ProviderProfile`
|
||||
### `AgentProfile`
|
||||
|
||||
```rust
|
||||
pub trait ProviderProfile: Send + Sync {
|
||||
pub trait AgentProfile: Send + Sync {
|
||||
fn id(&self) -> String;
|
||||
fn model(&self) -> String;
|
||||
fn tool_registry(&self) -> &ToolRegistry;
|
||||
|
|
@ -68,7 +68,7 @@ pub trait ProviderProfile: Send + Sync {
|
|||
project_docs: &[String],
|
||||
user_instructions: Option<&str>,
|
||||
) -> String;
|
||||
fn capabilities(&self) -> ProfileCapabilities;
|
||||
fn capabilities(&self) -> AgentProfile;
|
||||
fn knowledge_cutoff(&self) -> &str;
|
||||
// ... default methods for tools(), provider_options(), supports_*()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,18 +7,10 @@ use crate::subagent::{
|
|||
};
|
||||
use crate::tool_registry::ToolRegistry;
|
||||
use fabro_llm::types::ToolDefinition;
|
||||
use fabro_model::Provider;
|
||||
use fabro_model::{Catalog, Provider};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Static capabilities of a provider profile.
|
||||
pub struct ProfileCapabilities {
|
||||
pub supports_reasoning: bool,
|
||||
pub supports_streaming: bool,
|
||||
pub supports_parallel_tool_calls: bool,
|
||||
pub context_window_size: usize,
|
||||
}
|
||||
|
||||
pub trait ProviderProfile: Send + Sync {
|
||||
pub trait AgentProfile: Send + Sync {
|
||||
fn provider(&self) -> Provider;
|
||||
fn model(&self) -> &str;
|
||||
fn tool_registry(&self) -> &ToolRegistry;
|
||||
|
|
@ -31,31 +23,22 @@ pub trait ProviderProfile: Send + Sync {
|
|||
user_instructions: Option<&str>,
|
||||
skills: &[Skill],
|
||||
) -> String;
|
||||
fn capabilities(&self) -> ProfileCapabilities;
|
||||
fn knowledge_cutoff(&self) -> &str;
|
||||
|
||||
fn tools(&self) -> Vec<ToolDefinition> {
|
||||
self.tool_registry().definitions()
|
||||
}
|
||||
|
||||
fn provider_options(&self) -> Option<serde_json::Value> {
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_reasoning(&self) -> bool {
|
||||
self.capabilities().supports_reasoning
|
||||
}
|
||||
|
||||
fn supports_streaming(&self) -> bool {
|
||||
self.capabilities().supports_streaming
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
self.capabilities().supports_parallel_tool_calls
|
||||
fn knowledge_cutoff(&self) -> Option<String> {
|
||||
Catalog::builtin()
|
||||
.get(self.model())
|
||||
.and_then(|m| m.knowledge_cutoff().map(str::to_string))
|
||||
}
|
||||
|
||||
fn context_window_size(&self) -> usize {
|
||||
self.capabilities().context_window_size
|
||||
Catalog::builtin()
|
||||
.get(self.model())
|
||||
.map(|m| m.context_window() as usize)
|
||||
.unwrap_or(200_000)
|
||||
}
|
||||
|
||||
fn register_subagent_tools(
|
||||
|
|
@ -92,11 +75,8 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn profile_capabilities() {
|
||||
fn profile_context_window_defaults() {
|
||||
let profile = TestProfile::new();
|
||||
assert!(!profile.supports_reasoning());
|
||||
assert!(!profile.supports_streaming());
|
||||
assert!(!profile.supports_parallel_tool_calls());
|
||||
assert_eq!(profile.context_window_size(), 200_000);
|
||||
}
|
||||
|
||||
|
|
@ -119,12 +99,6 @@ mod tests {
|
|||
assert!(prompt.contains("Always use TDD"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_provider_options_none() {
|
||||
let profile = TestProfile::new();
|
||||
assert!(profile.provider_options().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_tools_empty_registry() {
|
||||
let profile = TestProfile::new();
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::config::ToolApprovalFn;
|
||||
use crate::{
|
||||
subagent::{SessionFactory, SubAgentManager},
|
||||
AgentEvent, AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile, ProviderProfile,
|
||||
AgentEvent, AgentProfile, AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile,
|
||||
Session, SessionConfig, Turn,
|
||||
};
|
||||
use clap::{Args, Parser};
|
||||
|
|
@ -188,7 +188,7 @@ fn build_profile(
|
|||
provider: Provider,
|
||||
model: &str,
|
||||
llm_client: Option<Client>,
|
||||
) -> Box<dyn ProviderProfile> {
|
||||
) -> Box<dyn AgentProfile> {
|
||||
let summarizer = build_summarizer(provider, llm_client);
|
||||
match provider {
|
||||
Provider::OpenAi => Box::new(OpenAiProfile::with_summarizer(model, summarizer)),
|
||||
|
|
@ -436,7 +436,7 @@ pub async fn run_with_args_and_client(
|
|||
let factory_hooks = config.tool_hooks.clone();
|
||||
let factory: SessionFactory = Arc::new(move || {
|
||||
let child_summarizer = build_summarizer(provider, Some(factory_client.clone()));
|
||||
let child_profile: Arc<dyn ProviderProfile> = match provider {
|
||||
let child_profile: Arc<dyn AgentProfile> = match provider {
|
||||
Provider::OpenAi => Arc::new(OpenAiProfile::with_summarizer(
|
||||
&factory_model,
|
||||
child_summarizer,
|
||||
|
|
@ -469,7 +469,7 @@ pub async fn run_with_args_and_client(
|
|||
)
|
||||
});
|
||||
profile.register_subagent_tools(manager, factory, 0);
|
||||
let profile: Arc<dyn ProviderProfile> = Arc::from(profile);
|
||||
let profile: Arc<dyn AgentProfile> = Arc::from(profile);
|
||||
|
||||
let mut session = Session::new(client, profile, env, config);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use crate::agent_profile::AgentProfile;
|
||||
use crate::error::AgentError;
|
||||
use crate::event::EventEmitter;
|
||||
use crate::file_tracker::FileTracker;
|
||||
use crate::history::History;
|
||||
use crate::provider_profile::ProviderProfile;
|
||||
use crate::types::{AgentEvent, Turn};
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_llm::types::{Message, Request};
|
||||
|
|
@ -14,7 +14,7 @@ use tracing::debug;
|
|||
pub fn check_context_usage(
|
||||
system_prompt: &str,
|
||||
history: &History,
|
||||
provider_profile: &dyn ProviderProfile,
|
||||
provider_profile: &dyn AgentProfile,
|
||||
threshold_percent: usize,
|
||||
emitter: &EventEmitter,
|
||||
session_id: &str,
|
||||
|
|
@ -43,7 +43,7 @@ pub fn check_context_usage(
|
|||
pub async fn compact_context(
|
||||
history: &mut History,
|
||||
llm_client: &Client,
|
||||
provider_profile: &dyn ProviderProfile,
|
||||
provider_profile: &dyn AgentProfile,
|
||||
system_prompt: &str,
|
||||
file_tracker: &FileTracker,
|
||||
preserve_count: usize,
|
||||
|
|
@ -338,7 +338,7 @@ mod tests {
|
|||
let emitter = EventEmitter::new();
|
||||
let mut rx = emitter.subscribe();
|
||||
// TestProfile has context_window=200_000 by default; use a small one
|
||||
let profile = crate::test_support::TestProfile::parallel_with_context_window(
|
||||
let profile = crate::test_support::TestProfile::with_context_window(
|
||||
crate::tool_registry::ToolRegistry::new(),
|
||||
100,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#[cfg(feature = "docker")]
|
||||
pub mod docker_sandbox;
|
||||
|
||||
pub mod agent_profile;
|
||||
pub mod cli;
|
||||
pub mod compaction;
|
||||
pub mod config;
|
||||
|
|
@ -13,7 +14,6 @@ pub mod loop_detection;
|
|||
pub mod mcp_integration;
|
||||
pub mod memory;
|
||||
pub mod profiles;
|
||||
pub mod provider_profile;
|
||||
pub mod read_before_write_sandbox;
|
||||
pub mod sandbox;
|
||||
pub mod session;
|
||||
|
|
@ -26,6 +26,7 @@ pub mod truncation;
|
|||
pub mod types;
|
||||
pub mod v4a_patch;
|
||||
|
||||
pub use agent_profile::AgentProfile;
|
||||
pub use config::{SessionConfig, ToolApprovalAdapter, ToolHookCallback, ToolHookDecision};
|
||||
#[cfg(feature = "docker")]
|
||||
pub use docker_sandbox::{DockerSandbox, DockerSandboxConfig};
|
||||
|
|
@ -37,7 +38,6 @@ pub use local_sandbox::LocalSandbox;
|
|||
pub use loop_detection::detect_loop;
|
||||
pub use memory::discover_memory;
|
||||
pub use profiles::{AnthropicProfile, EnvContext, GeminiProfile, OpenAiProfile};
|
||||
pub use provider_profile::{ProfileCapabilities, ProviderProfile};
|
||||
pub use read_before_write_sandbox::ReadBeforeWriteSandbox;
|
||||
pub use sandbox::{
|
||||
format_lines_numbered, shell_quote, DirEntry, ExecResult, GrepOptions, Sandbox, SandboxEvent,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use crate::agent_profile::AgentProfile;
|
||||
use crate::config::SessionConfig;
|
||||
use crate::profiles::assemble_system_prompt;
|
||||
use crate::profiles::BaseProfile;
|
||||
use crate::provider_profile::{ProfileCapabilities, ProviderProfile};
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::skills::Skill;
|
||||
use crate::tool_registry::ToolRegistry;
|
||||
use crate::tools::{make_edit_file_tool, register_core_tools, WebFetchSummarizer};
|
||||
use fabro_model::{Catalog, Provider};
|
||||
use fabro_model::Provider;
|
||||
|
||||
use super::EnvContext;
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ impl AnthropicProfile {
|
|||
}
|
||||
}
|
||||
|
||||
impl ProviderProfile for AnthropicProfile {
|
||||
impl AgentProfile for AnthropicProfile {
|
||||
fn provider(&self) -> Provider {
|
||||
self.base.provider
|
||||
}
|
||||
|
|
@ -162,49 +162,6 @@ in the project. Keep changes minimal and focused on the task.";
|
|||
skills,
|
||||
)
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> ProfileCapabilities {
|
||||
let context_window_size = Catalog::builtin()
|
||||
.get(self.model())
|
||||
.map(|info| info.context_window() as usize)
|
||||
.unwrap_or_else(|| {
|
||||
if self.model().contains("opus-4-6") {
|
||||
1_000_000
|
||||
} else {
|
||||
200_000
|
||||
}
|
||||
});
|
||||
ProfileCapabilities {
|
||||
supports_reasoning: true,
|
||||
supports_streaming: true,
|
||||
supports_parallel_tool_calls: true,
|
||||
context_window_size,
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_options(&self) -> Option<serde_json::Value> {
|
||||
let model = self.model();
|
||||
if model.contains("opus-4-6") {
|
||||
Some(serde_json::json!({
|
||||
"anthropic": {
|
||||
"thinking": {"type": "adaptive"},
|
||||
"beta_headers": ["context-1m-2025-08-07"]
|
||||
}
|
||||
}))
|
||||
} else if model.contains("sonnet-4-6") {
|
||||
Some(serde_json::json!({
|
||||
"anthropic": {
|
||||
"thinking": {"type": "adaptive"}
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn knowledge_cutoff(&self) -> &'static str {
|
||||
"May 2025"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -220,18 +177,18 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn anthropic_profile_capabilities() {
|
||||
let profile = AnthropicProfile::new("claude-sonnet-4-20250514");
|
||||
assert!(profile.supports_reasoning());
|
||||
assert!(profile.supports_streaming());
|
||||
assert!(profile.supports_parallel_tool_calls());
|
||||
fn anthropic_context_window_from_catalog() {
|
||||
let profile = AnthropicProfile::new("claude-opus-4-6");
|
||||
assert_eq!(profile.context_window_size(), 1_000_000);
|
||||
|
||||
let profile = AnthropicProfile::new("claude-sonnet-4-6");
|
||||
assert_eq!(profile.context_window_size(), 200_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anthropic_opus_4_6_has_1m_context_window() {
|
||||
fn anthropic_knowledge_cutoff_from_catalog() {
|
||||
let profile = AnthropicProfile::new("claude-opus-4-6");
|
||||
assert_eq!(profile.context_window_size(), 1_000_000);
|
||||
assert_eq!(profile.knowledge_cutoff(), Some("May 2025".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -332,47 +289,6 @@ mod tests {
|
|||
assert!(names.contains(&"web_fetch".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anthropic_provider_options_include_thinking_for_opus_4_6() {
|
||||
let profile = AnthropicProfile::new("claude-opus-4-6");
|
||||
let options = profile.provider_options();
|
||||
assert!(options.is_some(), "provider_options should return Some");
|
||||
let options = options.unwrap();
|
||||
let thinking_type = options["anthropic"]["thinking"]["type"].as_str();
|
||||
assert_eq!(
|
||||
thinking_type,
|
||||
Some("adaptive"),
|
||||
"thinking type should be adaptive"
|
||||
);
|
||||
let beta_headers = options["anthropic"]["beta_headers"]
|
||||
.as_array()
|
||||
.expect("beta_headers should be an array");
|
||||
assert_eq!(beta_headers[0].as_str(), Some("context-1m-2025-08-07"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anthropic_provider_options_include_thinking_for_sonnet_4_6() {
|
||||
let profile = AnthropicProfile::new("claude-sonnet-4-6");
|
||||
let options = profile.provider_options();
|
||||
assert!(options.is_some(), "provider_options should return Some");
|
||||
let options = options.unwrap();
|
||||
let thinking_type = options["anthropic"]["thinking"]["type"].as_str();
|
||||
assert_eq!(
|
||||
thinking_type,
|
||||
Some("adaptive"),
|
||||
"thinking type should be adaptive"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anthropic_provider_options_none_for_older_models() {
|
||||
let profile = AnthropicProfile::new("claude-sonnet-4-5");
|
||||
assert!(
|
||||
profile.provider_options().is_none(),
|
||||
"older models should not have provider_options"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anthropic_register_subagent_tools() {
|
||||
use crate::subagent::{SessionFactory, SubAgentManager};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::agent_profile::AgentProfile;
|
||||
use crate::config::SessionConfig;
|
||||
use crate::profiles::assemble_system_prompt;
|
||||
use crate::profiles::BaseProfile;
|
||||
use crate::provider_profile::{ProfileCapabilities, ProviderProfile};
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::skills::Skill;
|
||||
use crate::tool_registry::ToolRegistry;
|
||||
|
|
@ -9,7 +9,7 @@ use crate::tools::{
|
|||
make_edit_file_tool, make_list_dir_tool, make_read_many_files_tool, register_core_tools,
|
||||
WebFetchSummarizer,
|
||||
};
|
||||
use fabro_model::{Catalog, Provider};
|
||||
use fabro_model::Provider;
|
||||
|
||||
use super::EnvContext;
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ impl GeminiProfile {
|
|||
}
|
||||
}
|
||||
|
||||
impl ProviderProfile for GeminiProfile {
|
||||
impl AgentProfile for GeminiProfile {
|
||||
fn provider(&self) -> Provider {
|
||||
self.base.provider
|
||||
}
|
||||
|
|
@ -197,34 +197,6 @@ in the project.";
|
|||
skills,
|
||||
)
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> ProfileCapabilities {
|
||||
let context_window_size = Catalog::builtin()
|
||||
.get(self.model())
|
||||
.map(|info| info.context_window() as usize)
|
||||
.unwrap_or(1_000_000);
|
||||
ProfileCapabilities {
|
||||
supports_reasoning: true,
|
||||
supports_streaming: true,
|
||||
supports_parallel_tool_calls: true,
|
||||
context_window_size,
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_options(&self) -> Option<serde_json::Value> {
|
||||
Some(serde_json::json!({
|
||||
"gemini": {
|
||||
"safety_settings": {
|
||||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
"threshold": "BLOCK_ONLY_HIGH"
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn knowledge_cutoff(&self) -> &'static str {
|
||||
"January 2025"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -241,12 +213,9 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_profile_capabilities() {
|
||||
let profile = GeminiProfile::new("gemini-2.0-flash");
|
||||
assert!(profile.supports_reasoning());
|
||||
assert!(profile.supports_streaming());
|
||||
assert!(profile.supports_parallel_tool_calls());
|
||||
assert_eq!(profile.context_window_size(), 1_000_000);
|
||||
fn gemini_context_window_from_catalog() {
|
||||
let profile = GeminiProfile::new("gemini-3.1-pro-preview");
|
||||
assert_eq!(profile.context_window_size(), 1_048_576);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -307,18 +276,6 @@ mod tests {
|
|||
assert!(prompt.contains("linux"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_provider_options_returns_safety_settings() {
|
||||
let profile = GeminiProfile::new("gemini-2.0-flash");
|
||||
let options = profile.provider_options();
|
||||
assert!(options.is_some());
|
||||
let options = options.unwrap();
|
||||
let safety = &options["gemini"]["safety_settings"];
|
||||
assert!(safety.is_object());
|
||||
assert_eq!(safety["category"], "HARM_CATEGORY_DANGEROUS_CONTENT");
|
||||
assert_eq!(safety["threshold"], "BLOCK_ONLY_HIGH");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_tools_registered() {
|
||||
let profile = GeminiProfile::new("gemini-2.0-flash");
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
use crate::agent_profile::AgentProfile;
|
||||
use crate::config::SessionConfig;
|
||||
use crate::profiles::assemble_system_prompt;
|
||||
use crate::profiles::BaseProfile;
|
||||
use crate::provider_profile::{ProfileCapabilities, ProviderProfile};
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::skills::Skill;
|
||||
use crate::tool_registry::ToolRegistry;
|
||||
use crate::tools::{register_core_tools, WebFetchSummarizer};
|
||||
use crate::v4a_patch::make_apply_patch_tool;
|
||||
use fabro_model::{Catalog, Provider};
|
||||
use fabro_model::Provider;
|
||||
|
||||
use super::EnvContext;
|
||||
|
||||
pub struct OpenAiProfile {
|
||||
base: BaseProfile,
|
||||
reasoning_effort: Option<String>,
|
||||
}
|
||||
|
||||
impl OpenAiProfile {
|
||||
|
|
@ -39,14 +38,9 @@ impl OpenAiProfile {
|
|||
model: model.into(),
|
||||
registry,
|
||||
},
|
||||
reasoning_effort: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_reasoning_effort(&mut self, effort: Option<String>) {
|
||||
self.reasoning_effort = effort;
|
||||
}
|
||||
|
||||
/// Override the provider identity (e.g. for Z.AI or Minimax, which use the
|
||||
/// OpenAI Chat Completions protocol but route to different adapters).
|
||||
#[must_use]
|
||||
|
|
@ -54,9 +48,20 @@ impl OpenAiProfile {
|
|||
self.base.provider = provider;
|
||||
self
|
||||
}
|
||||
|
||||
fn provider_display_name(&self) -> &str {
|
||||
match self.base.provider {
|
||||
Provider::OpenAi => "OpenAI",
|
||||
Provider::Kimi => "Moonshot",
|
||||
Provider::Zai => "Zhipu AI",
|
||||
Provider::Minimax => "MiniMax",
|
||||
Provider::Inception => "Inception",
|
||||
other => other.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProviderProfile for OpenAiProfile {
|
||||
impl AgentProfile for OpenAiProfile {
|
||||
fn provider(&self) -> Provider {
|
||||
self.base.provider
|
||||
}
|
||||
|
|
@ -81,8 +86,9 @@ impl ProviderProfile for OpenAiProfile {
|
|||
user_instructions: Option<&str>,
|
||||
skills: &[Skill],
|
||||
) -> String {
|
||||
let core_prompt = "\
|
||||
You are a coding agent powered by OpenAI, running in a terminal-based agentic coding assistant. \
|
||||
let provider_name = self.provider_display_name();
|
||||
let core_prompt = format!("\
|
||||
You are a coding agent powered by {provider_name}, running in a terminal-based agentic coding assistant. \
|
||||
You are expected to be precise, safe, and helpful.
|
||||
|
||||
You can receive user prompts and context such as files in the workspace, communicate with the \
|
||||
|
|
@ -95,7 +101,7 @@ Be concise, direct, and friendly. Communicate efficiently, keeping the user clea
|
|||
about ongoing actions without unnecessary detail. Prioritize actionable guidance, clearly \
|
||||
stating assumptions, environment prerequisites, and next steps.
|
||||
|
||||
{env_block}
|
||||
{{env_block}}
|
||||
|
||||
# AGENTS.md
|
||||
|
||||
|
|
@ -178,10 +184,10 @@ information instead of returning the full page. URLs must start with http:// or
|
|||
# Coding Best Practices
|
||||
|
||||
Write clean, maintainable code. Handle errors appropriately. Follow existing code conventions \
|
||||
in the project.";
|
||||
in the project.");
|
||||
|
||||
assemble_system_prompt(
|
||||
core_prompt,
|
||||
&core_prompt,
|
||||
env,
|
||||
env_context,
|
||||
memory,
|
||||
|
|
@ -189,35 +195,6 @@ in the project.";
|
|||
skills,
|
||||
)
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> ProfileCapabilities {
|
||||
let context_window_size = Catalog::builtin()
|
||||
.get(self.model())
|
||||
.map(|info| info.context_window() as usize)
|
||||
.unwrap_or(128_000);
|
||||
ProfileCapabilities {
|
||||
supports_reasoning: true,
|
||||
supports_streaming: true,
|
||||
supports_parallel_tool_calls: true,
|
||||
context_window_size,
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_options(&self) -> Option<serde_json::Value> {
|
||||
self.reasoning_effort.as_ref().map(|effort| {
|
||||
serde_json::json!({
|
||||
"openai": {
|
||||
"reasoning": {
|
||||
"effort": effort
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn knowledge_cutoff(&self) -> &'static str {
|
||||
"April 2025"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -232,15 +209,6 @@ mod tests {
|
|||
assert_eq!(profile.model(), "o3-mini");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_profile_capabilities() {
|
||||
let profile = OpenAiProfile::new("o3-mini");
|
||||
assert!(profile.supports_reasoning());
|
||||
assert!(profile.supports_streaming());
|
||||
assert!(profile.supports_parallel_tool_calls());
|
||||
assert_eq!(profile.context_window_size(), 128_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_system_prompt_contains_env_context() {
|
||||
let profile = OpenAiProfile::new("o3-mini");
|
||||
|
|
@ -301,38 +269,6 @@ mod tests {
|
|||
assert!(prompt.contains("# User Instructions"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_provider_options_default_none() {
|
||||
let profile = OpenAiProfile::new("o3-mini");
|
||||
assert!(profile.provider_options().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_provider_options_with_reasoning_effort() {
|
||||
let mut profile = OpenAiProfile::new("o3-mini");
|
||||
profile.set_reasoning_effort(Some("high".to_string()));
|
||||
let options = profile.provider_options().unwrap();
|
||||
assert_eq!(
|
||||
options,
|
||||
serde_json::json!({
|
||||
"openai": {
|
||||
"reasoning": {
|
||||
"effort": "high"
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_provider_options_cleared() {
|
||||
let mut profile = OpenAiProfile::new("o3-mini");
|
||||
profile.set_reasoning_effort(Some("high".to_string()));
|
||||
assert!(profile.provider_options().is_some());
|
||||
profile.set_reasoning_effort(None);
|
||||
assert!(profile.provider_options().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_subagent_tools_registered() {
|
||||
use crate::subagent::SessionFactory;
|
||||
|
|
@ -362,4 +298,37 @@ mod tests {
|
|||
assert!(names.contains(&"web_search".to_string()));
|
||||
assert!(names.contains(&"web_fetch".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kimi_provider_prompt_says_moonshot() {
|
||||
let profile = OpenAiProfile::new("kimi-k2.5").with_provider(Provider::Kimi);
|
||||
let env = MockSandbox::linux();
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(prompt.contains("powered by Moonshot"));
|
||||
assert!(!prompt.contains("powered by OpenAI"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zai_provider_prompt_says_zhipu() {
|
||||
let profile = OpenAiProfile::new("glm-4.7").with_provider(Provider::Zai);
|
||||
let env = MockSandbox::linux();
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(prompt.contains("powered by Zhipu AI"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimax_provider_prompt_says_minimax() {
|
||||
let profile = OpenAiProfile::new("minimax-m2.5").with_provider(Provider::Minimax);
|
||||
let env = MockSandbox::linux();
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(prompt.contains("powered by MiniMax"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inception_provider_prompt_says_inception() {
|
||||
let profile = OpenAiProfile::new("mercury-2").with_provider(Provider::Inception);
|
||||
let env = MockSandbox::linux();
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(prompt.contains("powered by Inception"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crate::agent_profile::AgentProfile;
|
||||
use crate::config::SessionConfig;
|
||||
use crate::error::{AbortReason, AgentError};
|
||||
use crate::event::EventEmitter;
|
||||
|
|
@ -6,7 +7,6 @@ use crate::history::History;
|
|||
use crate::loop_detection::detect_loop;
|
||||
use crate::memory::discover_memory;
|
||||
use crate::profiles::EnvContext;
|
||||
use crate::provider_profile::ProviderProfile;
|
||||
use crate::sandbox::Sandbox;
|
||||
use crate::skills::{
|
||||
default_skill_dirs, discover_skills, expand_skill, make_use_skill_tool, Skill,
|
||||
|
|
@ -32,7 +32,7 @@ pub struct Session {
|
|||
event_emitter: EventEmitter,
|
||||
state: SessionState,
|
||||
llm_client: Client,
|
||||
provider_profile: Arc<dyn ProviderProfile>,
|
||||
provider_profile: Arc<dyn AgentProfile>,
|
||||
sandbox: Arc<dyn Sandbox>,
|
||||
steering_queue: Arc<Mutex<VecDeque<String>>>,
|
||||
followup_queue: Arc<Mutex<VecDeque<String>>>,
|
||||
|
|
@ -50,7 +50,7 @@ impl Session {
|
|||
#[must_use]
|
||||
pub fn new(
|
||||
llm_client: Client,
|
||||
provider_profile: Arc<dyn ProviderProfile>,
|
||||
provider_profile: Arc<dyn AgentProfile>,
|
||||
sandbox: Arc<dyn Sandbox>,
|
||||
config: SessionConfig,
|
||||
) -> Self {
|
||||
|
|
@ -332,7 +332,7 @@ impl Session {
|
|||
is_git_repo,
|
||||
current_date: today,
|
||||
model: model_name,
|
||||
knowledge_cutoff: self.provider_profile.knowledge_cutoff().to_string(),
|
||||
knowledge_cutoff: self.provider_profile.knowledge_cutoff().unwrap_or_default(),
|
||||
git_status_short,
|
||||
git_recent_commits,
|
||||
}
|
||||
|
|
@ -780,7 +780,7 @@ impl Session {
|
|||
// Execute tool calls (parallel or sequential based on provider)
|
||||
let results = crate::tool_execution::execute_tool_calls(
|
||||
&tool_calls,
|
||||
self.provider_profile.supports_parallel_tool_calls(),
|
||||
true,
|
||||
self.provider_profile.tool_registry(),
|
||||
self.sandbox.clone(),
|
||||
self.config.tool_hooks.as_ref(),
|
||||
|
|
@ -913,7 +913,7 @@ impl Session {
|
|||
reasoning_effort: self.config.reasoning_effort.clone(),
|
||||
speed: self.config.speed.clone(),
|
||||
metadata: None,
|
||||
provider_options: self.provider_profile.provider_options(),
|
||||
provider_options: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1490,7 +1490,7 @@ mod tests {
|
|||
|
||||
let provider = Arc::new(MockLlmProvider::new(responses));
|
||||
let client = make_client(provider).await;
|
||||
let profile = Arc::new(TestProfile::parallel(registry));
|
||||
let profile = Arc::new(TestProfile::with_tools(registry));
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let mut session = Session::new(client, profile, env, SessionConfig::default());
|
||||
let mut rx = session.subscribe();
|
||||
|
|
@ -1541,7 +1541,7 @@ mod tests {
|
|||
let provider = Arc::new(MockLlmProvider::new(responses));
|
||||
let client = make_client(provider).await;
|
||||
let registry = ToolRegistry::new();
|
||||
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
|
||||
let profile = Arc::new(TestProfile::with_context_window(registry, 100));
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let mut session = Session::new(client, profile, env, SessionConfig::default());
|
||||
let mut rx = session.subscribe();
|
||||
|
|
@ -1590,7 +1590,7 @@ mod tests {
|
|||
let client = make_client(provider).await;
|
||||
let registry = ToolRegistry::new();
|
||||
// Large context window so short input stays well under 80%
|
||||
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 200_000));
|
||||
let profile = Arc::new(TestProfile::with_context_window(registry, 200_000));
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let mut session = Session::new(client, profile, env, SessionConfig::default());
|
||||
let mut rx = session.subscribe();
|
||||
|
|
@ -2151,7 +2151,7 @@ mod tests {
|
|||
let provider = Arc::new(MockLlmProvider::new(responses));
|
||||
let client = make_client(provider).await;
|
||||
let registry = ToolRegistry::new();
|
||||
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
|
||||
let profile = Arc::new(TestProfile::with_context_window(registry, 100));
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let config = SessionConfig {
|
||||
enable_context_compaction: true,
|
||||
|
|
@ -2194,7 +2194,7 @@ mod tests {
|
|||
let provider = Arc::new(MockLlmProvider::new(responses));
|
||||
let client = make_client(provider).await;
|
||||
let registry = ToolRegistry::new();
|
||||
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
|
||||
let profile = Arc::new(TestProfile::with_context_window(registry, 100));
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let config = SessionConfig {
|
||||
enable_context_compaction: false,
|
||||
|
|
@ -2277,7 +2277,7 @@ mod tests {
|
|||
});
|
||||
let client = make_client(provider as Arc<dyn ProviderAdapter>).await;
|
||||
let registry = ToolRegistry::new();
|
||||
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
|
||||
let profile = Arc::new(TestProfile::with_context_window(registry, 100));
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let config = SessionConfig {
|
||||
enable_context_compaction: true,
|
||||
|
|
@ -2381,7 +2381,7 @@ mod tests {
|
|||
|
||||
let client = make_client(provider.clone() as Arc<dyn ProviderAdapter>).await;
|
||||
// Tiny context window to force compaction
|
||||
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
|
||||
let profile = Arc::new(TestProfile::with_context_window(registry, 100));
|
||||
let env = Arc::new(MockSandbox::default());
|
||||
let config = SessionConfig {
|
||||
enable_context_compaction: true,
|
||||
|
|
@ -2480,8 +2480,7 @@ mod tests {
|
|||
|
||||
let provider = Arc::new(MockLlmProvider::new(responses));
|
||||
let client = make_client(provider).await;
|
||||
let profile: Arc<dyn crate::provider_profile::ProviderProfile> =
|
||||
Arc::new(TestProfile::new());
|
||||
let profile: Arc<dyn crate::agent_profile::AgentProfile> = Arc::new(TestProfile::new());
|
||||
let env: Arc<dyn crate::sandbox::Sandbox> = Arc::new(MockSandbox::default());
|
||||
let mut session = Session::new(client, profile, env, config);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
pub use fabro_sandbox::test_support::{MockSandbox, MutableMockSandbox};
|
||||
|
||||
use crate::agent_profile::AgentProfile;
|
||||
use crate::config::SessionConfig;
|
||||
use crate::profiles::EnvContext;
|
||||
use crate::provider_profile::{ProfileCapabilities, ProviderProfile};
|
||||
use crate::sandbox::*;
|
||||
use crate::session::Session;
|
||||
use crate::skills::Skill;
|
||||
|
|
@ -21,7 +21,6 @@ use std::sync::{Arc, Mutex};
|
|||
|
||||
pub struct TestProfile {
|
||||
pub registry: ToolRegistry,
|
||||
pub parallel_tool_calls: bool,
|
||||
pub context_window: usize,
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +28,6 @@ impl TestProfile {
|
|||
pub fn new() -> Self {
|
||||
Self {
|
||||
registry: ToolRegistry::new(),
|
||||
parallel_tool_calls: false,
|
||||
context_window: 200_000,
|
||||
}
|
||||
}
|
||||
|
|
@ -37,29 +35,19 @@ impl TestProfile {
|
|||
pub fn with_tools(registry: ToolRegistry) -> Self {
|
||||
Self {
|
||||
registry,
|
||||
parallel_tool_calls: false,
|
||||
context_window: 200_000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parallel(registry: ToolRegistry) -> Self {
|
||||
pub fn with_context_window(registry: ToolRegistry, context_window: usize) -> Self {
|
||||
Self {
|
||||
registry,
|
||||
parallel_tool_calls: true,
|
||||
context_window: 200_000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parallel_with_context_window(registry: ToolRegistry, context_window: usize) -> Self {
|
||||
Self {
|
||||
registry,
|
||||
parallel_tool_calls: true,
|
||||
context_window,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProviderProfile for TestProfile {
|
||||
impl AgentProfile for TestProfile {
|
||||
fn provider(&self) -> Provider {
|
||||
Provider::Anthropic
|
||||
}
|
||||
|
|
@ -98,17 +86,8 @@ impl ProviderProfile for TestProfile {
|
|||
}
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> ProfileCapabilities {
|
||||
ProfileCapabilities {
|
||||
supports_reasoning: false,
|
||||
supports_streaming: false,
|
||||
supports_parallel_tool_calls: self.parallel_tool_calls,
|
||||
context_window_size: self.context_window,
|
||||
}
|
||||
}
|
||||
|
||||
fn knowledge_cutoff(&self) -> &'static str {
|
||||
"May 2025"
|
||||
fn context_window_size(&self) -> usize {
|
||||
self.context_window
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use fabro_agent::{AnthropicProfile, GeminiProfile, OpenAiProfile, ProviderProfile};
|
||||
use fabro_agent::{AgentProfile, AnthropicProfile, GeminiProfile, OpenAiProfile};
|
||||
use fabro_model::{Catalog, Provider};
|
||||
|
||||
#[test]
|
||||
|
|
@ -10,7 +10,7 @@ fn profile_context_window_matches_catalog_for_default_models() {
|
|||
.unwrap_or_else(|| panic!("no default model for {:?} in catalog", provider));
|
||||
let model = &catalog_info.id;
|
||||
|
||||
let profile: Box<dyn ProviderProfile> = match provider {
|
||||
let profile: Box<dyn AgentProfile> = match provider {
|
||||
Provider::OpenAi => Box::new(OpenAiProfile::new(model)),
|
||||
Provider::Kimi
|
||||
| Provider::Zai
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::path::Path;
|
|||
use std::sync::Arc;
|
||||
|
||||
use fabro_agent::{
|
||||
AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile, ProviderProfile, Session,
|
||||
AgentProfile, AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile, Session,
|
||||
SessionConfig, SubAgentManager, WebFetchSummarizer,
|
||||
};
|
||||
use fabro_llm::client::Client;
|
||||
|
|
@ -38,7 +38,7 @@ fn build_summarizer(provider: Provider, client: &Client) -> WebFetchSummarizer {
|
|||
}
|
||||
}
|
||||
|
||||
fn build_profile(provider: Provider, model: &str, client: &Client) -> Box<dyn ProviderProfile> {
|
||||
fn build_profile(provider: Provider, model: &str, client: &Client) -> Box<dyn AgentProfile> {
|
||||
let summarizer = Some(build_summarizer(provider, client));
|
||||
match provider {
|
||||
Provider::Anthropic => Box::new(AnthropicProfile::with_summarizer(model, summarizer)),
|
||||
|
|
@ -66,7 +66,7 @@ async fn make_session(provider: Provider, model: &str, cwd: &Path) -> Session {
|
|||
let factory_model: String = model.to_string();
|
||||
let factory_cwd = cwd.to_path_buf();
|
||||
let factory: fabro_agent::subagent::SessionFactory = Arc::new(move || {
|
||||
let sub_profile: Arc<dyn ProviderProfile> = {
|
||||
let sub_profile: Arc<dyn AgentProfile> = {
|
||||
let summarizer = Some(build_summarizer(provider, &factory_client));
|
||||
match provider {
|
||||
Provider::Anthropic => Arc::new(AnthropicProfile::with_summarizer(
|
||||
|
|
@ -99,7 +99,7 @@ async fn make_session(provider: Provider, model: &str, cwd: &Path) -> Session {
|
|||
});
|
||||
profile.register_subagent_tools(manager, factory, 0);
|
||||
|
||||
let profile: Arc<dyn ProviderProfile> = Arc::from(profile);
|
||||
let profile: Arc<dyn AgentProfile> = Arc::from(profile);
|
||||
let config = SessionConfig {
|
||||
max_turns: 20,
|
||||
..SessionConfig::default()
|
||||
|
|
@ -115,7 +115,7 @@ async fn make_session_with_config(
|
|||
) -> Session {
|
||||
dotenvy::dotenv().ok();
|
||||
let client = Client::from_env().await.expect("Client::from_env failed");
|
||||
let profile: Arc<dyn ProviderProfile> = Arc::from(build_profile(provider, model, &client));
|
||||
let profile: Arc<dyn AgentProfile> = Arc::from(build_profile(provider, model, &client));
|
||||
let env = Arc::new(LocalSandbox::new(cwd.to_path_buf()));
|
||||
Session::new(client, profile, env, config)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -596,10 +596,13 @@ fn apply_cache_control_to_conversation_prefix(messages: &mut [ApiMessage]) {
|
|||
|
||||
/// Collect beta headers from `provider_options` and merge with the caching header
|
||||
/// when auto-caching is active.
|
||||
const CONTEXT_1M_BETA_HEADER: &str = "context-1m-2025-08-07";
|
||||
|
||||
fn build_beta_header(
|
||||
provider_options: Option<&serde_json::Value>,
|
||||
include_cache_header: bool,
|
||||
include_fast_mode_header: bool,
|
||||
include_1m_context: bool,
|
||||
) -> Option<String> {
|
||||
let mut headers: Vec<String> = Vec::new();
|
||||
|
||||
|
|
@ -627,6 +630,11 @@ fn build_beta_header(
|
|||
headers.push(FAST_MODE_BETA_HEADER.to_string());
|
||||
}
|
||||
|
||||
// Add 1M context header for models with >= 1M context window
|
||||
if include_1m_context && !headers.iter().any(|h| h == CONTEXT_1M_BETA_HEADER) {
|
||||
headers.push(CONTEXT_1M_BETA_HEADER.to_string());
|
||||
}
|
||||
|
||||
if headers.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -1134,7 +1142,16 @@ fn build_api_request(
|
|||
(explicit_thinking, None)
|
||||
}
|
||||
} else {
|
||||
(explicit_thinking, None)
|
||||
// Auto-set adaptive thinking for known effort-capable models when no
|
||||
// explicit thinking config or reasoning_effort is provided.
|
||||
let thinking = explicit_thinking.or_else(|| {
|
||||
if model_info.is_some_and(|m| m.features.effort) {
|
||||
Some(serde_json::json!({"type": "adaptive"}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
(thinking, None)
|
||||
};
|
||||
|
||||
let is_fast = request.speed.as_deref() == Some("fast");
|
||||
|
|
@ -1168,9 +1185,13 @@ fn build_api_request(
|
|||
.header("x-api-key", &adapter.http.api_key)
|
||||
.header("anthropic-version", "2023-06-01");
|
||||
|
||||
if let Some(beta_str) =
|
||||
build_beta_header(request.provider_options.as_ref(), auto_cache, is_fast)
|
||||
{
|
||||
let include_1m_context = model_info.is_some_and(|m| m.context_window() >= 1_000_000);
|
||||
if let Some(beta_str) = build_beta_header(
|
||||
request.provider_options.as_ref(),
|
||||
auto_cache,
|
||||
is_fast,
|
||||
include_1m_context,
|
||||
) {
|
||||
req_builder = req_builder.header("anthropic-beta", beta_str);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -1548,13 +1569,13 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn beta_header_includes_cache_header() {
|
||||
let result = build_beta_header(None, true, false);
|
||||
let result = build_beta_header(None, true, false, false);
|
||||
assert_eq!(result, Some(CACHE_BETA_HEADER.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beta_header_no_cache_no_user_headers() {
|
||||
let result = build_beta_header(None, false, false);
|
||||
let result = build_beta_header(None, false, false, false);
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
|
|
@ -1565,7 +1586,7 @@ mod tests {
|
|||
"beta_headers": ["interleaved-thinking-2025-05-14"]
|
||||
}
|
||||
});
|
||||
let result = build_beta_header(Some(&opts), true, false);
|
||||
let result = build_beta_header(Some(&opts), true, false, false);
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(format!(
|
||||
|
|
@ -1581,7 +1602,7 @@ mod tests {
|
|||
"beta_headers": [CACHE_BETA_HEADER]
|
||||
}
|
||||
});
|
||||
let result = build_beta_header(Some(&opts), true, false);
|
||||
let result = build_beta_header(Some(&opts), true, false, false);
|
||||
// Should not duplicate the header
|
||||
assert_eq!(result, Some(CACHE_BETA_HEADER.to_string()));
|
||||
}
|
||||
|
|
@ -1593,7 +1614,7 @@ mod tests {
|
|||
"beta_headers": ["interleaved-thinking-2025-05-14"]
|
||||
}
|
||||
});
|
||||
let result = build_beta_header(Some(&opts), false, false);
|
||||
let result = build_beta_header(Some(&opts), false, false, false);
|
||||
assert_eq!(result, Some("interleaved-thinking-2025-05-14".to_string()));
|
||||
}
|
||||
|
||||
|
|
@ -2002,7 +2023,7 @@ mod tests {
|
|||
];
|
||||
|
||||
// No user headers — only cache header should appear
|
||||
let header = build_beta_header(None, true, false).unwrap_or_default();
|
||||
let header = build_beta_header(None, true, false, false).unwrap_or_default();
|
||||
for dep in &deprecated {
|
||||
assert!(
|
||||
!header.contains(dep),
|
||||
|
|
@ -2016,7 +2037,7 @@ mod tests {
|
|||
"beta_headers": ["interleaved-thinking-2025-05-14"]
|
||||
}
|
||||
});
|
||||
let header = build_beta_header(Some(&opts), true, false).unwrap_or_default();
|
||||
let header = build_beta_header(Some(&opts), true, false, false).unwrap_or_default();
|
||||
for dep in &deprecated {
|
||||
assert!(
|
||||
!header.contains(dep),
|
||||
|
|
@ -2173,7 +2194,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn beta_header_includes_both_cache_and_fast_mode() {
|
||||
let result = build_beta_header(None, true, true);
|
||||
let result = build_beta_header(None, true, true, false);
|
||||
let header = result.expect("should produce a header");
|
||||
assert!(
|
||||
header.contains(CACHE_BETA_HEADER),
|
||||
|
|
|
|||
|
|
@ -439,6 +439,7 @@ fn build_api_request(request: &Request) -> serde_json::Value {
|
|||
|
||||
let mut body = serde_json::to_value(&api_request).unwrap_or_default();
|
||||
merge_provider_options(&mut body, request.provider_options.as_ref());
|
||||
apply_default_safety_settings(&mut body);
|
||||
body
|
||||
}
|
||||
|
||||
|
|
@ -466,6 +467,22 @@ fn merge_provider_options(
|
|||
}
|
||||
}
|
||||
|
||||
/// Apply default safety settings if none were provided via provider_options.
|
||||
fn apply_default_safety_settings(body: &mut serde_json::Value) {
|
||||
if body.get("safety_settings").is_some() {
|
||||
return;
|
||||
}
|
||||
if let Some(body_map) = body.as_object_mut() {
|
||||
body_map.insert(
|
||||
"safety_settings".to_string(),
|
||||
serde_json::json!([{
|
||||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
"threshold": "BLOCK_ONLY_HIGH"
|
||||
}]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert `UsageMetadata` from the Gemini API into a unified `Usage`.
|
||||
fn parse_usage(metadata: Option<&UsageMetadata>) -> Usage {
|
||||
metadata.map_or_else(Usage::default, |u| {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"display_name": "Claude Opus 4.6",
|
||||
"limits": { "context_window": 1000000, "max_output": 128000 },
|
||||
"training": "2025-08-01",
|
||||
"knowledge_cutoff": "May 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 15.0,
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
"display_name": "Claude Sonnet 4.5",
|
||||
"limits": { "context_window": 200000, "max_output": 64000 },
|
||||
"training": "2025-08-01",
|
||||
"knowledge_cutoff": "May 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 3.0,
|
||||
|
|
@ -38,6 +40,7 @@
|
|||
"display_name": "Claude Sonnet 4.6",
|
||||
"limits": { "context_window": 200000, "max_output": 64000 },
|
||||
"training": "2025-08-01",
|
||||
"knowledge_cutoff": "May 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 3.0,
|
||||
|
|
@ -55,6 +58,7 @@
|
|||
"display_name": "Claude Haiku 4.5",
|
||||
"limits": { "context_window": 200000, "max_output": 8192 },
|
||||
"training": "2025-08-01",
|
||||
"knowledge_cutoff": "May 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": false },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 0.8,
|
||||
|
|
@ -71,6 +75,7 @@
|
|||
"display_name": "GPT-5.2",
|
||||
"limits": { "context_window": 1047576, "max_output": 128000 },
|
||||
"training": "2025-08-31",
|
||||
"knowledge_cutoff": "April 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 1.75,
|
||||
|
|
@ -87,6 +92,7 @@
|
|||
"display_name": "GPT-5 Mini",
|
||||
"limits": { "context_window": 1047576, "max_output": 128000 },
|
||||
"training": "2025-08-31",
|
||||
"knowledge_cutoff": "April 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 0.25,
|
||||
|
|
@ -103,6 +109,7 @@
|
|||
"display_name": "GPT-5.2 Codex",
|
||||
"limits": { "context_window": 1047576, "max_output": 128000 },
|
||||
"training": "2025-08-31",
|
||||
"knowledge_cutoff": "April 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 1.75,
|
||||
|
|
@ -119,6 +126,7 @@
|
|||
"display_name": "GPT-5.3 Codex",
|
||||
"limits": { "context_window": 1047576, "max_output": 128000 },
|
||||
"training": "2025-08-31",
|
||||
"knowledge_cutoff": "April 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 1.75,
|
||||
|
|
@ -135,6 +143,7 @@
|
|||
"display_name": "GPT-5.3 Codex Spark",
|
||||
"limits": { "context_window": 131072, "max_output": 128000 },
|
||||
"training": "2025-08-31",
|
||||
"knowledge_cutoff": "April 2025",
|
||||
"features": { "tools": true, "vision": false, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": null,
|
||||
|
|
@ -151,6 +160,7 @@
|
|||
"display_name": "GPT-5.4",
|
||||
"limits": { "context_window": 1047576, "max_output": 128000 },
|
||||
"training": "2025-08-31",
|
||||
"knowledge_cutoff": "April 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 2.5,
|
||||
|
|
@ -168,6 +178,7 @@
|
|||
"display_name": "GPT-5.4 Pro",
|
||||
"limits": { "context_window": 1047576, "max_output": 128000 },
|
||||
"training": "2025-08-31",
|
||||
"knowledge_cutoff": "April 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 30.0,
|
||||
|
|
@ -184,6 +195,7 @@
|
|||
"display_name": "GPT-5.4 Mini",
|
||||
"limits": { "context_window": 400000, "max_output": 128000 },
|
||||
"training": "2025-08-31",
|
||||
"knowledge_cutoff": "April 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 0.75,
|
||||
|
|
@ -200,6 +212,7 @@
|
|||
"display_name": "Gemini 3.1 Pro (Preview)",
|
||||
"limits": { "context_window": 1048576, "max_output": 65536 },
|
||||
"training": "2025-01-01",
|
||||
"knowledge_cutoff": "January 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 2.0,
|
||||
|
|
@ -217,6 +230,7 @@
|
|||
"display_name": "Gemini 3.1 Pro Custom Tools (Preview)",
|
||||
"limits": { "context_window": 1048576, "max_output": 65536 },
|
||||
"training": "2025-01-01",
|
||||
"knowledge_cutoff": "January 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 2.0,
|
||||
|
|
@ -233,6 +247,7 @@
|
|||
"display_name": "Gemini 3 Flash (Preview)",
|
||||
"limits": { "context_window": 1048576, "max_output": 65536 },
|
||||
"training": "2025-01-01",
|
||||
"knowledge_cutoff": "January 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 0.5,
|
||||
|
|
@ -249,6 +264,7 @@
|
|||
"display_name": "Gemini 3.1 Flash Lite (Preview)",
|
||||
"limits": { "context_window": 1048576, "max_output": 65536 },
|
||||
"training": "2025-01-01",
|
||||
"knowledge_cutoff": "January 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 0.25,
|
||||
|
|
@ -265,6 +281,7 @@
|
|||
"display_name": "Kimi K2.5",
|
||||
"limits": { "context_window": 262144, "max_output": 16000 },
|
||||
"training": "2025-10-01",
|
||||
"knowledge_cutoff": "October 2025",
|
||||
"features": { "tools": true, "vision": true, "reasoning": false },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 0.6,
|
||||
|
|
@ -282,6 +299,7 @@
|
|||
"display_name": "GLM 4.7",
|
||||
"limits": { "context_window": 202752, "max_output": 16384 },
|
||||
"training": null,
|
||||
"knowledge_cutoff": null,
|
||||
"features": { "tools": true, "vision": false, "reasoning": false },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 0.6,
|
||||
|
|
@ -299,6 +317,7 @@
|
|||
"display_name": "Minimax M2.5",
|
||||
"limits": { "context_window": 196608, "max_output": 16384 },
|
||||
"training": null,
|
||||
"knowledge_cutoff": null,
|
||||
"features": { "tools": true, "vision": false, "reasoning": false },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 0.3,
|
||||
|
|
@ -316,6 +335,7 @@
|
|||
"display_name": "Mercury 2",
|
||||
"limits": { "context_window": 131072, "max_output": 50000 },
|
||||
"training": null,
|
||||
"knowledge_cutoff": null,
|
||||
"features": { "tools": true, "vision": false, "reasoning": true, "effort": true },
|
||||
"costs": {
|
||||
"input_cost_per_mtok": 0.25,
|
||||
|
|
|
|||
|
|
@ -344,6 +344,7 @@ mod tests {
|
|||
max_output: Some(4096),
|
||||
},
|
||||
training: None,
|
||||
knowledge_cutoff: None,
|
||||
features: ModelFeatures {
|
||||
tools: true,
|
||||
vision: false,
|
||||
|
|
@ -448,6 +449,9 @@ mod tests {
|
|||
training: Some(
|
||||
"2025-08-01",
|
||||
),
|
||||
knowledge_cutoff: Some(
|
||||
"May 2025",
|
||||
),
|
||||
features: ModelFeatures {
|
||||
tools: true,
|
||||
vision: true,
|
||||
|
|
@ -515,6 +519,9 @@ mod tests {
|
|||
training: Some(
|
||||
"2025-01-01",
|
||||
),
|
||||
knowledge_cutoff: Some(
|
||||
"January 2025",
|
||||
),
|
||||
features: ModelFeatures {
|
||||
tools: true,
|
||||
vision: true,
|
||||
|
|
@ -569,6 +576,9 @@ mod tests {
|
|||
training: Some(
|
||||
"2025-10-01",
|
||||
),
|
||||
knowledge_cutoff: Some(
|
||||
"October 2025",
|
||||
),
|
||||
features: ModelFeatures {
|
||||
tools: true,
|
||||
vision: true,
|
||||
|
|
@ -628,6 +638,7 @@ mod tests {
|
|||
),
|
||||
},
|
||||
training: None,
|
||||
knowledge_cutoff: None,
|
||||
features: ModelFeatures {
|
||||
tools: true,
|
||||
vision: false,
|
||||
|
|
@ -677,6 +688,9 @@ mod tests {
|
|||
training: Some(
|
||||
"2025-08-31",
|
||||
),
|
||||
knowledge_cutoff: Some(
|
||||
"April 2025",
|
||||
),
|
||||
features: ModelFeatures {
|
||||
tools: true,
|
||||
vision: true,
|
||||
|
|
@ -724,6 +738,9 @@ mod tests {
|
|||
training: Some(
|
||||
"2025-08-31",
|
||||
),
|
||||
knowledge_cutoff: Some(
|
||||
"April 2025",
|
||||
),
|
||||
features: ModelFeatures {
|
||||
tools: true,
|
||||
vision: true,
|
||||
|
|
@ -797,6 +814,9 @@ mod tests {
|
|||
training: Some(
|
||||
"2025-08-31",
|
||||
),
|
||||
knowledge_cutoff: Some(
|
||||
"April 2025",
|
||||
),
|
||||
features: ModelFeatures {
|
||||
tools: true,
|
||||
vision: false,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ pub struct Model {
|
|||
pub display_name: String,
|
||||
pub limits: ModelLimits,
|
||||
pub training: Option<String>,
|
||||
pub knowledge_cutoff: Option<String>,
|
||||
pub features: ModelFeatures,
|
||||
pub costs: ModelCosts,
|
||||
pub estimated_output_tps: Option<f64>,
|
||||
|
|
@ -91,6 +92,10 @@ impl Model {
|
|||
self.training.as_deref()
|
||||
}
|
||||
|
||||
pub fn knowledge_cutoff(&self) -> Option<&str> {
|
||||
self.knowledge_cutoff.as_deref()
|
||||
}
|
||||
|
||||
pub fn input_cost_per_mtok(&self) -> Option<f64> {
|
||||
self.costs.input_cost_per_mtok
|
||||
}
|
||||
|
|
@ -135,6 +140,7 @@ mod tests {
|
|||
assert!(info.supports_reasoning());
|
||||
assert!(info.supports_effort());
|
||||
assert_eq!(info.training(), Some("2025-08-01"));
|
||||
assert_eq!(info.knowledge_cutoff(), Some("May 2025"));
|
||||
assert_eq!(info.input_cost_per_mtok(), Some(15.0));
|
||||
assert_eq!(info.output_cost_per_mtok(), Some(75.0));
|
||||
assert_eq!(info.cache_input_cost_per_mtok(), Some(1.5));
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ use std::sync::{Arc, Mutex};
|
|||
use std::time::Duration;
|
||||
|
||||
use fabro_agent::{
|
||||
AnthropicProfile, GeminiProfile, OpenAiProfile, ProviderProfile, Sandbox, Session,
|
||||
SessionConfig, SessionEvent, Turn,
|
||||
AgentProfile, AnthropicProfile, GeminiProfile, OpenAiProfile, Sandbox, Session, SessionConfig,
|
||||
SessionEvent, Turn,
|
||||
};
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_llm::provider::Provider;
|
||||
|
|
@ -151,7 +151,7 @@ pub async fn run_retro_agent(
|
|||
};
|
||||
profile.tool_registry_mut().register(submit_tool);
|
||||
|
||||
let profile: Arc<dyn ProviderProfile> = Arc::from(profile);
|
||||
let profile: Arc<dyn AgentProfile> = Arc::from(profile);
|
||||
|
||||
let config = SessionConfig {
|
||||
max_tool_rounds_per_input: 20,
|
||||
|
|
@ -323,7 +323,7 @@ fn spawn_retro_event_forwarder(
|
|||
})
|
||||
}
|
||||
|
||||
fn build_profile(provider: Provider, model: &str) -> Box<dyn ProviderProfile> {
|
||||
fn build_profile(provider: Provider, model: &str) -> Box<dyn AgentProfile> {
|
||||
match provider {
|
||||
Provider::OpenAi => Box::new(OpenAiProfile::new(model)),
|
||||
Provider::Kimi
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use async_trait::async_trait;
|
|||
|
||||
use fabro_agent::{
|
||||
subagent::{SessionFactory, SubAgentManager},
|
||||
AgentEvent, AnthropicProfile, GeminiProfile, OpenAiProfile, ProviderProfile, Sandbox, Session,
|
||||
AgentEvent, AgentProfile, AnthropicProfile, GeminiProfile, OpenAiProfile, Sandbox, Session,
|
||||
SessionConfig, Turn,
|
||||
};
|
||||
use fabro_llm::client::Client;
|
||||
|
|
@ -20,7 +20,7 @@ use crate::handler::agent::{CodergenBackend, CodergenResult};
|
|||
use crate::outcome::StageUsage;
|
||||
use fabro_graphviz::graph::Node;
|
||||
|
||||
fn build_profile(model: &str, provider: Provider) -> Box<dyn ProviderProfile> {
|
||||
fn build_profile(model: &str, provider: Provider) -> Box<dyn AgentProfile> {
|
||||
match provider {
|
||||
Provider::OpenAi => Box::new(OpenAiProfile::new(model)),
|
||||
Provider::Kimi
|
||||
|
|
@ -214,7 +214,7 @@ impl AgentApiBackend {
|
|||
let factory_env = Arc::clone(sandbox);
|
||||
let factory_tool_env = env.clone();
|
||||
let factory: SessionFactory = Arc::new(move || {
|
||||
let child_profile: Arc<dyn ProviderProfile> = match provider {
|
||||
let child_profile: Arc<dyn AgentProfile> = match provider {
|
||||
Provider::OpenAi => Arc::new(OpenAiProfile::new(&factory_model)),
|
||||
Provider::Kimi
|
||||
| Provider::Zai
|
||||
|
|
@ -239,7 +239,7 @@ impl AgentApiBackend {
|
|||
});
|
||||
|
||||
profile.register_subagent_tools(manager, factory, 0);
|
||||
let profile: Arc<dyn ProviderProfile> = Arc::from(profile);
|
||||
let profile: Arc<dyn AgentProfile> = Arc::from(profile);
|
||||
|
||||
let mut session = Session::new(client, profile, Arc::clone(sandbox), config);
|
||||
if !env.is_empty() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue