use std::fmt;
use std::io::Read;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use thiserror::Error;
const OPENAI_RESPONSE_MAX_BYTES: usize = 2 * 1024 * 1024;
const OPENAI_RESPONSE_MAX_DEPTH: usize = 64;
const OPENAI_RESPONSE_MAX_NODES: usize = 100_000;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelCapabilities {
pub identifier: String,
pub repository: Option<String>,
pub artifact: Option<String>,
pub artifact_sha256: Option<String>,
pub quantization: Option<String>,
pub chat_template: Option<String>,
pub context_window_tokens: Option<u32>,
pub native_tools: bool,
#[serde(default)]
pub qualification: Option<ModelQualification>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelQualification {
pub profile_name: String,
pub revision: String,
pub expected_artifact: String,
pub runtime: String,
pub runtime_version: String,
pub runtime_commit: String,
pub accelerator: String,
pub architecture: String,
pub mtp_enabled: bool,
pub artifact_validated: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderCapabilities {
pub provider: String,
pub wire_protocol: String,
pub model: ModelCapabilities,
pub streaming: bool,
#[serde(default)]
pub runtime_provenance: Option<RuntimeProvenance>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeProvenance {
pub runtime: String,
pub version: String,
pub commit: String,
pub distribution: String,
pub artifact: String,
#[serde(default)]
pub artifact_sha256: Option<String>,
pub platform: String,
pub backend: String,
#[serde(default)]
pub accelerator: Option<String>,
#[serde(default)]
pub driver: Option<String>,
#[serde(default)]
pub launch_arguments: Vec<String>,
#[serde(default)]
pub context_tokens: Option<u32>,
#[serde(default)]
pub chat_template: Option<String>,
#[serde(default)]
pub mtp_enabled: Option<bool>,
pub qualified_stack: bool,
pub qualification_note: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InferenceRequest {
pub context: String,
#[serde(default)]
pub messages: Vec<Value>,
pub max_output_tokens: u32,
pub temperature: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ToolCall {
#[serde(default)]
pub id: Option<String>,
pub name: String,
pub arguments: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
pub enum ModelAction {
Tool {
#[serde(default)]
tool_call_id: Option<String>,
tool: String,
arguments: Value,
},
ToolBatch {
calls: Vec<ToolCall>,
},
CandidateReady {
summary: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ModelResponse {
pub content: Option<String>,
pub tool_calls: Vec<ToolCall>,
pub usage: Option<Value>,
}
#[derive(Error)]
pub enum InferenceError {
#[error("inference request timed out")]
Timeout,
#[error("inference server is unavailable")]
Unavailable(String),
#[error("inference response exceeded the size limit")]
ResponseTooLarge,
#[error("inference response contained invalid UTF-8")]
InvalidUtf8,
#[error("inference server returned HTTP {status}")]
HttpStatus { status: u16, body: String },
#[error("malformed inference response")]
Malformed(String),
}
impl fmt::Debug for InferenceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = formatter.debug_struct("InferenceError");
match self {
Self::Timeout => debug.field("kind", &"timeout"),
Self::HttpStatus { status, body } => debug
.field("kind", &"http_status")
.field("status", status)
.field("response_bytes", &body.len()),
Self::Unavailable(_) => debug.field("kind", &"transport"),
Self::ResponseTooLarge => debug.field("kind", &"response_too_large"),
Self::InvalidUtf8 => debug.field("kind", &"invalid_utf8"),
Self::Malformed(_) => debug.field("kind", &"malformed_response"),
};
debug.finish()
}
}
impl InferenceError {
#[must_use]
pub fn audit_metadata(&self) -> Value {
match self {
Self::Timeout => json!({"reason_code": "provider_timeout"}),
Self::HttpStatus { status, body } => json!({
"reason_code": "provider_http_status",
"http_status": status,
"response_bytes": body.len(),
}),
Self::Unavailable(_) => json!({"reason_code": "provider_transport"}),
Self::ResponseTooLarge => json!({"reason_code": "provider_response_too_large"}),
Self::InvalidUtf8 => json!({"reason_code": "provider_invalid_utf8"}),
Self::Malformed(_) => json!({"reason_code": "provider_malformed_json"}),
}
}
}
pub trait InferenceProvider {
fn capabilities(&self) -> ProviderCapabilities;
fn complete(&mut self, request: &InferenceRequest) -> Result<ModelResponse, InferenceError>;
}
#[derive(Debug, Clone)]
pub struct OpenAiCompatibleProvider {
endpoint: String,
model: ModelCapabilities,
api_key: Option<String>,
timeout: Duration,
runtime_provenance: Option<RuntimeProvenance>,
tool_schemas: Value,
}
impl OpenAiCompatibleProvider {
#[must_use]
pub fn new(
endpoint: impl Into<String>,
model: impl Into<String>,
api_key: Option<String>,
timeout: Duration,
) -> Self {
Self {
endpoint: endpoint.into().trim_end_matches('/').to_owned(),
model: ModelCapabilities {
identifier: model.into(),
repository: None,
artifact: None,
artifact_sha256: None,
quantization: None,
chat_template: None,
context_window_tokens: None,
native_tools: true,
qualification: None,
},
api_key,
timeout,
runtime_provenance: None,
tool_schemas: tool_schemas(),
}
}
#[must_use]
pub fn with_model_capabilities(mut self, model: ModelCapabilities) -> Self {
self.model = model;
self
}
#[must_use]
pub fn with_runtime_provenance(mut self, provenance: RuntimeProvenance) -> Self {
self.runtime_provenance = Some(provenance);
self
}
#[must_use]
pub fn with_tool_schemas(mut self, tool_schemas: Value) -> Self {
self.tool_schemas = tool_schemas;
self
}
fn request_body(&self, request: &InferenceRequest) -> Value {
let mut messages = vec![
json!({"role": "system", "content": "Follow the supplied policy and use only the declared tools."}),
json!({"role": "user", "content": request.context}),
];
messages.extend(request.messages.clone());
if !request.messages.is_empty() {
messages.push(json!({
"role": "user",
"content": "Continue from the recorded tool interaction using the current objective state above. Choose exactly one next action."
}));
}
json!({
"model": self.model.identifier,
"messages": messages,
"temperature": request.temperature,
"max_tokens": request.max_output_tokens,
"tools": self.tool_schemas,
"tool_choice": "auto",
"stream": false
})
}
}
impl InferenceProvider for OpenAiCompatibleProvider {
fn capabilities(&self) -> ProviderCapabilities {
ProviderCapabilities {
provider: "openai-compatible-http".to_owned(),
wire_protocol: "openai-chat-completions-v1".to_owned(),
model: self.model.clone(),
streaming: false,
runtime_provenance: self.runtime_provenance.clone(),
}
}
fn complete(&mut self, request: &InferenceRequest) -> Result<ModelResponse, InferenceError> {
let url = format!("{}/v1/chat/completions", self.endpoint);
let body = serde_json::to_string(&self.request_body(request))
.map_err(|error| InferenceError::Malformed(error.to_string()))?;
let agent = ureq::AgentBuilder::new().timeout(self.timeout).build();
let mut http_request = agent
.post(&url)
.set("content-type", "application/json")
.set("accept", "application/json");
if let Some(api_key) = &self.api_key {
http_request = http_request.set("authorization", &format!("Bearer {api_key}"));
}
let response = match http_request.send_string(&body) {
Ok(response) => response,
Err(ureq::Error::Status(status, response)) => {
let body = read_bounded(response.into_reader(), 64 * 1024)
.unwrap_or_else(|error| format!("unreadable error body: {error}"));
return Err(InferenceError::HttpStatus { status, body });
}
Err(ureq::Error::Transport(error)) => {
let detail = error.to_string();
if detail.to_ascii_lowercase().contains("timed out")
|| detail.to_ascii_lowercase().contains("timeout")
{
return Err(InferenceError::Timeout);
}
return Err(InferenceError::Unavailable(detail));
}
};
let body = read_bounded_bytes(response.into_reader(), OPENAI_RESPONSE_MAX_BYTES)
.map_err(inference_read_error)?;
parse_openai_response(&body)
}
}
pub fn parse_action(response: &ModelResponse) -> Result<ModelAction, InferenceError> {
if !response.tool_calls.is_empty() {
if response
.tool_calls
.iter()
.any(|call| !call.arguments.is_object())
{
return Err(InferenceError::Malformed(
"tool arguments must be a JSON object".to_owned(),
));
}
if response
.tool_calls
.iter()
.any(|call| call.name == "candidate_ready")
{
if response.tool_calls.len() != 1 {
return Err(InferenceError::Malformed(
"candidate_ready cannot be batched with tool calls".to_owned(),
));
}
let summary = response.tool_calls[0]
.arguments
.get("summary")
.and_then(Value::as_str)
.filter(|summary| !summary.is_empty())
.ok_or_else(|| {
InferenceError::Malformed(
"candidate_ready requires a non-empty summary".to_owned(),
)
})?;
return Ok(ModelAction::CandidateReady {
summary: summary.to_owned(),
});
}
if response.tool_calls.len() > 1 {
return Ok(ModelAction::ToolBatch {
calls: response.tool_calls.clone(),
});
}
let call = &response.tool_calls[0];
return Ok(ModelAction::Tool {
tool_call_id: call.id.clone(),
tool: call.name.clone(),
arguments: call.arguments.clone(),
});
}
let content = response.content.as_deref().ok_or_else(|| {
InferenceError::Malformed("response has no content or tool call".to_owned())
})?;
let action: ModelAction = serde_json::from_str(content).map_err(|error| {
InferenceError::Malformed(format!("fallback action must be strict JSON: {error}"))
})?;
match &action {
ModelAction::Tool { arguments, .. } if !arguments.is_object() => {
return Err(InferenceError::Malformed(
"tool arguments must be a JSON object".to_owned(),
));
}
ModelAction::ToolBatch { calls }
if calls.is_empty() || calls.iter().any(|call| !call.arguments.is_object()) =>
{
return Err(InferenceError::Malformed(
"tool batch must contain calls with JSON-object arguments".to_owned(),
));
}
_ => {}
}
Ok(action)
}
fn parse_openai_response(body: &[u8]) -> Result<ModelResponse, InferenceError> {
std::str::from_utf8(body).map_err(|_| InferenceError::InvalidUtf8)?;
scan_json_bounded(
body,
OPENAI_RESPONSE_MAX_DEPTH,
OPENAI_RESPONSE_MAX_NODES,
OPENAI_RESPONSE_MAX_BYTES,
)?;
let response: OpenAiResponse = serde_json::from_slice(body)
.map_err(|error| InferenceError::Malformed(format!("invalid JSON: {error}")))?;
let message = response
.choices
.first()
.and_then(|choice| choice.message.as_ref())
.ok_or_else(|| InferenceError::Malformed("missing choices[0].message".to_owned()))?;
let mut tool_calls = Vec::new();
for call in message.tool_calls.as_deref().unwrap_or_default() {
let function = call
.function
.as_ref()
.ok_or_else(|| InferenceError::Malformed("tool call missing function".to_owned()))?;
let name = function
.name
.as_deref()
.ok_or_else(|| InferenceError::Malformed("tool call missing name".to_owned()))?;
let arguments = function
.arguments
.as_deref()
.ok_or_else(|| InferenceError::Malformed("tool call missing arguments".to_owned()))?;
scan_json_bounded(
arguments.as_bytes(),
OPENAI_RESPONSE_MAX_DEPTH,
OPENAI_RESPONSE_MAX_NODES,
OPENAI_RESPONSE_MAX_BYTES,
)?;
let arguments = serde_json::from_str(arguments).map_err(|error| {
InferenceError::Malformed(format!("tool arguments are invalid JSON: {error}"))
})?;
tool_calls.push(ToolCall {
id: call.id.clone(),
name: name.to_owned(),
arguments,
});
}
Ok(ModelResponse {
content: message.content.clone(),
tool_calls,
usage: response
.usage
.and_then(|usage| serde_json::to_value(usage).ok()),
})
}
fn inference_read_error(error: std::io::Error) -> InferenceError {
if error.kind() == std::io::ErrorKind::InvalidData {
InferenceError::InvalidUtf8
} else if error.to_string().contains("response exceeded size limit") {
InferenceError::ResponseTooLarge
} else {
InferenceError::Malformed("bounded response read failed".to_owned())
}
}
#[derive(Debug, Deserialize)]
struct OpenAiResponse {
#[serde(default)]
choices: Vec<OpenAiChoice>,
#[serde(default)]
usage: Option<OpenAiUsage>,
}
#[derive(Debug, Serialize, Deserialize)]
struct OpenAiUsage {
#[serde(default, skip_serializing_if = "Option::is_none")]
prompt_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
completion_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
total_tokens: Option<u64>,
}
#[derive(Debug, Deserialize)]
struct OpenAiChoice {
message: Option<OpenAiMessage>,
}
#[derive(Debug, Deserialize)]
struct OpenAiMessage {
content: Option<String>,
#[serde(default)]
tool_calls: Option<Vec<OpenAiToolCall>>,
}
#[derive(Debug, Deserialize)]
struct OpenAiToolCall {
id: Option<String>,
function: Option<OpenAiFunction>,
}
#[derive(Debug, Deserialize)]
struct OpenAiFunction {
name: Option<String>,
arguments: Option<String>,
}
fn read_bounded(mut reader: impl Read, maximum: usize) -> std::io::Result<String> {
let bytes = read_bounded_bytes(&mut reader, maximum)?;
String::from_utf8(bytes)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
}
fn read_bounded_bytes(mut reader: impl Read, maximum: usize) -> std::io::Result<Vec<u8>> {
let limit = u64::try_from(maximum + 1).expect("response bound fits u64");
let mut bytes = Vec::new();
reader.by_ref().take(limit).read_to_end(&mut bytes)?;
if bytes.len() > maximum {
return Err(std::io::Error::other("response exceeded size limit"));
}
Ok(bytes)
}
fn scan_json_bounded(
bytes: &[u8],
max_depth: usize,
max_nodes: usize,
max_string_bytes: usize,
) -> Result<(), InferenceError> {
struct Scanner<'a> {
bytes: &'a [u8],
index: usize,
nodes: usize,
max_depth: usize,
max_nodes: usize,
max_string_bytes: usize,
}
impl Scanner<'_> {
fn error(&self, message: &str) -> InferenceError {
InferenceError::Malformed(format!("{message} at byte {}", self.index))
}
fn ws(&mut self) {
while self
.bytes
.get(self.index)
.is_some_and(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t'))
{
self.index += 1;
}
}
fn consume(&mut self, expected: u8) -> bool {
if self.bytes.get(self.index) == Some(&expected) {
self.index += 1;
true
} else {
false
}
}
fn value(&mut self, depth: usize) -> Result<(), InferenceError> {
if depth > self.max_depth {
return Err(self.error("JSON nesting exceeds outer response bound"));
}
self.nodes = self.nodes.saturating_add(1);
if self.nodes > self.max_nodes {
return Err(self.error("JSON node count exceeds outer response bound"));
}
self.ws();
match self.bytes.get(self.index).copied() {
Some(b'{') => self.object(depth),
Some(b'[') => self.array(depth),
Some(b'"') => self.string(),
Some(b't') => self.literal(b"true"),
Some(b'f') => self.literal(b"false"),
Some(b'n') => self.literal(b"null"),
Some(b'-' | b'0'..=b'9') => self.number(),
_ => Err(self.error("invalid JSON value")),
}
}
fn object(&mut self, depth: usize) -> Result<(), InferenceError> {
self.index += 1;
self.ws();
let mut keys = std::collections::BTreeSet::new();
if self.consume(b'}') {
return Ok(());
}
loop {
self.ws();
let start = self.index;
if !self.bytes.get(self.index).is_some_and(|byte| *byte == b'"') {
return Err(self.error("object key must be a string"));
}
self.string()?;
let key: String = serde_json::from_slice(&self.bytes[start..self.index])
.map_err(|error| self.error(&format!("invalid object key: {error}")))?;
if !keys.insert(key) {
return Err(self.error("duplicate JSON object key"));
}
self.ws();
if !self.consume(b':') {
return Err(self.error("object key is missing ':'"));
}
self.value(depth + 1)?;
self.ws();
if self.consume(b'}') {
return Ok(());
}
if !self.consume(b',') {
return Err(self.error("object member is missing ','"));
}
}
}
fn array(&mut self, depth: usize) -> Result<(), InferenceError> {
self.index += 1;
self.ws();
if self.consume(b']') {
return Ok(());
}
loop {
self.value(depth + 1)?;
self.ws();
if self.consume(b']') {
return Ok(());
}
if !self.consume(b',') {
return Err(self.error("array item is missing ','"));
}
}
}
fn string(&mut self) -> Result<(), InferenceError> {
let start = self.index;
self.index += 1;
let mut escaped = false;
while let Some(byte) = self.bytes.get(self.index).copied() {
self.index += 1;
if escaped {
escaped = false;
continue;
}
if byte == b'\\' {
escaped = true;
continue;
}
if byte == b'"' {
if self.index.saturating_sub(start) > self.max_string_bytes {
return Err(self.error("JSON string exceeds outer response bound"));
}
return Ok(());
}
if byte < 0x20 {
return Err(self.error("JSON string contains a control byte"));
}
}
Err(self.error("unterminated JSON string"))
}
fn literal(&mut self, literal: &[u8]) -> Result<(), InferenceError> {
if self.bytes.get(self.index..self.index + literal.len()) != Some(literal) {
return Err(self.error("invalid JSON literal"));
}
self.index += literal.len();
Ok(())
}
fn number(&mut self) -> Result<(), InferenceError> {
let start = self.index;
while self
.bytes
.get(self.index)
.is_some_and(|byte| matches!(byte, b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9'))
{
self.index += 1;
}
if start == self.index {
return Err(self.error("invalid JSON number"));
}
Ok(())
}
}
let mut scanner = Scanner {
bytes,
index: 0,
nodes: 0,
max_depth,
max_nodes,
max_string_bytes,
};
scanner.ws();
scanner.value(1)?;
scanner.ws();
if scanner.index != bytes.len() {
return Err(scanner.error("trailing bytes after JSON value"));
}
Ok(())
}
#[must_use]
pub fn tool_schemas() -> Value {
json!([
function_tool(
"read_file",
"Read a UTF-8 file within the workspace",
json!({
"type": "object", "properties": {"path": {"type": "string", "description": "Workspace-relative path preferred; every path must remain inside the workspace and .. is rejected"}}, "required": ["path"], "additionalProperties": false
})
),
function_tool(
"search",
"Search workspace text with ripgrep",
json!({
"type": "object", "properties": {"query": {"type": "string"}, "path": {"type": "string", "description": "Optional workspace-confined directory or file; relative path preferred"}}, "required": ["query"], "additionalProperties": false
})
),
function_tool(
"apply_patch",
"Apply one validated unified diff within the workspace. Include --- and +++ file headers and @@ hunks.",
json!({
"type": "object", "properties": {"patch": {"type": "string"}}, "required": ["patch"], "additionalProperties": false
})
),
function_tool(
"shell",
"Run one bounded argv command within the workspace",
json!({
"type": "object", "properties": {"argv": {"type": "array", "items": {"type": "string"}}, "cwd": {"type": "string"}, "timeout_ms": {"type": "integer"}, "env": {"type": "object", "additionalProperties": {"type": "string"}}}, "required": ["argv"], "additionalProperties": false
})
),
function_tool(
"git",
"Inspect objective Git state",
json!({
"type": "object", "properties": {"operation": {"type": "string", "enum": ["status", "diff", "show"]}, "revision": {"type": "string"}}, "required": ["operation"], "additionalProperties": false
})
),
function_tool(
"candidate_ready",
"Submit the exact current workspace for independent FalseGreen verification. This does not mean Accepted.",
json!({
"type": "object", "properties": {"summary": {"type": "string"}}, "required": ["summary"], "additionalProperties": false
})
)
])
}
fn function_tool(name: &str, description: &str, parameters: Value) -> Value {
json!({"type": "function", "function": {"name": name, "description": description, "parameters": parameters}})
}
#[cfg(test)]
mod tests {
use std::io::{Read, Write};
use std::net::TcpListener;
use std::thread;
use std::time::Duration;
use serde_json::json;
use super::{
InferenceError, InferenceProvider, InferenceRequest, ModelAction, ModelResponse,
OPENAI_RESPONSE_MAX_BYTES, OPENAI_RESPONSE_MAX_DEPTH, OPENAI_RESPONSE_MAX_NODES,
OpenAiCompatibleProvider, ToolCall, parse_action, parse_openai_response,
read_bounded_bytes, scan_json_bounded,
};
fn serve_once(body: &'static str, delay: Duration) -> String {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
let address = listener.local_addr().expect("address");
thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("accept");
let mut request = [0_u8; 8192];
let _ = stream.read(&mut request);
thread::sleep(delay);
write!(
stream,
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
)
.expect("write");
});
format!("http://{address}")
}
fn request() -> InferenceRequest {
InferenceRequest {
context: "test".to_owned(),
messages: Vec::new(),
max_output_tokens: 10,
temperature: 0.0,
}
}
#[test]
fn accepts_valid_completion_and_native_tool_call() {
let endpoint = serve_once(
r#"{"choices":[{"message":{"content":null,"tool_calls":[{"function":{"name":"read_file","arguments":"{\"path\":\"README.md\"}"}}]}}]}"#,
Duration::ZERO,
);
let mut provider =
OpenAiCompatibleProvider::new(endpoint, "test-model", None, Duration::from_secs(1));
let response = provider.complete(&request()).expect("completion");
assert_eq!(
parse_action(&response).expect("action"),
ModelAction::Tool {
tool_call_id: None,
tool: "read_file".to_owned(),
arguments: json!({"path": "README.md"})
}
);
}
#[test]
fn accepts_multiple_independent_native_tool_calls() {
let response = ModelResponse {
content: None,
tool_calls: vec![
ToolCall {
id: Some("one".to_owned()),
name: "read_file".to_owned(),
arguments: json!({"path": "src/lib.rs"}),
},
ToolCall {
id: Some("two".to_owned()),
name: "read_file".to_owned(),
arguments: json!({"path": "Cargo.toml"}),
},
],
usage: None,
};
assert!(matches!(
parse_action(&response).expect("batch"),
ModelAction::ToolBatch { calls } if calls.len() == 2
));
}
#[test]
fn maps_native_candidate_signal_without_granting_acceptance() {
let response = ModelResponse {
content: None,
tool_calls: vec![ToolCall {
id: Some("candidate".to_owned()),
name: "candidate_ready".to_owned(),
arguments: json!({"summary": "tests pass"}),
}],
usage: None,
};
assert_eq!(
parse_action(&response).expect("candidate action"),
ModelAction::CandidateReady {
summary: "tests pass".to_owned()
}
);
}
#[test]
fn parses_strict_fallback_completion() {
let response = ModelResponse {
content: Some(r#"{"action":"candidate_ready","summary":"tests pass"}"#.to_owned()),
tool_calls: Vec::new(),
usage: None,
};
assert_eq!(
parse_action(&response).expect("action"),
ModelAction::CandidateReady {
summary: "tests pass".to_owned()
}
);
}
#[test]
fn rejects_malformed_response() {
let endpoint = serve_once("{}", Duration::ZERO);
let mut provider =
OpenAiCompatibleProvider::new(endpoint, "test-model", None, Duration::from_secs(1));
assert!(matches!(
provider.complete(&request()),
Err(InferenceError::Malformed(_))
));
}
#[test]
fn reports_timeout() {
let endpoint = serve_once(
r#"{"choices":[{"message":{"content":"{}"}}]}"#,
Duration::from_millis(200),
);
let mut provider =
OpenAiCompatibleProvider::new(endpoint, "test-model", None, Duration::from_millis(30));
assert!(matches!(
provider.complete(&request()),
Err(InferenceError::Timeout)
));
}
#[test]
fn reports_server_unavailable() {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
let endpoint = format!("http://{}", listener.local_addr().expect("address"));
drop(listener);
let mut provider =
OpenAiCompatibleProvider::new(endpoint, "test-model", None, Duration::from_millis(100));
assert!(matches!(
provider.complete(&request()),
Err(InferenceError::Unavailable(_))
));
}
#[test]
fn outer_response_scanner_bounds_depth_nodes_duplicates_and_utf8() {
let valid = br#"{"choices":[{"message":{"content":"ok"}}]}"#;
assert!(parse_openai_response(valid).is_ok());
let duplicate = br#"{"choices":[],"choices":[]}"#;
assert!(parse_openai_response(duplicate).is_err());
let mut deep = Vec::new();
for _ in 0..5_000 {
deep.extend_from_slice(b"[");
}
deep.extend_from_slice(b"null");
for _ in 0..5_000 {
deep.extend_from_slice(b"]");
}
assert!(parse_openai_response(&deep).is_err());
let mut huge_array = b"[".to_vec();
for index in 0..100_001 {
if index > 0 {
huge_array.push(b',');
}
huge_array.extend_from_slice(b"0");
}
huge_array.push(b']');
assert!(
scan_json_bounded(
&huge_array,
OPENAI_RESPONSE_MAX_DEPTH,
OPENAI_RESPONSE_MAX_NODES,
OPENAI_RESPONSE_MAX_BYTES,
)
.is_err()
);
assert!(
parse_openai_response(b"{\"choices\":[{\"message\":{\"content\":\"\xff\"}}]}").is_err()
);
}
#[test]
fn outer_response_size_limit_is_exact() {
let prefix = br#"{"choices":[{"message":{"content":""#;
let suffix = br#""}}]}"#;
for target in [OPENAI_RESPONSE_MAX_BYTES - 1, OPENAI_RESPONSE_MAX_BYTES] {
let content_len = target - prefix.len() - suffix.len();
let mut body = Vec::with_capacity(target);
body.extend_from_slice(prefix);
body.extend(std::iter::repeat_n(b'a', content_len));
body.extend_from_slice(suffix);
assert_eq!(body.len(), target);
assert!(parse_openai_response(&body).is_ok());
assert_eq!(
read_bounded_bytes(std::io::Cursor::new(body), OPENAI_RESPONSE_MAX_BYTES)
.expect("within bound")
.len(),
target
);
}
let over = vec![b'x'; OPENAI_RESPONSE_MAX_BYTES + 1];
assert!(read_bounded_bytes(std::io::Cursor::new(over), OPENAI_RESPONSE_MAX_BYTES).is_err());
}
}