Implement Venice.ai provider
This commit is contained in:
269
src/agent/provider/venice/tests.rs
Normal file
269
src/agent/provider/venice/tests.rs
Normal file
@@ -0,0 +1,269 @@
|
||||
use mxlink::matrix_sdk::ruma::OwnedMxcUri;
|
||||
use mxlink::matrix_sdk::ruma::events::room::message::{
|
||||
FileMessageEventContent, ImageMessageEventContent,
|
||||
};
|
||||
use mxlink::mime;
|
||||
|
||||
use super::super::ControllerTrait;
|
||||
use crate::agent::AgentPurpose;
|
||||
use crate::conversation::llm::{
|
||||
Author as LLMAuthor, FileDetails, ImageDetails, Message as LLMMessage,
|
||||
MessageContent as LLMMessageContent,
|
||||
};
|
||||
|
||||
use super::config::{Config, VeniceParameters, WebSearchMode};
|
||||
use super::controller::Controller;
|
||||
use super::utils::convert_llm_messages_to_venice;
|
||||
use super::wire::{
|
||||
ContentPart, EditImageRequest, GenerateImageRequest, MessageContent, SpeechRequest,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn config_round_trips_with_venice_parameters() {
|
||||
let yaml = r#"
|
||||
base_url: https://api.venice.ai/api/v1
|
||||
api_key: test-key
|
||||
text_generation:
|
||||
model_id: kimi-k2-5
|
||||
temperature: 0.7
|
||||
max_response_tokens: 1024
|
||||
max_context_tokens: 65536
|
||||
venice_parameters:
|
||||
enable_web_search: "auto"
|
||||
enable_web_citations: true
|
||||
speech_to_text:
|
||||
model_id: nvidia/parakeet-tdt-0.6b-v3
|
||||
"#;
|
||||
|
||||
let config: Config = serde_yaml_ng::from_str(yaml).expect("config should deserialize");
|
||||
|
||||
let tg = config.text_generation.expect("text_generation present");
|
||||
let vp = tg.venice_parameters.expect("venice_parameters present");
|
||||
|
||||
assert!(matches!(vp.enable_web_search, Some(WebSearchMode::Auto)));
|
||||
assert_eq!(vp.enable_web_citations, Some(true));
|
||||
assert_eq!(vp.character_slug, None);
|
||||
|
||||
// The bag must serialize the enum to the exact wire string, and an unset knob must be ABSENT
|
||||
// (not `null`) so the strict `additionalProperties: false` body is honored.
|
||||
let json = serde_json::to_string(&vp).expect("serialize venice_parameters");
|
||||
assert!(
|
||||
json.contains("\"enable_web_search\":\"auto\""),
|
||||
"web search should be the literal \"auto\": {json}"
|
||||
);
|
||||
assert!(
|
||||
!json.contains("character_slug"),
|
||||
"an unset knob must be omitted entirely: {json}"
|
||||
);
|
||||
assert!(!json.contains("null"), "no nulls belong in the body: {json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_image_to_data_uri_and_skips_files() {
|
||||
let messages = vec![
|
||||
LLMMessage {
|
||||
author: LLMAuthor::User,
|
||||
sender_id: None,
|
||||
timestamp: chrono::Utc::now(),
|
||||
content: LLMMessageContent::Text("describe this".to_owned()),
|
||||
},
|
||||
LLMMessage {
|
||||
author: LLMAuthor::User,
|
||||
sender_id: None,
|
||||
timestamp: chrono::Utc::now(),
|
||||
content: LLMMessageContent::Image(ImageDetails::new(
|
||||
ImageMessageEventContent::plain(
|
||||
"pic.png".to_owned(),
|
||||
OwnedMxcUri::from("mxc://example.com/abc"),
|
||||
),
|
||||
mime::IMAGE_PNG,
|
||||
vec![1, 2, 3],
|
||||
)),
|
||||
},
|
||||
LLMMessage {
|
||||
author: LLMAuthor::User,
|
||||
sender_id: None,
|
||||
timestamp: chrono::Utc::now(),
|
||||
content: LLMMessageContent::File(FileDetails::new(
|
||||
FileMessageEventContent::plain(
|
||||
"doc.pdf".to_owned(),
|
||||
OwnedMxcUri::from("mxc://example.com/def"),
|
||||
),
|
||||
mime::APPLICATION_PDF,
|
||||
vec![4, 5, 6],
|
||||
)),
|
||||
},
|
||||
];
|
||||
|
||||
let converted = convert_llm_messages_to_venice(messages);
|
||||
|
||||
// Text and image survive; the file is warn-skipped.
|
||||
assert_eq!(converted.len(), 2);
|
||||
|
||||
match &converted[0].content {
|
||||
MessageContent::Text(text) => assert_eq!(text, "describe this"),
|
||||
other => panic!("expected bare text, got {other:?}"),
|
||||
}
|
||||
|
||||
match &converted[1].content {
|
||||
MessageContent::Parts(parts) => match &parts[0] {
|
||||
ContentPart::ImageUrl { image_url } => assert!(
|
||||
image_url.url.starts_with("data:image/png;base64,"),
|
||||
"image should be inlined as a data URI: {}",
|
||||
image_url.url
|
||||
),
|
||||
},
|
||||
other => panic!("expected image parts, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_purpose_truth_table() {
|
||||
let config: Config = serde_yaml_ng::from_str(
|
||||
r#"
|
||||
base_url: https://api.venice.ai/api/v1
|
||||
api_key: test-key
|
||||
text_generation:
|
||||
model_id: kimi-k2-5
|
||||
speech_to_text:
|
||||
model_id: nvidia/parakeet-tdt-0.6b-v3
|
||||
"#,
|
||||
)
|
||||
.expect("config should deserialize");
|
||||
|
||||
let controller = Controller::new(config);
|
||||
|
||||
assert!(controller.supports_purpose(AgentPurpose::TextGeneration));
|
||||
assert!(controller.supports_purpose(AgentPurpose::SpeechToText));
|
||||
assert!(controller.supports_purpose(AgentPurpose::CatchAll));
|
||||
assert!(!controller.supports_purpose(AgentPurpose::TextToSpeech));
|
||||
assert!(!controller.supports_purpose(AgentPurpose::ImageGeneration));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_purpose_true_when_image_and_tts_blocks_present() {
|
||||
let config: Config = serde_yaml_ng::from_str(
|
||||
r#"
|
||||
base_url: https://api.venice.ai/api/v1
|
||||
api_key: test-key
|
||||
text_to_speech:
|
||||
model_id: tts-kokoro
|
||||
image_generation:
|
||||
model_id: chroma
|
||||
"#,
|
||||
)
|
||||
.expect("config should deserialize");
|
||||
|
||||
let controller = Controller::new(config);
|
||||
|
||||
assert!(controller.supports_purpose(AgentPurpose::TextToSpeech));
|
||||
assert!(controller.supports_purpose(AgentPurpose::ImageGeneration));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn speech_request_serializes_voice_and_omits_unset() {
|
||||
let request = SpeechRequest {
|
||||
model: "tts-kokoro".to_owned(),
|
||||
input: "hello".to_owned(),
|
||||
voice: Some("af_sky".to_owned()),
|
||||
speed: None,
|
||||
response_format: Some("mp3".to_owned()),
|
||||
prompt: None,
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&request).expect("serialize SpeechRequest");
|
||||
|
||||
assert!(
|
||||
json.contains("\"voice\":\"af_sky\""),
|
||||
"voice should be present: {json}"
|
||||
);
|
||||
assert!(
|
||||
!json.contains("temperature"),
|
||||
"an unset knob must be omitted (not null): {json}"
|
||||
);
|
||||
assert!(!json.contains("null"), "no nulls belong in the body: {json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_image_request_pins_flags_and_omits_unset() {
|
||||
let request = GenerateImageRequest {
|
||||
model: "chroma".to_owned(),
|
||||
prompt: "a cat".to_owned(),
|
||||
return_binary: false,
|
||||
variants: 1,
|
||||
negative_prompt: None,
|
||||
cfg_scale: None,
|
||||
steps: None,
|
||||
style_preset: None,
|
||||
seed: None,
|
||||
safe_mode: None,
|
||||
hide_watermark: None,
|
||||
format: None,
|
||||
width: None,
|
||||
height: None,
|
||||
aspect_ratio: None,
|
||||
resolution: None,
|
||||
quality: None,
|
||||
lora_strength: None,
|
||||
embed_exif_metadata: None,
|
||||
enable_web_search: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&request).expect("serialize GenerateImageRequest");
|
||||
|
||||
assert!(json.contains("\"model\":\"chroma\""), "{json}");
|
||||
assert!(
|
||||
json.contains("\"return_binary\":false"),
|
||||
"return_binary must be pinned false: {json}"
|
||||
);
|
||||
assert!(
|
||||
json.contains("\"variants\":1"),
|
||||
"variants must be pinned 1: {json}"
|
||||
);
|
||||
assert!(
|
||||
!json.contains("cfg_scale"),
|
||||
"an unset knob must be omitted: {json}"
|
||||
);
|
||||
assert!(!json.contains("null"), "no nulls belong in the body: {json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_image_request_carries_model_and_base64_image() {
|
||||
let request = EditImageRequest {
|
||||
model: "firered-image-edit".to_owned(),
|
||||
prompt: "make it a sunrise".to_owned(),
|
||||
image: "aGVsbG8=".to_owned(),
|
||||
output_format: None,
|
||||
aspect_ratio: None,
|
||||
resolution: None,
|
||||
safe_mode: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&request).expect("serialize EditImageRequest");
|
||||
|
||||
assert!(
|
||||
json.contains("\"model\":\"firered-image-edit\""),
|
||||
"{json}"
|
||||
);
|
||||
assert!(
|
||||
json.contains("\"image\":\"aGVsbG8=\""),
|
||||
"the base64 image string must be present: {json}"
|
||||
);
|
||||
assert!(
|
||||
!json.contains("output_format"),
|
||||
"an unset knob must be omitted: {json}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_mode_off_deserializes_from_bare_yaml_off() {
|
||||
// `off` is a YAML-1.1 boolean but a plain string under serde_yaml_ng's YAML-1.2 core schema,
|
||||
// so it deserializes straight into the lowercase `WebSearchMode::Off`. This pins that the
|
||||
// sample config and docs can use the bare, unquoted `off` without it parsing as a boolean.
|
||||
let params: VeniceParameters =
|
||||
serde_yaml_ng::from_str("enable_web_search: off").expect("bare `off` should deserialize");
|
||||
|
||||
assert!(matches!(params.enable_web_search, Some(WebSearchMode::Off)));
|
||||
}
|
||||
Reference in New Issue
Block a user