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