Skip to main content

llm_api/
lib.rs

1//! Canonical model invocation and continuation contract. No Agent or transport implementation.
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4mod continuation;
5mod policy;
6pub use continuation::Continuation;
7pub use policy::{
8    controls, input_modality_for_kind, input_modality_for_mime, normalize_capability_input,
9    normalize_constraint_input, normalize_input_token, payload_input_modalities, resolve,
10    BackendCapability, EffectiveGeneration, GenerationControls, GenerationParameters,
11    GenerationSupport, ModelCapabilities, DEFAULT_MAX_OUTPUT_TOKENS, INPUT_AUDIO, INPUT_FILE,
12    INPUT_IMAGE, INPUT_VIDEO, REASONING_EFFORT_LADDER,
13};
14pub use service::Image;
15
16/// Logical model use case resolved by the deployment's LLM implementation.
17#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
18#[serde(transparent)]
19pub struct UseCase(pub String);
20
21/// Logical model mode selected by the product, independent of provider and model identifiers.
22#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct ModelMode(pub String);
25
26/// Caller-declared requirements. Listed input modalities and true flags require confirmed
27/// support; an empty `input` list and false flags impose no requirement.
28/// Adapters must not infer or override these declarations from message content.
29#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
30#[serde(deny_unknown_fields)]
31pub struct ModelConstraints {
32    /// Closed vocabulary: `image`, `video`, `audio`, `file`. `text` and `vision` are rejected.
33    #[serde(default)]
34    pub input: Vec<String>,
35    /// The model must support tool calls.
36    pub tool_calling: bool,
37    /// The model must support schema-constrained output.
38    pub structured_output: bool,
39}
40
41/// Role of a message in a model conversation.
42#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44#[derive(Default)]
45pub enum MessageRole {
46    /// Runtime-supplied instruction.
47    System,
48    /// User-supplied input.
49    #[default]
50    User,
51    /// Model output.
52    Assistant,
53    /// Tool execution output.
54    Tool,
55}
56
57/// One typed part of a model message.
58#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
59#[serde(tag = "type", rename_all = "snake_case")]
60#[serde(deny_unknown_fields)]
61pub enum ContentPart {
62    /// Plain text content.
63    Text {
64        /// Text value.
65        text: String,
66    },
67    /// Scope-owned content-addressed artifact managed by the application artifact service.
68    Artifact {
69        /// Canonical `meow-artifact://` URI.
70        uri: String,
71        /// MIME type copied from the validated artifact metadata.
72        mime_type: String,
73    },
74    /// An image already materialized by the caller.
75    Image { image: Image },
76    /// A model-requested tool invocation.
77    ToolCall(ToolCall),
78    /// Result of an earlier tool invocation.
79    ToolResult {
80        /// Provider-neutral call identifier.
81        call_id: String,
82        /// Structured result payload.
83        result: Value,
84        /// Whether the tool execution failed.
85        is_error: bool,
86    },
87}
88
89/// Provider-neutral conversation message.
90#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
91#[serde(deny_unknown_fields)]
92pub struct Message {
93    /// Speaker role.
94    pub role: MessageRole,
95    /// Ordered message content.
96    pub content: Vec<ContentPart>,
97    /// Private model continuation; never part of a user transcript.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub continuation: Option<Continuation>,
100}
101
102/// A callable function exposed to the model. Execution policy belongs to the caller.
103#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
104pub struct ToolDefinition {
105    pub name: String,
106    pub description: String,
107    pub input_schema: Value,
108}
109/// Tool call returned by an LLM.
110#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
111#[serde(deny_unknown_fields)]
112pub struct ToolCall {
113    /// Provider-neutral identifier used to correlate the result.
114    pub id: String,
115    /// Requested tool name.
116    pub name: String,
117    /// Structured arguments.
118    pub arguments: Value,
119}
120
121/// One logical LLM invocation.
122#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
123pub struct CompletionRequest {
124    /// Logical routing key resolved by the LLM adapter.
125    pub use_case: UseCase,
126    /// Logical quality/cost mode resolved by the LLM adapter adapter.
127    pub model_mode: ModelMode,
128    /// Conversation input.
129    pub messages: Vec<Message>,
130    /// Tools available to the model.
131    pub tools: Vec<ToolDefinition>,
132    /// Required model capabilities.
133    pub constraints: ModelConstraints,
134    /// Optional maximum number of output tokens.
135    pub max_output_tokens: Option<u32>,
136    /// Whether the caller requested sanitized diagnostic metadata.
137    pub diagnostics: bool,
138}
139
140/// Provider-neutral limits for one frozen logical model route.
141///
142/// `profile_key` is opaque to Runtime. Implementations must change it whenever the concrete
143/// model, tokenizer, or another input-serialization detail changes.
144#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
145pub struct ModelProfile {
146    /// Opaque stable identity of the frozen model and tokenizer route.
147    pub profile_key: String,
148    /// Maximum total context accepted by the concrete model.
149    pub context_window_tokens: u32,
150}
151
152/// Why a model invocation stopped.
153#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
154#[serde(rename_all = "snake_case")]
155#[derive(Default)]
156pub enum FinishReason {
157    /// The model completed normally.
158    #[default]
159    Stop,
160    /// The model requested one or more tools.
161    ToolCalls,
162    /// The configured output limit was reached.
163    Length,
164    /// The LLM adapter cannot map the provider result to a more specific reason.
165    Other,
166}
167
168/// Normalized token accounting reported by an implementation.
169#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
170pub struct TokenUsage {
171    /// Input token count.
172    pub input_tokens: u64,
173    /// Output token count.
174    pub output_tokens: u64,
175    /// Input tokens served from a provider cache when reported.
176    pub cached_input_tokens: Option<u64>,
177    /// Reasoning output tokens when reported separately.
178    pub reasoning_output_tokens: Option<u64>,
179    /// Provider-normalized billable credits consumed by this request.
180    pub credits: Option<u64>,
181}
182
183/// Provider-neutral completion returned to Runtime.
184#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
185pub struct Completion {
186    /// Model response message.
187    pub message: Message,
188    /// Normalized finish reason.
189    pub finish_reason: FinishReason,
190    /// Normalized token accounting.
191    pub usage: Option<TokenUsage>,
192    /// Optional sanitized diagnostics without credentials or provider internals.
193    pub diagnostics: Option<Value>,
194}
195
196pub mod service;
197
198impl MessageRole {
199    pub const fn as_str(self) -> &'static str {
200        match self {
201            Self::System => "system",
202            Self::User => "user",
203            Self::Assistant => "assistant",
204            Self::Tool => "tool",
205        }
206    }
207}
208
209impl Default for Message {
210    fn default() -> Self {
211        Self::text(MessageRole::User, "")
212    }
213}
214impl Message {
215    pub fn text(role: MessageRole, text: impl Into<String>) -> Self {
216        Self {
217            role,
218            content: vec![ContentPart::Text { text: text.into() }],
219            continuation: None,
220        }
221    }
222    pub fn text_content(&self) -> String {
223        self.content
224            .iter()
225            .filter_map(|part| match part {
226                ContentPart::Text { text } => Some(text.as_str()),
227                _ => None,
228            })
229            .collect::<Vec<_>>()
230            .join("\n")
231    }
232}
233
234impl TokenUsage {
235    pub fn total_tokens(self) -> u64 {
236        self.input_tokens.saturating_add(self.output_tokens)
237    }
238}
239impl Message {
240    pub fn with_images(mut self, images: impl IntoIterator<Item = Image>) -> Self {
241        self.content
242            .extend(images.into_iter().map(|image| ContentPart::Image { image }));
243        self
244    }
245}
246
247/// Offline normative schema for persisted model messages.
248pub const MESSAGE_SCHEMA: &str = include_str!("../schema/message.v1.schema.json");