use std::ffi::{c_char, c_int, c_void, CStr, CString};
use std::ptr;
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
use crate::InferenceError;
const AVAILABILITY_CACHE_TTL: Duration = Duration::from_secs(5);
#[cfg(car_fm_swift_built)]
extern "C" {
fn car_fm_is_available() -> c_int;
fn car_fm_context_size() -> c_int;
fn car_fm_supports_parallel_tool_calls() -> c_int;
fn car_fm_supports_vision() -> c_int;
fn car_fm_pcc_available() -> c_int;
fn car_fm_pcc_context_size() -> c_int;
fn car_fm_pcc_generate(
prompt: *const c_char,
instructions: *const c_char,
reasoning_level: *const c_char,
max_tokens: i32,
temperature: f64,
out_text: *mut *mut c_char,
out_err: *mut *mut c_char,
) -> c_int;
fn car_fm_generate_with_images(
prompt: *const c_char,
instructions: *const c_char,
images_json: *const c_char,
max_tokens: i32,
temperature: f64,
out_text: *mut *mut c_char,
out_err: *mut *mut c_char,
) -> c_int;
fn car_fm_count_tokens(
instructions: *const c_char,
prompt: *const c_char,
completion: *const c_char,
out_prompt_tokens: *mut i32,
out_completion_tokens: *mut i32,
) -> c_int;
fn car_fm_free_string(ptr: *mut c_char);
fn car_fm_generate(
prompt: *const c_char,
instructions: *const c_char,
max_tokens: i32,
temperature: f64,
out_text: *mut *mut c_char,
out_err: *mut *mut c_char,
) -> c_int;
fn car_fm_generate_stream(
prompt: *const c_char,
instructions: *const c_char,
max_tokens: i32,
temperature: f64,
callback: extern "C" fn(token: *const c_char, state: *mut c_void) -> c_int,
state: *mut c_void,
out_err: *mut *mut c_char,
) -> c_int;
fn car_fm_generate_with_tools(
prompt: *const c_char,
instructions: *const c_char,
tools_json: *const c_char,
tool_choice: *const c_char,
max_tokens: i32,
temperature: f64,
out_text: *mut *mut c_char,
out_tool_calls_json: *mut *mut c_char,
out_err: *mut *mut c_char,
) -> c_int;
fn car_fm_generate_structured(
prompt: *const c_char,
instructions: *const c_char,
schema_json: *const c_char,
max_tokens: i32,
temperature: f64,
out_json: *mut *mut c_char,
out_err: *mut *mut c_char,
) -> c_int;
}
#[cfg(not(car_fm_swift_built))]
mod swift_stubs {
use super::{c_char, c_int, c_void};
pub(super) unsafe fn car_fm_is_available() -> c_int {
0
}
pub(super) unsafe fn car_fm_context_size() -> c_int {
0
}
pub(super) unsafe fn car_fm_supports_parallel_tool_calls() -> c_int {
0
}
pub(super) unsafe fn car_fm_supports_vision() -> c_int {
0
}
pub(super) unsafe fn car_fm_pcc_available() -> c_int {
0
}
pub(super) unsafe fn car_fm_pcc_context_size() -> c_int {
0
}
pub(super) unsafe fn car_fm_pcc_generate(
_prompt: *const c_char,
_instructions: *const c_char,
_reasoning_level: *const c_char,
_max_tokens: i32,
_temperature: f64,
_out_text: *mut *mut c_char,
_out_err: *mut *mut c_char,
) -> c_int {
unreachable!("car_fm_pcc_generate called without the Swift bridge")
}
pub(super) unsafe fn car_fm_generate_with_images(
_prompt: *const c_char,
_instructions: *const c_char,
_images_json: *const c_char,
_max_tokens: i32,
_temperature: f64,
_out_text: *mut *mut c_char,
_out_err: *mut *mut c_char,
) -> c_int {
unreachable!("car_fm_generate_with_images called without the Swift bridge")
}
pub(super) unsafe fn car_fm_count_tokens(
_instructions: *const c_char,
_prompt: *const c_char,
_completion: *const c_char,
_out_prompt_tokens: *mut i32,
_out_completion_tokens: *mut i32,
) -> c_int {
1
}
pub(super) unsafe fn car_fm_free_string(_ptr: *mut c_char) {}
pub(super) unsafe fn car_fm_generate(
_prompt: *const c_char,
_instructions: *const c_char,
_max_tokens: i32,
_temperature: f64,
_out_text: *mut *mut c_char,
_out_err: *mut *mut c_char,
) -> c_int {
unreachable!("car_fm_generate called without the Swift bridge")
}
pub(super) unsafe fn car_fm_generate_stream(
_prompt: *const c_char,
_instructions: *const c_char,
_max_tokens: i32,
_temperature: f64,
_callback: extern "C" fn(token: *const c_char, state: *mut c_void) -> c_int,
_state: *mut c_void,
_out_err: *mut *mut c_char,
) -> c_int {
unreachable!("car_fm_generate_stream called without the Swift bridge")
}
pub(super) unsafe fn car_fm_generate_with_tools(
_prompt: *const c_char,
_instructions: *const c_char,
_tools_json: *const c_char,
_tool_choice: *const c_char,
_max_tokens: i32,
_temperature: f64,
_out_text: *mut *mut c_char,
_out_tool_calls_json: *mut *mut c_char,
_out_err: *mut *mut c_char,
) -> c_int {
unreachable!("car_fm_generate_with_tools called without the Swift bridge")
}
pub(super) unsafe fn car_fm_generate_structured(
_prompt: *const c_char,
_instructions: *const c_char,
_schema_json: *const c_char,
_max_tokens: i32,
_temperature: f64,
_out_json: *mut *mut c_char,
_out_err: *mut *mut c_char,
) -> c_int {
unreachable!("car_fm_generate_structured called without the Swift bridge")
}
}
#[cfg(not(car_fm_swift_built))]
use swift_stubs::{
car_fm_context_size, car_fm_count_tokens, car_fm_free_string, car_fm_generate,
car_fm_generate_stream, car_fm_generate_structured, car_fm_generate_with_images,
car_fm_generate_with_tools, car_fm_is_available, car_fm_pcc_available, car_fm_pcc_context_size,
car_fm_pcc_generate, car_fm_supports_parallel_tool_calls, car_fm_supports_vision,
};
pub fn is_available() -> bool {
static CACHE: Mutex<Option<(Instant, bool)>> = Mutex::new(None);
let now = Instant::now();
let mut guard = match CACHE.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
if let Some((stamped, value)) = *guard {
if now.duration_since(stamped) < AVAILABILITY_CACHE_TTL {
return value;
}
}
let value = unsafe { car_fm_is_available() != 0 };
*guard = Some((Instant::now(), value));
value
}
pub fn context_size() -> Option<u32> {
static CACHE: OnceLock<Option<u32>> = OnceLock::new();
*CACHE.get_or_init(|| {
let raw = unsafe { car_fm_context_size() };
u32::try_from(raw).ok().filter(|&v| v > 0)
})
}
pub fn supports_parallel_tool_calls() -> bool {
static CACHE: OnceLock<bool> = OnceLock::new();
*CACHE.get_or_init(|| unsafe { car_fm_supports_parallel_tool_calls() != 0 })
}
pub fn count_tokens(
instructions: Option<&str>,
prompt: &str,
completion: &str,
) -> Option<(u64, u64)> {
let instructions_c = CString::new(instructions.unwrap_or("")).ok()?;
let prompt_c = CString::new(prompt).ok()?;
let completion_c = CString::new(completion).ok()?;
let mut prompt_tokens: i32 = -1;
let mut completion_tokens: i32 = -1;
let rc = unsafe {
car_fm_count_tokens(
instructions_c.as_ptr(),
prompt_c.as_ptr(),
completion_c.as_ptr(),
&mut prompt_tokens,
&mut completion_tokens,
)
};
if rc != 0 || prompt_tokens < 0 || completion_tokens < 0 {
return None;
}
Some((prompt_tokens as u64, completion_tokens as u64))
}
pub fn supports_vision() -> bool {
static CACHE: OnceLock<bool> = OnceLock::new();
*CACHE.get_or_init(|| unsafe { car_fm_supports_vision() != 0 })
}
pub fn generate_with_images(
prompt: &str,
instructions: Option<&str>,
images: &[String],
max_tokens: u32,
temperature: f32,
) -> Result<String, InferenceError> {
if !is_available() {
return Err(unavailable_error());
}
if !supports_vision() {
return Err(InferenceError::UnsupportedMode {
mode: "multimodal-content",
backend: "foundation-models",
reason: "this device's system model does not accept image input \
(requires macOS 27+); route image content to a VL model",
});
}
let images_json = serde_json::to_string(images).map_err(|e| {
InferenceError::InferenceFailed(format!("image list failed to serialize: {e}"))
})?;
let prompt_c = CString::new(prompt)
.map_err(|e| InferenceError::InferenceFailed(format!("prompt has interior NUL: {e}")))?;
let instructions_c = CString::new(instructions.unwrap_or("")).map_err(|e| {
InferenceError::InferenceFailed(format!("instructions have interior NUL: {e}"))
})?;
let images_c = CString::new(images_json).map_err(|e| {
InferenceError::InferenceFailed(format!("image JSON has interior NUL: {e}"))
})?;
let mut out_text: *mut c_char = ptr::null_mut();
let mut out_err: *mut c_char = ptr::null_mut();
let rc = unsafe {
car_fm_generate_with_images(
prompt_c.as_ptr(),
instructions_c.as_ptr(),
images_c.as_ptr(),
max_tokens as i32,
temperature as f64,
&mut out_text,
&mut out_err,
)
};
if rc != 0 {
let message = consume_swift_string(out_err);
if rc == 4 {
return Err(InferenceError::UnsupportedMode {
mode: "multimodal-content",
backend: "foundation-models",
reason: "this device's system model does not accept image input",
});
}
return Err(map_shim_error(message));
}
Ok(consume_swift_string(out_text))
}
fn map_shim_error(raw: String) -> InferenceError {
let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&raw) else {
return InferenceError::InferenceFailed(raw);
};
let Some(kind) = parsed.get("car_fm_error").and_then(|k| k.as_str()) else {
return InferenceError::InferenceFailed(raw);
};
let message = parsed
.get("message")
.and_then(|m| m.as_str())
.unwrap_or(&raw)
.to_string();
match kind {
"rateLimited" | "timeout" => InferenceError::Transient {
status: None,
message: format!("FoundationModels {kind}: {message}"),
},
"contextSizeExceeded" => {
let window = parsed.get("context_size").and_then(|v| v.as_i64());
let used = parsed.get("token_count").and_then(|v| v.as_i64());
match (window, used) {
(Some(window), Some(used)) => InferenceError::InferenceFailed(format!(
"FoundationModels context exceeded: {used} tokens against a {window}-token \
window. Instructions, prompt and output all count against it; reduce the \
assembly budget for this model or route to a larger one."
)),
_ => InferenceError::InferenceFailed(format!(
"FoundationModels context exceeded: {message}"
)),
}
}
"guardrailViolation" | "refusal" => InferenceError::InferenceFailed(format!(
"FoundationModels declined the request ({kind}): {message}"
)),
_ => InferenceError::InferenceFailed(format!("FoundationModels {kind}: {message}")),
}
}
pub fn pcc_available() -> bool {
static CACHE: OnceLock<bool> = OnceLock::new();
*CACHE.get_or_init(|| unsafe { car_fm_pcc_available() != 0 })
}
pub fn pcc_context_size() -> Option<u32> {
static CACHE: OnceLock<Option<u32>> = OnceLock::new();
*CACHE.get_or_init(|| {
let raw = unsafe { car_fm_pcc_context_size() };
u32::try_from(raw).ok().filter(|&v| v > 0)
})
}
pub fn pcc_generate(
prompt: &str,
instructions: Option<&str>,
reasoning_level: Option<&str>,
max_tokens: u32,
temperature: f32,
) -> Result<String, InferenceError> {
if !pcc_available() {
return Err(InferenceError::UnsupportedMode {
mode: "private-cloud-compute",
backend: "foundation-models",
reason: "Private Cloud Compute is not usable on this device — it needs \
macOS 27+, an eligible device, and a signed-in Apple Account",
});
}
let prompt_c = CString::new(prompt)
.map_err(|e| InferenceError::InferenceFailed(format!("prompt has interior NUL: {e}")))?;
let instructions_c = CString::new(instructions.unwrap_or(""))
.map_err(|e| InferenceError::InferenceFailed(format!("instructions have NUL: {e}")))?;
let reasoning_c = CString::new(reasoning_level.unwrap_or(""))
.map_err(|e| InferenceError::InferenceFailed(format!("reasoning level has NUL: {e}")))?;
let mut out_text: *mut c_char = ptr::null_mut();
let mut out_err: *mut c_char = ptr::null_mut();
let rc = unsafe {
car_fm_pcc_generate(
prompt_c.as_ptr(),
instructions_c.as_ptr(),
reasoning_c.as_ptr(),
max_tokens.min(i32::MAX as u32) as i32,
temperature as f64,
&mut out_text,
&mut out_err,
)
};
if rc != 0 {
let message = consume_swift_string(out_err);
if rc == 4 {
return Err(InferenceError::UnsupportedMode {
mode: "private-cloud-compute",
backend: "foundation-models",
reason: "Private Cloud Compute is not available on this device",
});
}
return Err(map_shim_error(message));
}
Ok(consume_swift_string(out_text))
}
pub fn generate(
prompt: &str,
instructions: Option<&str>,
max_tokens: u32,
temperature: f32,
) -> Result<String, InferenceError> {
if !is_available() {
return Err(unavailable_error());
}
let prompt_c = CString::new(prompt)
.map_err(|e| InferenceError::InferenceFailed(format!("prompt has interior NUL: {e}")))?;
let instr_c = match instructions {
Some(s) if !s.is_empty() => Some(CString::new(s).map_err(|e| {
InferenceError::InferenceFailed(format!("instructions have interior NUL: {e}"))
})?),
_ => None,
};
let mut out_text: *mut c_char = ptr::null_mut();
let mut out_err: *mut c_char = ptr::null_mut();
let rc = unsafe {
car_fm_generate(
prompt_c.as_ptr(),
instr_c.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
max_tokens.min(i32::MAX as u32) as i32,
temperature as f64,
&mut out_text as *mut *mut c_char,
&mut out_err as *mut *mut c_char,
)
};
if rc != 0 {
return Err(map_shim_error(consume_swift_string(out_err)));
}
Ok(consume_swift_string(out_text))
}
pub fn schema_degradations(schema: &serde_json::Value) -> Vec<String> {
let mut findings = Vec::new();
walk_schema(schema, "$", &mut findings);
findings
}
fn walk_schema(node: &serde_json::Value, path: &str, findings: &mut Vec<String>) {
let Some(obj) = node.as_object() else {
findings.push(format!(
"{path}: schema node is not a JSON object — degraded to permissive string"
));
return;
};
for combinator in ["anyOf", "oneOf"] {
if let Some(variants) = obj.get(combinator).and_then(|v| v.as_array()) {
for (index, variant) in variants.iter().enumerate() {
walk_schema(variant, &format!("{path}.{combinator}[{index}]"), findings);
}
return;
}
}
for combinator in ["allOf", "not", "$ref"] {
if obj.contains_key(combinator) {
findings.push(format!(
"{path}: `{combinator}` is not representable — flattened to permissive string"
));
}
}
let type_str = match obj.get("type") {
None => {
if !obj.contains_key("properties") {
if !["allOf", "not", "$ref"]
.iter()
.any(|c| obj.contains_key(*c))
{
findings.push(format!(
"{path}: typeless node without `properties` — degraded to permissive \
string"
));
}
return;
}
"object"
}
Some(serde_json::Value::String(t)) => t.as_str(),
Some(serde_json::Value::Array(members)) => {
for member in members.iter().filter(|m| m.as_str() != Some("null")) {
if let Some(name) = member.as_str() {
let mut narrowed = obj.clone();
narrowed.insert("type".into(), serde_json::Value::String(name.to_string()));
walk_schema(
&serde_json::Value::Object(narrowed),
&format!("{path}|{name}"),
findings,
);
}
}
return;
}
Some(other) => {
findings.push(format!(
"{path}: non-string `type` ({other}) — degraded to permissive string"
));
return;
}
};
match type_str {
"object" => {
if let Some(props) = obj.get("properties").and_then(|p| p.as_object()) {
for (key, sub) in props {
walk_schema(sub, &format!("{path}.{key}"), findings);
}
}
}
"array" => {
if let Some(items) = obj.get("items") {
walk_schema(items, &format!("{path}[]"), findings);
}
}
"string" => {
if let Some(choices) = obj.get("enum").and_then(|e| e.as_array()) {
if choices.iter().any(|c| !c.is_string()) {
findings.push(format!(
"{path}: `enum` contains non-string members — enum constraint dropped, \
degraded to permissive string"
));
}
}
}
"integer" | "number" | "boolean" => {
if obj.contains_key("enum") {
findings.push(format!(
"{path}: `enum` on `{type_str}` is not representable — values ignored, \
plain `{type_str}` kept"
));
}
}
other => {
findings.push(format!(
"{path}: unrecognized `type` \"{other}\" — degraded to permissive string"
));
}
}
}
fn warn_schema_degradations(what: &str, schema: &serde_json::Value) {
for finding in schema_degradations(schema) {
tracing::warn!(
"FoundationModels constrained decoding: {what}: {finding} — the generated value is \
preserved but this part of the schema contract is not enforced"
);
}
}
pub fn generate_with_tools(
prompt: &str,
instructions: Option<&str>,
tools: &[serde_json::Value],
tool_choice: Option<&str>,
max_tokens: u32,
temperature: f32,
) -> Result<(String, Vec<crate::tasks::generate::ToolCall>), InferenceError> {
if !is_available() {
return Err(unavailable_error());
}
let prompt_c = CString::new(prompt)
.map_err(|e| InferenceError::InferenceFailed(format!("prompt has interior NUL: {e}")))?;
let instr_c = match instructions {
Some(s) if !s.is_empty() => Some(CString::new(s).map_err(|e| {
InferenceError::InferenceFailed(format!("instructions have interior NUL: {e}"))
})?),
_ => None,
};
for tool in tools {
let name = tool
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("<unnamed>");
if let Some(params) = tool.get("parameters") {
warn_schema_degradations(&format!("tool '{name}' parameters"), params);
}
}
let tools_json = serde_json::to_string(tools)
.map_err(|e| InferenceError::InferenceFailed(format!("tools serialization: {e}")))?;
let choice_c = tool_choice
.map(str::trim)
.filter(|c| !c.is_empty())
.and_then(|c| CString::new(c).ok());
let tools_c = CString::new(tools_json)
.map_err(|e| InferenceError::InferenceFailed(format!("tools have interior NUL: {e}")))?;
let mut out_text: *mut c_char = ptr::null_mut();
let mut out_calls: *mut c_char = ptr::null_mut();
let mut out_err: *mut c_char = ptr::null_mut();
let rc = unsafe {
car_fm_generate_with_tools(
prompt_c.as_ptr(),
instr_c.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
tools_c.as_ptr(),
choice_c.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
max_tokens.min(i32::MAX as u32) as i32,
temperature as f64,
&mut out_text as *mut *mut c_char,
&mut out_calls as *mut *mut c_char,
&mut out_err as *mut *mut c_char,
)
};
if rc != 0 {
return Err(map_shim_error(consume_swift_string(out_err)));
}
let text = consume_swift_string(out_text);
let calls_json = consume_swift_string(out_calls);
let tool_calls = parse_bridge_tool_calls(&calls_json)?;
Ok((text, tool_calls))
}
fn parse_bridge_tool_calls(
calls_json: &str,
) -> Result<Vec<crate::tasks::generate::ToolCall>, InferenceError> {
if calls_json.trim().is_empty() {
return Ok(vec![]);
}
#[derive(serde::Deserialize)]
struct BridgeCall {
name: String,
#[serde(default)]
arguments: std::collections::HashMap<String, serde_json::Value>,
}
let calls: Vec<BridgeCall> = serde_json::from_str(calls_json).map_err(|e| {
InferenceError::InferenceFailed(format!(
"FoundationModels bridge returned malformed tool-call JSON: {e}"
))
})?;
Ok(calls
.into_iter()
.map(|c| crate::tasks::generate::ToolCall {
id: None,
name: c.name,
arguments: c.arguments,
})
.collect())
}
pub fn generate_structured(
prompt: &str,
instructions: Option<&str>,
schema: &serde_json::Value,
max_tokens: u32,
temperature: f32,
) -> Result<String, InferenceError> {
if !is_available() {
return Err(unavailable_error());
}
let prompt_c = CString::new(prompt)
.map_err(|e| InferenceError::InferenceFailed(format!("prompt has interior NUL: {e}")))?;
let instr_c = match instructions {
Some(s) if !s.is_empty() => Some(CString::new(s).map_err(|e| {
InferenceError::InferenceFailed(format!("instructions have interior NUL: {e}"))
})?),
_ => None,
};
warn_schema_degradations("response_format JsonSchema", schema);
let schema_json = serde_json::to_string(schema)
.map_err(|e| InferenceError::InferenceFailed(format!("schema serialization: {e}")))?;
let schema_c = CString::new(schema_json)
.map_err(|e| InferenceError::InferenceFailed(format!("schema has interior NUL: {e}")))?;
let mut out_json: *mut c_char = ptr::null_mut();
let mut out_err: *mut c_char = ptr::null_mut();
let rc = unsafe {
car_fm_generate_structured(
prompt_c.as_ptr(),
instr_c.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
schema_c.as_ptr(),
max_tokens.min(i32::MAX as u32) as i32,
temperature as f64,
&mut out_json as *mut *mut c_char,
&mut out_err as *mut *mut c_char,
)
};
if rc != 0 {
return Err(map_shim_error(consume_swift_string(out_err)));
}
Ok(consume_swift_string(out_json))
}
pub struct StreamCallback<'a> {
on_delta: Box<dyn FnMut(&str) -> bool + Send + 'a>,
}
impl<'a> StreamCallback<'a> {
pub fn new<F>(on_delta: F) -> Self
where
F: FnMut(&str) -> bool + Send + 'a,
{
Self {
on_delta: Box::new(on_delta),
}
}
}
extern "C" fn stream_trampoline(token: *const c_char, state: *mut c_void) -> c_int {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if state.is_null() {
return 1;
}
let cb = unsafe { &mut *(state as *mut StreamCallback) };
let s = if token.is_null() {
""
} else {
match unsafe { CStr::from_ptr(token) }.to_str() {
Ok(s) => s,
Err(_) => return 1,
}
};
if (cb.on_delta)(s) {
0 } else {
1 }
}));
result.unwrap_or(1)
}
pub fn stream(
prompt: &str,
instructions: Option<&str>,
max_tokens: u32,
temperature: f32,
mut callback: StreamCallback<'_>,
) -> Result<(), InferenceError> {
if !is_available() {
return Err(unavailable_error());
}
let prompt_c = CString::new(prompt)
.map_err(|e| InferenceError::InferenceFailed(format!("prompt has interior NUL: {e}")))?;
let instr_c = match instructions {
Some(s) if !s.is_empty() => Some(CString::new(s).map_err(|e| {
InferenceError::InferenceFailed(format!("instructions have interior NUL: {e}"))
})?),
_ => None,
};
let mut out_err: *mut c_char = ptr::null_mut();
let state: *mut c_void = &mut callback as *mut StreamCallback as *mut c_void;
let rc = unsafe {
car_fm_generate_stream(
prompt_c.as_ptr(),
instr_c.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
max_tokens.min(i32::MAX as u32) as i32,
temperature as f64,
stream_trampoline,
state,
&mut out_err as *mut *mut c_char,
)
};
if rc != 0 {
return Err(map_shim_error(consume_swift_string(out_err)));
}
Ok(())
}
fn consume_swift_string(ptr: *mut c_char) -> String {
if ptr.is_null() {
return String::new();
}
let s = unsafe { CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned();
unsafe { car_fm_free_string(ptr) };
s
}
fn unavailable_error() -> InferenceError {
InferenceError::UnsupportedMode {
mode: "apple-foundation-models",
backend: "foundation-models",
reason: "FoundationModels framework reports unavailable on this host. Requires macOS 26+ \
on Apple Silicon with Apple Intelligence enabled. Falling through to the next \
router candidate.",
}
}
#[cfg(test)]
mod tests {
use super::{
count_tokens, map_shim_error, parse_bridge_tool_calls, pcc_available, pcc_context_size,
pcc_generate, schema_degradations,
};
use crate::InferenceError;
#[test]
fn parses_bridge_tool_call_wire_shape() {
let calls = parse_bridge_tool_calls(
r#"[{"name":"get_weather","arguments":{"city":"Austin","days":3}}]"#,
)
.unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "get_weather");
assert_eq!(calls[0].id, None);
assert_eq!(
calls[0].arguments.get("city"),
Some(&serde_json::json!("Austin"))
);
assert_eq!(calls[0].arguments.get("days"), Some(&serde_json::json!(3)));
}
#[test]
fn empty_or_missing_calls_parse_to_empty() {
assert!(parse_bridge_tool_calls("").unwrap().is_empty());
assert!(parse_bridge_tool_calls("[]").unwrap().is_empty());
}
#[test]
fn malformed_calls_json_is_an_error_not_a_silent_drop() {
assert!(parse_bridge_tool_calls("{not json").is_err());
}
#[test]
fn clean_schema_has_no_degradations() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"city": {"type": "string", "enum": ["Austin", "Boston"]},
"days": {"type": "integer"},
"tags": {"type": "array", "items": {"type": "string"}},
"nested": {"properties": {"ok": {"type": "boolean"}}}
},
"required": ["city"]
});
assert!(schema_degradations(&schema).is_empty());
}
#[test]
fn nullable_union_is_converted_not_degraded() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"amount": {"type": ["number", "null"]}
}
});
assert!(
schema_degradations(&schema).is_empty(),
"a nullable union is representable: {:?}",
schema_degradations(&schema)
);
}
#[test]
fn multi_member_union_walks_each_member() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"value": {"type": ["string", "null"], "enum": ["a", 1]}
}
});
let all = schema_degradations(&schema).join("\n");
assert!(
all.contains("$.value|string") && all.contains("non-string members"),
"a lossy member must still be reported: {all}"
);
}
#[test]
fn anyof_and_oneof_are_converted_but_allof_ref_and_typeless_are_not() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"choice": {"oneOf": [{"type": "string"}, {"type": "integer"}]},
"either": {"anyOf": [{"type": "string"}, {"type": "boolean"}]},
"linked": {"$ref": "#/definitions/thing"},
"mystery": {"description": "no type at all"}
}
});
let findings = schema_degradations(&schema);
let all = findings.join("\n");
assert!(
!all.contains("$.choice") && !all.contains("$.either"),
"anyOf/oneOf are converted natively: {all}"
);
assert!(all.contains("$.linked") && all.contains("$ref"), "{all}");
assert!(
all.contains("$.mystery") && all.contains("typeless"),
"{all}"
);
}
#[test]
fn a_lossy_branch_inside_a_union_is_still_reported() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"choice": {"oneOf": [{"type": "string"}, {"$ref": "#/definitions/thing"}]}
}
});
let all = schema_degradations(&schema).join("\n");
assert!(
all.contains("$.choice.oneOf[1]") && all.contains("$ref"),
"a lossy branch must still surface: {all}"
);
}
#[test]
fn array_items_are_walked() {
let clean = serde_json::json!({
"type": "array",
"items": {"anyOf": [{"type": "string"}]}
});
assert!(schema_degradations(&clean).is_empty());
let lossy = serde_json::json!({
"type": "array",
"items": {"$ref": "#/definitions/thing"}
});
let findings = schema_degradations(&lossy);
assert_eq!(findings.len(), 1);
assert!(findings[0].contains("$[]") && findings[0].contains("$ref"));
}
#[test]
fn numeric_enum_and_unrecognized_type_are_flagged() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"level": {"type": "integer", "enum": [1, 2, 3]},
"weird": {"type": "null"},
"mixed": {"type": "string", "enum": ["a", 1]}
}
});
let findings = schema_degradations(&schema);
let all = findings.join("\n");
assert!(
all.contains("$.level") && all.contains("values ignored"),
"{all}"
);
assert!(
all.contains("$.weird") && all.contains("unrecognized"),
"{all}"
);
assert!(
all.contains("$.mixed") && all.contains("non-string members"),
"{all}"
);
assert_eq!(findings.len(), 3);
}
#[test]
fn pcc_refuses_to_generate_exactly_when_it_reports_unavailable() {
if pcc_available() {
return;
}
let err = pcc_generate("hi", None, None, 8, 0.0)
.expect_err("an unavailable PCC must not generate");
assert!(
matches!(err, InferenceError::UnsupportedMode { mode, .. } if mode
== "private-cloud-compute"),
"refusal must be a routing signal so the router falls through, got {err:?}"
);
assert!(
err.to_string().contains("Apple Account"),
"refusal must name the actual cause: {err}"
);
}
#[test]
fn pcc_context_size_is_independent_of_usability() {
if let Some(window) = pcc_context_size() {
assert!(
window >= 4096,
"PCC's window should exceed the on-device model's, got {window}"
);
}
}
#[test]
fn transient_shim_errors_map_to_transient() {
for kind in ["rateLimited", "timeout"] {
let raw = format!("{{\"car_fm_error\":\"{kind}\",\"message\":\"slow down\"}}");
match map_shim_error(raw) {
InferenceError::Transient { status, message } => {
assert!(status.is_none(), "on-device failures have no HTTP status");
assert!(
message.contains(kind),
"message should name the kind: {message}"
);
}
other => panic!("{kind} must be Transient, got {other:?}"),
}
}
}
#[test]
fn content_policy_outcomes_are_not_transient() {
for kind in ["guardrailViolation", "refusal"] {
let raw = format!("{{\"car_fm_error\":\"{kind}\",\"message\":\"no\"}}");
assert!(
matches!(map_shim_error(raw), InferenceError::InferenceFailed(_)),
"{kind} must not be retryable"
);
}
}
#[test]
fn context_overflow_names_the_window_and_the_overflow() {
let raw = "{\"car_fm_error\":\"contextSizeExceeded\",\"message\":\"too big\",\
\"context_size\":4096,\"token_count\":5200}"
.to_string();
let InferenceError::InferenceFailed(message) = map_shim_error(raw) else {
panic!("context overflow is not retryable");
};
assert!(message.contains("4096"), "must name the window: {message}");
assert!(
message.contains("5200"),
"must name the overflow: {message}"
);
}
#[test]
fn unstructured_errors_pass_through_unchanged() {
let raw = "some opaque framework failure".to_string();
match map_shim_error(raw.clone()) {
InferenceError::InferenceFailed(message) => assert_eq!(message, raw),
other => panic!("plain strings must stay InferenceFailed, got {other:?}"),
}
}
#[test]
fn empty_completion_counts_as_zero_not_unknown() {
let Some((prompt_tokens, completion_tokens)) = count_tokens(None, "hi", "") else {
return;
};
assert_eq!(
completion_tokens, 0,
"an empty completion is zero tokens, not an unavailable count"
);
assert!(
prompt_tokens > 0,
"a non-empty prompt must count above zero, got {prompt_tokens}"
);
}
#[test]
fn instructions_count_toward_the_input() {
let Some((bare, _)) = count_tokens(None, "hi", "") else {
return;
};
let Some((with_instructions, _)) =
count_tokens(Some("You are a concise and careful assistant."), "hi", "")
else {
return;
};
assert!(
with_instructions > bare,
"instructions must add to the input count: {with_instructions} vs {bare}"
);
}
}