use tokio_util::sync::CancellationToken;
use super::gateway::GatewayVideo;
use super::{VideoAnswer, VideoConfig, VideoRequest, VideoUnderstanding};
use crate::shared::api::catalogue::{self, CatalogueRequest, CatalogueShape, ModelSlot};
use crate::shared::config::{CloudProvider, MediaResolution, VideoProvider};
pub(crate) const FRESH_VIDEO: &str = "https://www.youtube.com/watch?v=IwZVXmQdX1E";
pub(crate) const A_LINE_OF_IT: &str = "bound for the moon";
const MEASURED_ON: &str = "google/gemini-3.5-flash-lite";
pub(crate) fn key() -> Option<String> {
let key = std::env::var("MINDFORK_OPENROUTER_KEY")
.ok()
.filter(|k| !k.trim().is_empty());
if key.is_none() {
eprintln!("skip: MINDFORK_OPENROUTER_KEY not set");
}
key
}
pub(crate) fn config(key: &str) -> VideoConfig {
let model = std::env::var("MINDFORK_OPENROUTER_VIDEO_MODEL")
.ok()
.filter(|m| !m.trim().is_empty())
.unwrap_or_else(|| MEASURED_ON.to_string());
VideoConfig {
provider: VideoProvider::OpenRouter,
attribution: true,
model,
base_url: CloudProvider::OpenRouter.chat_base_url().to_string(),
api_key: key.to_string(),
media_resolution: MediaResolution::Low,
max_minutes: 30,
}
}
async fn asked(client: &GatewayVideo, url: &str, prompt: &str) -> anyhow::Result<VideoAnswer> {
let request = VideoRequest {
url: url.to_string(),
prompt: prompt.to_string(),
start_secs: None,
end_secs: None,
max_output_tokens: 1200,
};
let started = std::time::Instant::now();
let answer = client.describe(request, &CancellationToken::new()).await;
match &answer {
Ok(answer) => eprintln!(
" answered in {:.1?}, cut: {}: {}",
started.elapsed(),
answer.truncated,
answer.text.replace('\n', " ")
),
Err(err) => eprintln!(" refused in {:.1?}: {err}", started.elapsed()),
}
answer
}
async fn video_list(key: Option<&str>) -> Vec<catalogue::CatalogModel> {
let request = CatalogueRequest {
shape: CatalogueShape::OpenRouterVideo,
base: CloudProvider::OpenRouter.chat_base_url().to_string(),
key: key.map(str::to_string),
attribution: true,
};
let listed = catalogue::fetch(&request).await.expect("the list");
eprintln!(" the gateway sent {} entries", listed.len());
catalogue::for_slot(listed, ModelSlot::Video)
}
#[tokio::test]
#[ignore = "requires MINDFORK_OPENROUTER_KEY"]
async fn the_gateways_video_list_answers_live() {
let Some(key) = key() else { return };
let public = video_list(None).await;
let mine = video_list(Some(&key)).await;
eprintln!(
" offered: {} without a key, {} with one; the first: {:?}, {:?}",
public.len(),
mine.len(),
public.first().map(|m| &m.id),
mine.first().map(|m| &m.id)
);
for (name, list) in [("public", &public), ("the account's", &mine)] {
assert!(list.len() >= 20, "{name}: {}", list.len());
assert!(
list.iter().all(|m| m.facts.video == Some(true)),
"{name}: every entry says it takes video"
);
assert!(
list.iter().all(|m| !m.id.ends_with(":batch")),
"{name}: no batch twin"
);
let gemini = |m: &catalogue::CatalogModel| m.id.contains("google/gemini");
let family = list.iter().filter(|m| gemini(m)).count();
assert!(family >= 5, "{name}: {family} of the family");
assert!(
list.iter().take(family).all(gemini),
"{name}: the family opens the list"
);
assert!(list.iter().any(|m| m.id == MEASURED_ON), "{name}");
}
let apart = public.len().abs_diff(mine.len());
assert!(
apart <= public.len() / 4,
"the two lists are of one size, give or take the account's own settings: \
{} and {}",
public.len(),
mine.len()
);
}
#[tokio::test]
#[ignore = "requires MINDFORK_OPENROUTER_KEY"]
async fn a_video_is_watched_through_the_gateway_live() {
let Some(key) = key() else { return };
let client = GatewayVideo::new(config(&key));
let answer = asked(
&client,
FRESH_VIDEO,
"Transcribe the first three sentences spoken in this video, verbatim.",
)
.await
.expect("the video is watched");
assert!(
answer.text.to_lowercase().contains(A_LINE_OF_IT),
"{}",
answer.text
);
assert!(!answer.truncated);
}
#[tokio::test]
#[ignore = "requires MINDFORK_OPENROUTER_KEY"]
async fn a_link_the_gateway_does_not_read_is_an_error_not_a_description_live() {
let Some(key) = key() else { return };
let client = GatewayVideo::new(config(&key));
let err = asked(
&client,
"https://example.com/",
"Say what this video shows in one sentence.",
)
.await
.expect_err("nothing was watched");
let text = err.to_string();
assert!(text.contains("without reading the video"), "{text}");
}
#[tokio::test]
#[ignore = "requires MINDFORK_OPENROUTER_KEY"]
async fn a_video_that_is_not_there_is_a_refusal_in_the_providers_words_live() {
let Some(key) = key() else { return };
let client = GatewayVideo::new(config(&key));
let err = asked(
&client,
"https://www.youtube.com/watch?v=aaaaaaaaaaa",
"Say what this video shows in one sentence.",
)
.await
.expect_err("there is no such video");
let text = err.to_string();
assert!(text.contains("Provider returned error"), "{text}");
assert!(
!text.contains("metadata"),
"the sentence, not the JSON: {text}"
);
}
#[tokio::test]
#[ignore = "requires MINDFORK_OPENROUTER_KEY"]
async fn a_refused_key_is_said_in_the_gateways_words_live() {
if key().is_none() {
return;
}
let nobody = format!("sk-or-v1-{}", "0".repeat(64));
let client = GatewayVideo::new(config(&nobody));
let err = asked(&client, FRESH_VIDEO, "Say what this video shows.")
.await
.expect_err("refused");
let text = err.to_string();
assert!(
text.contains("401") && text.contains("User not found"),
"{text}"
);
}