Skip to main content

artifactize_core/llm/
api.rs

1//! The wire APIs artifactize speaks, and what each accepts.
2
3use serde::{Deserialize, Serialize};
4
5use crate::config::Provider;
6
7/// The API a review's requests use: a built-in provider's, or a custom endpoint's.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Api {
10    OpenaiResponses,
11    OpenaiChat,
12    Anthropic,
13    Gemini,
14    Codex,
15}
16
17impl From<Provider> for Api {
18    fn from(provider: Provider) -> Self {
19        match provider {
20            Provider::Openai => Self::OpenaiResponses,
21            Provider::Anthropic => Self::Anthropic,
22            Provider::Gemini => Self::Gemini,
23            Provider::Codex => Self::Codex,
24        }
25    }
26}
27
28impl From<EndpointApi> for Api {
29    fn from(api: EndpointApi) -> Self {
30        match api {
31            EndpointApi::OpenaiResponses => Self::OpenaiResponses,
32            EndpointApi::OpenaiChat => Self::OpenaiChat,
33            EndpointApi::Anthropic => Self::Anthropic,
34            EndpointApi::Gemini => Self::Gemini,
35        }
36    }
37}
38
39impl Api {
40    /// Declared effort is exact: an effort the API does not take fails rather than remaps.
41    pub fn validate_reasoning(self, reasoning: &str) -> Result<(), String> {
42        let supported = match self {
43            // Codex takes the Responses efforts as they are, as Pi's openai-codex does; Chat
44            // Completions' `reasoning_effort` takes the same names.
45            Self::OpenaiResponses | Self::OpenaiChat | Self::Codex => matches!(
46                reasoning,
47                "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
48            ),
49            Self::Anthropic => matches!(reasoning, "low" | "medium" | "high" | "max"),
50            // Gemini 3's thinking levels.
51            Self::Gemini => matches!(reasoning, "minimal" | "low" | "medium" | "high"),
52        };
53        if supported {
54            Ok(())
55        } else {
56            Err(format!(
57                "Unsupported reasoning {reasoning:?} for {self:?}; effort is never remapped."
58            ))
59        }
60    }
61
62    /// Whether final answers use OpenAI's structured-output subset (closed objects, every
63    /// property required, numeric and array bounds, size limits) rather than the smaller
64    /// keyword set Anthropic and Gemini share.
65    pub(crate) fn openai_schema(self) -> bool {
66        matches!(self, Self::OpenaiResponses | Self::OpenaiChat | Self::Codex)
67    }
68}
69
70/// The API a custom endpoint speaks.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
72#[serde(rename_all = "kebab-case")]
73pub enum EndpointApi {
74    /// OpenAI Responses (`/responses`).
75    OpenaiResponses,
76    /// OpenAI Chat Completions (`/chat/completions`), which most compatible servers offer.
77    OpenaiChat,
78    /// Anthropic Messages (`/v1/messages`).
79    Anthropic,
80    /// Gemini GenerateContent.
81    Gemini,
82}