Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 20 additions & 1 deletion src/anthropic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,28 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::api::{Message, Role};
use crate::backend::Backend;

const MAX_TOKENS: u32 = 80000;

/// The Anthropic backend: an API key and the model to call.
pub struct Anthropic {
key: String,
model: String,
}

impl Anthropic {
pub fn new(key: String, model: String) -> Self {
Anthropic { key, model }
}
}

impl Backend for Anthropic {
fn complete(&self, messages: &[Message]) -> anyhow::Result<String> {
send(&self.key, &self.model, messages)
}
}

/// Anthropic 400s on an empty text block, so render empty fields as a visible
/// placeholder.
fn field_or_placeholder(field: &str) -> &str {
Expand Down Expand Up @@ -77,7 +96,7 @@ enum ResponseBlock {
}

/// Send a chat-style prompt to the Claude Messages API and return the text.
pub fn complete(api_key: &str, model: &str, messages: &[Message]) -> anyhow::Result<String> {
fn send(api_key: &str, model: &str, messages: &[Message]) -> anyhow::Result<String> {
let req = build_request(model, messages);

tracing::debug!(
Expand Down
64 changes: 64 additions & 0 deletions src/backend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! The model-backend interface: one trait both providers implement, plus the
//! single place where a `Provider` choice is turned into a ready-to-call,
//! key-bearing backend.

use anyhow::Result;

use crate::anthropic::Anthropic;
use crate::api::Message;
use crate::config_files::{Provider, Secrets};
use crate::openai::Openai;

/// A resolved model backend — provider, key, and model already settled. Callers
/// hand it refac's provider-agnostic [`Message`]s and get back the completion.
///
/// Resolved to `Box<dyn Backend>` so call sites depend only on the interface,
/// never on which provider answered. The trait is where the upcoming tool /
/// function-call round-trip lands (both providers support it), keeping the
/// edit loop provider-agnostic.
pub trait Backend {
/// Send the conversation and return the model's text output.
fn complete(&self, messages: &[Message]) -> Result<String>;
}

/// Turn a resolved provider + model into a callable backend, failing if that
/// provider's API key is missing. This is the one spot that knows how each
/// provider sources its key, so the rest of refac stays provider-agnostic.
pub fn resolve(provider: Provider, model: &str, secrets: &Secrets) -> Result<Box<dyn Backend>> {
match provider {
Provider::Anthropic => {
let key = secrets.anthropic_api_key.clone().ok_or_else(|| {
anyhow::anyhow!("No Anthropic API key found. Set ANTHROPIC_API_KEY or run 'refac login'.")
})?;
Ok(Box::new(Anthropic::new(key, model.to_string())))
}
Provider::Openai => {
let key = secrets.openai_api_key.clone().ok_or_else(|| {
anyhow::anyhow!("No OpenAI API key found. Set OPENAI_API_KEY or run 'refac login'.")
})?;
Ok(Box::new(Openai::new(key, model.to_string())))
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn resolve_errors_without_a_key() {
let secrets = Secrets::default();
assert!(resolve(Provider::Anthropic, "m", &secrets).is_err());
assert!(resolve(Provider::Openai, "m", &secrets).is_err());
}

#[test]
fn resolve_succeeds_with_the_matching_key() {
let secrets = Secrets {
anthropic_api_key: Some("a".into()),
openai_api_key: Some("o".into()),
};
assert!(resolve(Provider::Anthropic, "m", &secrets).is_ok());
assert!(resolve(Provider::Openai, "m", &secrets).is_ok());
}
}
20 changes: 2 additions & 18 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod anthropic;
mod api;
mod api_client;
mod backend;
mod config_files;
mod openai;
mod prompt;
Expand Down Expand Up @@ -109,24 +110,7 @@ fn refactor(
let provider = config.provider(sc);
let model = config.model(provider);

let output = match provider {
Provider::Anthropic => {
let key = sc.anthropic_api_key.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"No Anthropic API key found. Set ANTHROPIC_API_KEY or run 'refac login'."
)
})?;
anthropic::complete(key, &model, &messages)?
}
Provider::Openai => {
let key = sc.openai_api_key.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"No OpenAI API key found. Set OPENAI_API_KEY or run 'refac login'."
)
})?;
openai::complete(key, &model, &messages)?
}
};
let output = backend::resolve(provider, &model, sc)?.complete(&messages)?;

log(
LogEntry {
Expand Down
21 changes: 20 additions & 1 deletion src/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,28 @@ use serde::{Deserialize, Serialize};

use crate::api::{Message, Role};
use crate::api_client::{Client, Endpoint, Req};
use crate::backend::Backend;

/// The OpenAI backend: an API key and the model to call.
pub struct Openai {
key: String,
model: String,
}

impl Openai {
pub fn new(key: String, model: String) -> Self {
Openai { key, model }
}
}

impl Backend for Openai {
fn complete(&self, messages: &[Message]) -> anyhow::Result<String> {
send(&self.key, &self.model, messages)
}
}

/// Send refac's messages to the OpenAI chat-completions API and return the text.
pub fn complete(api_key: &str, model: &str, messages: &[Message]) -> anyhow::Result<String> {
fn send(api_key: &str, model: &str, messages: &[Message]) -> anyhow::Result<String> {
let client = Client::new(api_key);

// OpenAI takes one string per message; sending each field as its own message
Expand Down