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