Skip to main content

edgequake_llm/
api_format.rs

1//! SPEC-131 — upstream LLM HTTP API format selection.
2//!
3//! Transport is configuration, not model identity (LAW-131-5).
4
5use crate::error::{LlmError, Result};
6
7/// Environment variable selecting Chat Completions vs Responses transport.
8pub const ENV_API_FORMAT: &str = "EDGEQUAKE_LLM_API_FORMAT";
9
10/// Upstream OpenAI-compatible request shape.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum ApiFormat {
13    /// `POST …/chat/completions` (product default).
14    #[default]
15    ChatCompletions,
16    /// `POST …/responses` (Bedrock Mantle GPT-5.6, Open Responses).
17    Responses,
18}
19
20impl ApiFormat {
21    /// Parse a format string (`chat_completions` | `responses`).
22    pub fn parse(raw: &str) -> Result<Self> {
23        match raw.trim().to_ascii_lowercase().as_str() {
24            "" | "chat_completions" | "chat" | "chat-completions" => Ok(Self::ChatCompletions),
25            "responses" | "response" => Ok(Self::Responses),
26            other => Err(LlmError::ConfigError(format!(
27                "Invalid {ENV_API_FORMAT}={other:?}; expected chat_completions|responses"
28            ))),
29        }
30    }
31
32    /// Read from env; unset → ChatCompletions. Invalid → error (fail loud).
33    pub fn from_env() -> Result<Self> {
34        match std::env::var(ENV_API_FORMAT) {
35            Ok(v) if !v.trim().is_empty() => Self::parse(&v),
36            _ => Ok(Self::ChatCompletions),
37        }
38    }
39
40    /// Convenience: true when Responses transport is selected.
41    pub fn is_responses(self) -> bool {
42        matches!(self, Self::Responses)
43    }
44}
45
46/// True when env selects Responses (invalid values treated as false for hot paths
47/// that already validated at factory boot — prefer [`ApiFormat::from_env`] at start).
48pub fn responses_api_from_env() -> bool {
49    ApiFormat::from_env()
50        .map(|f| f.is_responses())
51        .unwrap_or(false)
52}
53
54/// Helper used by tests / Acc: treat empty as chat without error.
55#[allow(dead_code)]
56pub fn api_format_or_default() -> ApiFormat {
57    ApiFormat::from_env().unwrap_or_default()
58}
59
60/// Unused placeholder removed — Responses selection uses [`ApiFormat::from_env`].
61#[cfg(test)]
62mod _pin {
63    #[test]
64    fn api_format_default_is_chat() {
65        std::env::remove_var(super::ENV_API_FORMAT);
66        assert_eq!(
67            super::ApiFormat::from_env().unwrap(),
68            super::ApiFormat::ChatCompletions
69        );
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn parse_defaults_and_aliases() {
79        assert_eq!(
80            ApiFormat::parse("chat_completions").unwrap(),
81            ApiFormat::ChatCompletions
82        );
83        assert_eq!(ApiFormat::parse("responses").unwrap(), ApiFormat::Responses);
84        assert!(ApiFormat::parse("foo").is_err());
85    }
86
87    #[test]
88    fn from_env_default_chat() {
89        std::env::remove_var(ENV_API_FORMAT);
90        assert_eq!(ApiFormat::from_env().unwrap(), ApiFormat::ChatCompletions);
91    }
92
93    #[test]
94    fn from_env_invalid_errors() {
95        std::env::set_var(ENV_API_FORMAT, "not-a-format");
96        assert!(ApiFormat::from_env().is_err());
97        std::env::remove_var(ENV_API_FORMAT);
98    }
99}