use crate::chat_completion;
pub const MUSE_SPARK_1_2: &str = "muse-spark-1.2";
pub const MUSE_SPARK_1_2_CONTRIBUTOR: &str = "muse-spark-1.2-contributor";
pub(crate) const PROVIDER_NAME: &str = "meta";
pub(crate) const POLICY: chat_completion::ChatCompletionProviderPolicy =
chat_completion::ChatCompletionProviderPolicy {
display_name: "Meta Model API",
structured_output: chat_completion::StructuredOutputMode::JsonSchema,
telemetry_name: PROVIDER_NAME,
unsupported_schema_reason: "Muse structured output requires an explicit object root schema",
};
pub struct MuseConfig {
pub api_key: String,
pub base_url: String,
pub model: String,
}
#[cfg(test)]
mod tests {
use serde_json::{Value, json};
use wiremock::matchers::{bearer_token, body_json, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use super::*;
use crate::{model, tool};
fn person_schema_value() -> Value {
json!({
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"],
"additionalProperties": false
})
}
fn person_schema() -> crate::OutputSchema {
crate::OutputSchema::new(person_schema_value()).expect("schema should be valid")
}
fn request(prompt: &str) -> model::ModelRequest {
model::ModelRequest::new(prompt, person_schema())
}
fn read_request(prompt: &str) -> model::ModelRequest {
request(prompt).with_tool(tool::ToolDefinition::read())
}
fn read_tool_wire() -> Value {
let definition = tool::ToolDefinition::read();
json!({
"type": "function",
"function": {
"description": definition.description(),
"name": definition.name(),
"parameters": definition.parameters()
}
})
}
fn muse(server: &MockServer) -> model::ModelClient {
model::ModelClient::muse(MuseConfig {
api_key: "test-key".to_string(),
base_url: format!("{}/", server.uri()),
model: "muse-spark-1.2".to_string(),
})
.expect("fixture configuration should be valid")
}
fn response_format() -> Value {
json!({
"type": "json_schema",
"json_schema": {
"name": "ag_harness_output",
"schema": person_schema_value()
}
})
}
#[test]
fn exposes_standard_and_contributor_model_identifiers() {
let models = [MUSE_SPARK_1_2, MUSE_SPARK_1_2_CONTRIBUTOR];
assert_eq!(models, ["muse-spark-1.2", "muse-spark-1.2-contributor"]);
}
#[test]
fn metadata_exposes_provider_and_model() {
let model = model::ModelClient::muse(MuseConfig {
api_key: "test-key".to_string(),
base_url: "https://api.meta.ai/v1".to_string(),
model: MUSE_SPARK_1_2_CONTRIBUTOR.to_string(),
})
.expect("fixture configuration should be valid");
let metadata = model.metadata();
assert_eq!(metadata.provider(), "meta");
assert_eq!(metadata.model(), "muse-spark-1.2-contributor");
}
#[test]
fn rejects_empty_model_during_construction() {
let config = MuseConfig {
api_key: "test-key".to_string(),
base_url: "https://api.meta.ai/v1".to_string(),
model: " ".to_string(),
};
let error = model::ModelClient::muse(config)
.err()
.expect("empty model configuration should be rejected");
assert_eq!(error, model::ModelMetadataError::EmptyModel);
}
#[tokio::test]
async fn completes_native_json_schema_request() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.and(bearer_token("test-key"))
.and(body_json(json!({
"messages": [
{"content": "extract the name", "role": "user"}
],
"model": "muse-spark-1.2",
"response_format": response_format()
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"choices": [{
"finish_reason": "stop",
"message": {"content": r#"{"name":"Ada"}"#}
}]
})))
.expect(1)
.mount(&server)
.await;
let model = muse(&server);
let response = model
.complete(request("extract the name"))
.await
.expect("Muse request should succeed");
assert_eq!(response.output(), Some(&json!({ "name": "Ada" })));
}
#[tokio::test]
async fn advertises_and_decodes_read_tool_call() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.and(bearer_token("test-key"))
.and(body_json(json!({
"messages": [
{"content": "inspect the manifest", "role": "user"}
],
"model": "muse-spark-1.2",
"response_format": response_format(),
"tools": [read_tool_wire()]
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"choices": [{
"finish_reason": "tool_calls",
"message": {
"content": null,
"tool_calls": [{
"id": "call_muse_read",
"type": "function",
"function": {
"name": "read",
"arguments": r#"{"path":"Cargo.toml","offset":1,"limit":12}"#
}
}]
}
}]
})))
.expect(1)
.mount(&server)
.await;
let model = muse(&server);
let response = model
.complete(read_request("inspect the manifest"))
.await
.expect("Muse read request should decode");
assert!(response.output().is_none());
let call = response
.call()
.expect("response should contain a tool call");
assert_eq!(call.id(), "call_muse_read");
assert_eq!(call.name(), "read");
assert_eq!(call.arguments().path(), "Cargo.toml");
assert_eq!(call.arguments().offset(), Some(1));
assert_eq!(call.arguments().limit(), Some(12));
}
#[tokio::test]
async fn retains_local_schema_validation() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"choices": [{
"finish_reason": "stop",
"message": {"content": r#"{"name":42}"#}
}]
})))
.mount(&server)
.await;
let model = muse(&server);
let error = model
.complete(request("extract the name"))
.await
.expect_err("schema violation should fail");
assert!(matches!(
error,
model::ModelError::SchemaViolation { path, reason }
if path == "/name" && reason.contains("string")
));
}
#[tokio::test]
async fn rejects_schemas_without_explicit_object_root() {
let server = MockServer::start().await;
let model = muse(&server);
let schema =
crate::OutputSchema::new(json!({ "type": "array" })).expect("schema should be valid");
let error = model
.complete(model::ModelRequest::new("list names", schema))
.await
.expect_err("schema without an explicit object root should fail");
assert!(matches!(
error,
model::ModelError::UnsupportedOutputSchema { reason }
if reason == "Muse structured output requires an explicit object root schema"
));
assert!(
server
.received_requests()
.await
.expect("request recording should be enabled")
.is_empty()
);
}
#[tokio::test]
async fn reports_meta_http_failure_without_exposing_the_key() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"error": {"message": "invalid API key"}
})))
.mount(&server)
.await;
let model = muse(&server);
let error = model
.complete(request("hello"))
.await
.expect_err("HTTP failure should fail");
let message = error.to_string();
assert!(message.contains("Meta Model API returned HTTP 401 Unauthorized"));
assert!(message.contains("invalid API key"));
assert!(!message.contains("test-key"));
}
}