codetether-agent 4.5.2

A2A-native AI coding agent for the CodeTether ecosystem
Documentation
use crate::provider::{CompletionRequest, ContentPart, Message, ProviderRegistry, Role};
use anyhow::{Context, Result, anyhow};
use serde::Deserialize;
use serde_json::json;
use std::sync::Arc;
use tokio::sync::OnceCell;

#[derive(Debug, Deserialize)]
struct MorphResponse {
    choices: Vec<MorphChoice>,
}

#[derive(Debug, Deserialize)]
struct MorphChoice {
    message: MorphMessage,
}

#[derive(Debug, Deserialize)]
struct MorphMessage {
    #[serde(default)]
    content: Option<String>,
}

static MORPH_PROVIDER_REGISTRY: OnceCell<Arc<ProviderRegistry>> = OnceCell::const_new();

fn backend_enabled() -> bool {
    match std::env::var("CODETETHER_MORPH_TOOL_BACKEND") {
        Ok(v) => matches!(
            v.trim().to_ascii_lowercase().as_str(),
            "1" | "true" | "yes" | "on"
        ),
        Err(_) => false,
    }
}

pub fn should_use_morph_backend() -> bool {
    backend_enabled()
}

fn morph_model_ref() -> String {
    let configured = std::env::var("CODETETHER_MORPH_TOOL_MODEL")
        .ok()
        .filter(|v| !v.trim().is_empty())
        .unwrap_or_else(|| "openrouter/morph/morph-v3-large".to_string());
    let trimmed = configured.trim();
    if trimmed.starts_with("openrouter/") {
        return trimmed.to_string();
    }
    if trimmed.starts_with("morph/") {
        return format!("openrouter/{trimmed}");
    }
    trimmed.to_string()
}

fn direct_openrouter_base_url() -> Option<String> {
    std::env::var("CODETETHER_OPENROUTER_BASE_URL")
        .ok()
        .filter(|v| !v.trim().is_empty())
        .map(|v| v.trim().trim_end_matches('/').to_string())
}

async fn get_registry() -> Result<Arc<ProviderRegistry>> {
    let registry = MORPH_PROVIDER_REGISTRY
        .get_or_try_init(|| async {
            let loaded = ProviderRegistry::from_vault().await?;
            Ok::<_, anyhow::Error>(Arc::new(loaded))
        })
        .await?;
    Ok(registry.clone())
}

async fn apply_with_provider(prompt: &str) -> Result<String> {
    let model_ref = morph_model_ref();
    let registry = get_registry()
        .await
        .context("Failed to initialize provider registry for Morph backend")?;
    let (provider, model) = registry
        .resolve_model(&model_ref)
        .with_context(|| format!("Failed to resolve Morph model '{model_ref}'"))?;

    tracing::info!(
        provider = %provider.name(),
        model = %model,
        "Executing Morph-backed edit via provider registry"
    );

    let request = CompletionRequest {
        messages: vec![Message {
            role: Role::User,
            content: vec![ContentPart::Text {
                text: prompt.to_string(),
            }],
        }],
        tools: vec![],
        model,
        temperature: None,
        top_p: None,
        max_tokens: None,
        stop: vec![],
    };

    let response = provider
        .complete(request)
        .await
        .context("Morph provider request failed")?;

    let content = response
        .message
        .content
        .into_iter()
        .filter_map(|part| match part {
            ContentPart::Text { text } => Some(text),
            _ => None,
        })
        .collect::<Vec<_>>()
        .join("\n");
    let content = content.trim().to_string();
    if content.is_empty() {
        return Err(anyhow!("Morph backend returned empty content"));
    }
    Ok(content)
}

async fn apply_with_direct_openrouter(prompt: &str, base_url: &str) -> Result<String> {
    let api_key = std::env::var("OPENROUTER_API_KEY").context(
        "OPENROUTER_API_KEY is required when CODETETHER_OPENROUTER_BASE_URL is set for direct Morph backend calls",
    )?;
    let model_ref = morph_model_ref();
    let model = if let Some(stripped) = model_ref.strip_prefix("openrouter/") {
        stripped.to_string()
    } else {
        model_ref
    };
    let body = json!({
        "model": model,
        "messages": [{
            "role": "user",
            "content": prompt
        }]
    });

    let client = reqwest::Client::new();
    let resp = client
        .post(format!("{base_url}/chat/completions"))
        .header("Authorization", format!("Bearer {api_key}"))
        .header("Content-Type", "application/json")
        .header("HTTP-Referer", "https://codetether.run")
        .header("X-Title", "CodeTether Agent")
        .json(&body)
        .send()
        .await
        .context("Failed to send Morph request")?;

    let status = resp.status();
    let text = resp.text().await.context("Failed to read Morph response")?;
    if !status.is_success() {
        anyhow::bail!("Morph backend error: {} {}", status, text);
    }

    let parsed: MorphResponse =
        serde_json::from_str(&text).context("Failed to parse Morph response JSON")?;
    let content = parsed
        .choices
        .first()
        .and_then(|c| c.message.content.clone())
        .filter(|s| !s.trim().is_empty())
        .ok_or_else(|| anyhow!("Morph backend returned empty content"))?;

    Ok(content)
}

pub async fn apply_edit_with_morph(code: &str, instruction: &str, update: &str) -> Result<String> {
    let prompt = format!(
        "<instruction>{instruction}</instruction>\n<code>{code}</code>\n<update>{update}</update>"
    );
    if let Some(base_url) = direct_openrouter_base_url() {
        tracing::info!(
            base_url = %base_url,
            "Using direct OpenRouter HTTP path for Morph backend"
        );
        return apply_with_direct_openrouter(&prompt, &base_url).await;
    }
    apply_with_provider(&prompt).await
}