Skip to main content

locode_provider/
request.rs

1//! The API-agnostic request the engine hands to any [`Provider`](crate::Provider).
2
3use locode_protocol::{Message, ToolSpec};
4
5/// One provider-neutral sampling request (ADR-0007).
6///
7/// The loop builds exactly one of these and knows nothing about any wire. Per
8/// ADR-0013 there is **no separate `system` field** — `messages` carries the whole
9/// role-tagged stream (System/Developer included), and each wire hoists leading
10/// System messages into its own top-level slot (e.g. Anthropic's `system`).
11#[derive(Debug, Clone)]
12pub struct ConversationRequest {
13    /// The full role-tagged conversation stream (System/Developer/User/Assistant).
14    pub messages: Vec<Message>,
15    /// The tool specs offered to the model, mapped by the wire to its tool format.
16    pub tools: Vec<ToolSpec>,
17    /// Provider-neutral sampling knobs.
18    pub sampling_args: SamplingArgs,
19    /// A hint for where the wire should place prompt-cache breakpoints.
20    pub cache_hint: CacheHint,
21}
22
23/// Provider-neutral sampling knobs — the **common core** only.
24///
25/// Wire-specific parameters (Anthropic `top_k`/`stop_sequences`/thinking budget,
26/// OpenAI `frequency_penalty`, …) are the concern of each wire's request builder,
27/// not this type, keeping [`ConversationRequest`] API-agnostic (ADR-0007). Grok
28/// Build uses the same layering: a neutral core plus per-wire superset structs.
29#[derive(Debug, Clone, PartialEq)]
30pub struct SamplingArgs {
31    /// The maximum number of tokens to generate.
32    pub max_tokens: u32,
33    /// Sampling temperature; a wire may omit it (e.g. Anthropic requires temp=1
34    /// when thinking is on, so the wire drops it there).
35    pub temperature: Option<f32>,
36    /// Nucleus-sampling probability mass.
37    pub top_p: Option<f32>,
38    /// Neutral reasoning budget; each wire maps it to its own control
39    /// (Anthropic `budget_tokens`, OpenAI reasoning `effort`).
40    pub reasoning_effort: Option<ReasoningEffort>,
41}
42
43impl Default for SamplingArgs {
44    fn default() -> Self {
45        Self {
46            max_tokens: 4096,
47            temperature: None,
48            top_p: None,
49            reasoning_effort: None,
50        }
51    }
52}
53
54/// A neutral reasoning-effort level, mapped per-wire (ADR-0007).
55///
56/// Grok Build proves this shape: one neutral enum with per-backend mappings
57/// (`to_messages_api()` → Anthropic budget, `to_responses_api()` → OpenAI effort).
58/// Tiers fragment per vendor/model generation (OpenAI `none…xhigh`, codex
59/// `minimal…xhigh`, grok stores per-model allowed-effort lists), so the ladder
60/// covers the observed union and `Other` passes vendor-specific strings
61/// through verbatim (ADR-0007 amendment 2026-07-19). Wires surface the API's
62/// own error on unsupported tiers — never silently clamp (silent clamping
63/// would corrupt eval comparisons).
64#[derive(Debug, Clone, PartialEq, Eq)]
65#[non_exhaustive]
66pub enum ReasoningEffort {
67    /// Explicitly send the wire's "none" (distinct from omitting the param —
68    /// `SamplingArgs.reasoning_effort: None` omits it entirely).
69    None,
70    /// Minimal reasoning.
71    Minimal,
72    /// Low reasoning effort.
73    Low,
74    /// Medium reasoning effort.
75    Medium,
76    /// High reasoning effort.
77    High,
78    /// Extra-high reasoning effort.
79    XHigh,
80    /// A vendor/model-specific tier, passed through verbatim by wires that
81    /// take effort strings; wires with fixed mappings soft-reject it.
82    Other(String),
83}
84
85/// Where the wire should place prompt-cache breakpoints (ADR-0007).
86///
87/// A reserved seam: the actual `cache_control` placement (Anthropic: one marker on
88/// the last message + ≤4 on system blocks) lives in the wire (Task 12).
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
90pub enum CacheHint {
91    /// No prompt caching.
92    Off,
93    /// Apply the wire's default breakpoint policy.
94    #[default]
95    Standard,
96}