fabro/lib/crates/fabro-llm/tests/integration.rs
Bryan Helmkamp 28884ae093 rename Arc to Fabro in all Rust crates, symbols, env vars, and supporting files
- Rename 20 crate directories lib/crates/arc-* → fabro-*
- Update all Cargo.toml: crate names, dep paths, feature flags, bin name
- Rename arc_server module → fabro_server in fabro-llm
- ArcError → FabroError across 30+ files
- ARC_VERSION/ARC_GIT_SHA/ARC_BUILD_DATE → FABRO_* constants
- All use/qualified paths: arc_agent:: → fabro_agent::, etc. (~1500 occurrences)
- Env vars ARC_* → FABRO_* in string literals and shell scripts
- String literals: X-Arc-Demo, arc-bot, arc@local, arc-web, arc-mcp, etc.
- Path strings: .arc/ → .fabro/, arc.toml → fabro.toml, refs/arc/ → refs/fabro/
- arc-api.yaml → fabro-api.yaml (OpenAPI spec)
- skills/arc-create-workflow → fabro-create-workflow
- trycmd fixtures: $ arc → $ fabro
- Inline snapshots (insta) updated
- CI, Docker, install.sh, scripts, CLAUDE.md, AGENTS.md
- TypeScript app: env vars, headers, JWT issuer
- Docs: page slugs, git refs, config paths, sandbox names, repo URLs
- Repo references: brynary/arc → fabro-sh/fabro

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 12:25:58 -04:00

194 lines
6.6 KiB
Rust

use fabro_llm::provider::ProviderAdapter;
use fabro_llm::providers::{AnthropicAdapter, GeminiAdapter, OpenAiAdapter};
use fabro_llm::types::{Message, Request};
fn make_request(model: &str) -> Request {
Request {
model: model.to_string(),
messages: vec![Message::user("Say hello in exactly one word")],
provider: None,
tools: None,
tool_choice: None,
response_format: None,
temperature: Some(0.0),
top_p: None,
max_tokens: Some(50),
stop_sequences: None,
reasoning_effort: None,
metadata: None,
provider_options: None,
}
}
#[tokio::test]
#[ignore = "requires ANTHROPIC_API_KEY"]
async fn anthropic_complete() {
dotenvy::dotenv().ok();
let api_key = std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY must be set");
let adapter = AnthropicAdapter::new(api_key);
let request = make_request("claude-haiku-4-5");
let response = adapter.complete(&request).await.unwrap();
assert!(
!response.text().is_empty(),
"response text should not be empty"
);
assert_eq!(response.finish_reason, fabro_llm::types::FinishReason::Stop);
assert!(response.usage.input_tokens > 0);
assert!(response.usage.output_tokens > 0);
assert_eq!(response.provider, "anthropic");
}
#[tokio::test]
#[ignore = "requires OPENAI_API_KEY"]
async fn openai_complete() {
dotenvy::dotenv().ok();
let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY must be set");
let adapter = OpenAiAdapter::new(api_key);
let request = make_request("gpt-4o-mini");
let response = adapter.complete(&request).await.unwrap();
assert!(
!response.text().is_empty(),
"response text should not be empty"
);
assert_eq!(response.finish_reason, fabro_llm::types::FinishReason::Stop);
assert!(response.usage.input_tokens > 0);
assert!(response.usage.output_tokens > 0);
assert_eq!(response.provider, "openai");
}
#[tokio::test]
#[ignore = "requires OPENAI_API_KEY"]
async fn openai_gpt_5_3_codex_complete() {
let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY must be set");
let adapter = OpenAiAdapter::new(api_key);
let request = make_request("gpt-5.3-codex");
let response = adapter.complete(&request).await.unwrap();
assert!(
!response.text().is_empty(),
"response text should not be empty"
);
assert!(response.usage.input_tokens > 0);
assert!(response.usage.output_tokens > 0);
assert_eq!(response.provider, "openai");
}
#[tokio::test]
#[ignore = "requires GEMINI_API_KEY"]
async fn gemini_complete() {
dotenvy::dotenv().ok();
let api_key = std::env::var("GEMINI_API_KEY").expect("GEMINI_API_KEY must be set");
let adapter = GeminiAdapter::new(api_key);
let request = make_request("gemini-2.5-flash");
let response = adapter.complete(&request).await.unwrap();
assert!(
!response.text().is_empty(),
"response text should not be empty"
);
assert_eq!(response.finish_reason, fabro_llm::types::FinishReason::Stop);
assert!(response.usage.input_tokens > 0);
assert!(response.usage.output_tokens > 0);
assert_eq!(response.provider, "gemini");
}
async fn run_multi_turn_cache_test(
adapter: &dyn ProviderAdapter,
model: &str,
min_cache_ratio: f64,
) {
// Claude Haiku 4.5 requires 4096 tokens minimum for prompt caching.
// Each repeat is ~78 tokens; 70 repeats ≈ 5460 tokens, safely above the threshold.
let padding = "This is a detailed context paragraph that provides background information \
about the conversation. It contains various facts and details that the model should \
remember throughout the multi-turn interaction. The purpose of this padding is to \
ensure the system prompt exceeds the minimum cache threshold for the provider. \
We include information about mathematics, science, history, and general knowledge. \
The model should use this context when answering questions. "
.repeat(70);
let system_message = Message::system(format!(
"You are a helpful math assistant. Answer briefly.\n\n{padding}"
));
let questions = [
"What is 1+1?",
"What is 2+2?",
"What is 3+3?",
"What is 4+4?",
"What is 5+5?",
"What is 6+6?",
];
let mut messages = vec![system_message, Message::user(questions[0])];
let mut best_cache_ratio = 0.0_f64;
for turn in 0..6 {
let request = Request {
model: model.to_string(),
messages: messages.clone(),
provider: None,
tools: None,
tool_choice: None,
response_format: None,
temperature: Some(0.0),
top_p: None,
max_tokens: Some(100),
stop_sequences: None,
reasoning_effort: None,
metadata: None,
provider_options: None,
};
let response = adapter.complete(&request).await.unwrap();
let text = response.text();
assert!(
!text.is_empty(),
"response text should not be empty on turn {turn}"
);
let cache_read = response.usage.cache_read_tokens.unwrap_or(0) as f64;
let input = response.usage.input_tokens as f64;
let ratio = cache_read / input;
best_cache_ratio = best_cache_ratio.max(ratio);
messages.push(Message::assistant(text));
if turn < 5 {
messages.push(Message::user(questions[turn + 1]));
}
}
assert!(
best_cache_ratio >= min_cache_ratio,
"best cache ratio {best_cache_ratio:.3} should be at least {min_cache_ratio} across all turns"
);
}
#[tokio::test]
#[ignore = "requires ANTHROPIC_API_KEY"]
async fn anthropic_multi_turn_cache() {
dotenvy::dotenv().ok();
let api_key = std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY must be set");
let adapter = AnthropicAdapter::new(api_key);
run_multi_turn_cache_test(&adapter, "claude-haiku-4-5", 0.5).await;
}
#[tokio::test]
#[ignore = "requires OPENAI_API_KEY"]
async fn openai_multi_turn_cache() {
dotenvy::dotenv().ok();
let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY must be set");
let adapter = OpenAiAdapter::new(api_key);
run_multi_turn_cache_test(&adapter, "gpt-4o-mini", 0.5).await;
}
#[tokio::test]
#[ignore = "requires GEMINI_API_KEY"]
async fn gemini_multi_turn_cache() {
dotenvy::dotenv().ok();
let api_key = std::env::var("GEMINI_API_KEY").expect("GEMINI_API_KEY must be set");
let adapter = GeminiAdapter::new(api_key);
run_multi_turn_cache_test(&adapter, "gemini-2.5-flash", 0.5).await;
}