feat: Implement openai/ollama client

This commit is contained in:
2026-01-06 23:50:05 +09:00
parent 170c8708ae
commit a7581f27bb
27 changed files with 2871 additions and 167 deletions
-118
View File
@@ -1,118 +0,0 @@
//! APIレスポンス記録ツール
//!
//! 実際のAnthropicAPIからのレスポンスをファイルに記録する。
//! 後でテストフィクスチャとして使用可能。
//!
//! ## 使用方法
//!
//! ```bash
//! # 記録モード (APIを呼び出して記録)
//! ANTHROPIC_API_KEY=your-key cargo run --example record_anthropic
//!
//! # 記録されたファイルは worker/tests/fixtures/ に保存される
//! ```
use std::fs::{self, File};
use std::io::{BufWriter, Write};
use std::path::Path;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use futures::StreamExt;
use worker::llm_client::{LlmClient, Request, providers::anthropic::AnthropicClient};
/// 記録されたSSEイベント
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct RecordedEvent {
elapsed_ms: u64,
event_type: String,
data: String,
}
/// セッションメタデータ
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct SessionMetadata {
timestamp: u64,
model: String,
description: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("ANTHROPIC_API_KEY")
.expect("ANTHROPIC_API_KEY environment variable must be set");
let model = "claude-sonnet-4-20250514";
let description = "Simple greeting test";
println!("=== Anthropic API Response Recorder ===\n");
println!("Model: {}", model);
println!("Description: {}\n", description);
// クライアントを作成
let client = AnthropicClient::new(&api_key, model);
// シンプルなリクエスト
let request = Request::new()
.system("You are a helpful assistant. Be very concise.")
.user("Say hello in one word.")
.max_tokens(50);
println!("📤 Sending request...\n");
// レスポンスを記録
let start_time = Instant::now();
let mut events: Vec<RecordedEvent> = Vec::new();
let mut stream = client.stream(request).await?;
while let Some(result) = stream.next().await {
let elapsed = start_time.elapsed().as_millis() as u64;
match result {
Ok(event) => {
// Eventをシリアライズして記録
let event_json = serde_json::to_string(&event)?;
println!("[{:>6}ms] {:?}", elapsed, event);
events.push(RecordedEvent {
elapsed_ms: elapsed,
event_type: format!("{:?}", std::mem::discriminant(&event)),
data: event_json,
});
}
Err(e) => {
eprintln!("Error: {}", e);
break;
}
}
}
println!("\n📊 Recorded {} events", events.len());
// ファイルに保存
let fixtures_dir = Path::new("worker/tests/fixtures");
fs::create_dir_all(fixtures_dir)?;
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let filename = format!("anthropic_{}.jsonl", timestamp);
let filepath = fixtures_dir.join(&filename);
let file = File::create(&filepath)?;
let mut writer = BufWriter::new(file);
// メタデータを書き込み
let metadata = SessionMetadata {
timestamp,
model: model.to_string(),
description: description.to_string(),
};
writeln!(writer, "{}", serde_json::to_string(&metadata)?)?;
// イベントを書き込み
for event in &events {
writeln!(writer, "{}", serde_json::to_string(event)?)?;
}
writer.flush()?;
println!("💾 Saved to: {}", filepath.display());
Ok(())
}
+135 -44
View File
@@ -16,80 +16,171 @@
//! ANTHROPIC_API_KEY=your-key cargo run --example record_test_fixtures -- --all
//! ```
mod recorder;
mod scenarios;
use clap::{Parser, ValueEnum};
use worker::llm_client::providers::anthropic::AnthropicClient;
use worker::llm_client::providers::openai::OpenAIClient;
fn print_usage() {
println!("Usage: cargo run --example record_test_fixtures -- <scenario_name>");
println!(" cargo run --example record_test_fixtures -- --all");
println!();
println!("Available scenarios:");
for scenario in scenarios::scenarios() {
println!(" {:20} - {}", scenario.output_name, scenario.name);
}
println!();
println!("Options:");
println!(" --all Record all scenarios");
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Scenario name
#[arg(short, long)]
scenario: Option<String>,
/// Run all scenarios
#[arg(long, default_value_t = false)]
all: bool,
/// Client to use
#[arg(short, long, value_enum, default_value_t = ClientType::Anthropic)]
client: ClientType,
/// Model to use (optional, defaults per client)
#[arg(short, long)]
model: Option<String>,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
enum ClientType {
Anthropic,
Openai,
Ollama,
}
async fn run_scenario_with_anthropic(
scenario: &scenarios::TestScenario,
subdir: &str,
model: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("ANTHROPIC_API_KEY")
.expect("ANTHROPIC_API_KEY environment variable must be set");
let model = model.as_deref().unwrap_or("claude-sonnet-4-20250514");
let client = AnthropicClient::new(&api_key, model);
recorder::record_request(
&client,
scenario.request.clone(),
scenario.name,
scenario.output_name,
subdir,
model,
)
.await?;
Ok(())
}
async fn run_scenario_with_openai(
scenario: &scenarios::TestScenario,
subdir: &str,
model: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY environment variable must be set");
let model = model.as_deref().unwrap_or("gpt-4o");
let client = OpenAIClient::new(&api_key, model);
recorder::record_request(
&client,
scenario.request.clone(),
scenario.name,
scenario.output_name,
subdir,
model,
)
.await?;
Ok(())
}
async fn run_scenario_with_ollama(
scenario: &scenarios::TestScenario,
subdir: &str,
model: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
use worker::llm_client::providers::ollama::OllamaClient;
// Ollama typically runs local, no key needed or placeholder
let model = model.as_deref().unwrap_or("llama3"); // default example
let client = OllamaClient::new(model); // base_url placeholder, handled by client default
recorder::record_request(
&client,
scenario.request.clone(),
scenario.name,
scenario.output_name,
subdir,
model,
)
.await?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
dotenv::dotenv().ok();
let args = Args::parse();
// 引数がなければ使い方を表示
if args.len() < 2 {
print_usage();
return Ok(());
if !args.all && args.scenario.is_none() {
use clap::CommandFactory;
let mut cmd = Args::command();
cmd.error(
clap::error::ErrorKind::MissingRequiredArgument,
"Either --all or --scenario <SCENARIO> must be provided",
)
.exit();
}
let arg = &args[1];
// 全シナリオを取得
let all_scenarios = scenarios::scenarios();
// 実行するシナリオを決定
let scenarios_to_run: Vec<_> = if arg == "--all" {
// Determine scenarios to run
let scenarios_to_run: Vec<_> = if args.all {
all_scenarios
} else {
// 指定されたシナリオを検索
let scenario_name = args.scenario.as_ref().unwrap();
let found: Vec<_> = all_scenarios
.into_iter()
.filter(|s| s.output_name == arg)
.filter(|s| s.output_name == scenario_name)
.collect();
if found.is_empty() {
eprintln!("Error: Unknown scenario '{}'", arg);
println!();
print_usage();
std::process::exit(1);
eprintln!("Error: Unknown scenario '{}'", scenario_name);
// Verify correct name by listing
println!("Available scenarios:");
for s in scenarios::scenarios() {
println!(" {}", s.output_name);
}
std::process::exit(1);
}
found
};
// APIキーを取得
let api_key = std::env::var("ANTHROPIC_API_KEY")
.expect("ANTHROPIC_API_KEY environment variable must be set");
let model = "claude-sonnet-4-20250514";
println!("=== Test Fixture Generator ===");
println!("Model: {}", model);
println!("Client: {:?}", args.client);
if let Some(ref m) = args.model {
println!("Model: {}", m);
}
println!("Scenarios: {}\n", scenarios_to_run.len());
let client = AnthropicClient::new(&api_key, model);
let subdir = match args.client {
ClientType::Anthropic => "anthropic",
ClientType::Openai => "openai",
ClientType::Ollama => "ollama",
};
// シナリオを記録
// シナリオのフィルタリングは main.rs のロジックで実行済み
// ここでは単純なループで実行
for scenario in scenarios_to_run {
recorder::record_request(
&client,
scenario.request,
scenario.name,
scenario.output_name,
model,
)
.await?;
match args.client {
ClientType::Anthropic => run_scenario_with_anthropic(&scenario, subdir, args.model.clone()).await?,
ClientType::Openai => run_scenario_with_openai(&scenario, subdir, args.model.clone()).await?,
ClientType::Ollama => run_scenario_with_ollama(&scenario, subdir, args.model.clone()).await?,
}
}
println!("\n✅ Done!");
@@ -49,6 +49,7 @@ pub async fn record_request<C: LlmClient>(
request: Request,
description: &str,
output_name: &str,
subdir: &str, // e.g. "anthropic", "openai"
model: &str,
) -> Result<usize, Box<dyn std::error::Error>> {
println!("\n📝 Recording: {}", description);
@@ -78,8 +79,8 @@ pub async fn record_request<C: LlmClient>(
}
// 保存
let fixtures_dir = Path::new("worker/tests/fixtures");
fs::create_dir_all(fixtures_dir)?;
let fixtures_dir = Path::new("worker/tests/fixtures").join(subdir);
fs::create_dir_all(&fixtures_dir)?;
let filepath = fixtures_dir.join(format!("{}.jsonl", output_name));
@@ -19,6 +19,7 @@ pub fn scenarios() -> Vec<TestScenario> {
vec![
simple_text_scenario(),
tool_call_scenario(),
long_text_scenario(),
]
}
@@ -59,3 +60,15 @@ fn tool_call_scenario() -> TestScenario {
.max_tokens(200),
}
}
/// 長文生成シナリオ
fn long_text_scenario() -> TestScenario {
TestScenario {
name: "Long text response",
output_name: "long_text",
request: Request::new()
.system("You are a creative writer.")
.user("Write a short story about a robot discovering a garden. It should be at least 300 words.")
.max_tokens(1000),
}
}