use serde_json::json;
use super::*;
use crate::completion::{CompletionRequest, ToolDefinition};
use crate::message::ToolChoice;
use crate::providers::anthropic::AnthropicConfig;
use crate::providers::cohere::CohereConfig;
use crate::providers::gemini::GeminiConfig;
use crate::providers::ollama::OllamaConfig;
use crate::providers::openai::OpenAIConfig;
use crate::providers::openai::embedding::EncodingFormat;
use crate::providers::openai::wire::{GROQ, LLAMACPP, MISTRAL, OPENAI, PERPLEXITY, TOGETHER};
use crate::providers::voyageai::VoyageAiConfig;
use crate::wire::{Mode, Wire};
const BAD: &str = "http://bad host";
fn request() -> CompletionRequest {
CompletionRequest::new("hi").max_tokens(16)
}
fn failure<T>(result: Result<T, EncodeError>) -> ProviderError {
match result {
Ok(_) => panic!("the request must not build"),
Err(error) => error.into(),
}
}
fn assert_request_building(case: &str, error: &ProviderError) {
assert_eq!(
error.boundary(),
AdapterErrorBoundary::Request,
"{case}: {error}"
);
assert!(!error.is_retryable(), "{case}: {error}");
assert_eq!(error.report().kind, ErrorKind::Request, "{case}: {error}");
match error {
ProviderError::Request(_) | ProviderError::UnsupportedOption(_) => {}
ProviderError::Http(_)
| ProviderError::Url(_)
| ProviderError::Json(_)
| ProviderError::Response(_)
| ProviderError::Provider(_)
| ProviderError::ProviderResponse(_)
| ProviderError::InvalidAuthentication(_)
| ProviderError::CacheExpired { .. }
| ProviderError::MismatchedDimensions { .. }
| ProviderError::Truncated
| ProviderError::Relayed(_) => {
panic!("{case}: an encode failure must not classify as {error:?}")
}
}
}
#[test]
fn every_encode_error_constructor_classifies_as_request_building() {
let json = serde_json::from_str::<serde_json::Value>("{").expect_err("malformed");
let http = http::Request::builder()
.uri("http://bad host")
.body(())
.expect_err("bad uri");
let boxed: BoxError = Box::new(std::io::Error::other("io"));
let message = crate::message::MessageError::ConversionError("nope".into());
let cases: Vec<(&str, EncodeError)> = vec![
("request", EncodeError::request("no endpoint")),
("from json", json.into()),
("from http", http.into()),
("from boxed", boxed.into()),
("from message", message.into()),
(
"unsupported option",
EncodeError::unsupported(crate::completion::UnsupportedOption::new(
"seed",
"aws_bedrock",
"model",
"no seed",
)),
),
];
for (case, error) in cases {
let error = ProviderError::from(error);
assert_request_building(case, &error);
}
}
#[test]
fn provider_encode_failures_classify_as_request_building() {
let openai = OpenAIConfig::with_key(&OPENAI, "k").with_base_url(BAD);
let perplexity = OpenAIConfig::with_key(&PERPLEXITY, "k");
let anthropic = AnthropicConfig::new("k").with_base_url(BAD);
let gemini = GeminiConfig::new("k").with_base_url(BAD);
let specific_tool = CompletionRequest::new("hi")
.tool(ToolDefinition {
name: crate::message::ToolName::new("f").expect("tool name"),
description: "d".into(),
parameters: json!({"type": "object"}),
})
.tool_choice(ToolChoice::Specific {
function_names: vec![crate::message::ToolName::new("f").expect("tool name")],
});
let cases = [
(
"openai chat",
failure(
OpenAIConfig::with_key(&GROQ, "k")
.with_base_url(BAD)
.completion("m")
.encode(request(), Mode::Unary),
),
"RequestError: invalid uri character",
),
(
"openai responses",
failure(openai.completion("gpt-5").encode(request(), Mode::Unary)),
"RequestError: invalid uri character",
),
(
"openai embeddings",
failure(
openai
.embedding("text-embedding-3-small", None)
.encode(vec!["a".into()], Mode::Unary),
),
"RequestError: invalid uri character",
),
(
"openai embeddings, base64",
failure(
OpenAIConfig::with_key(&OPENAI, "k")
.embedding("text-embedding-3-small", None)
.with_encoding_format(EncodingFormat::Base64)
.encode(vec!["a".into()], Mode::Unary),
),
"RequestError: Rig cannot decode openai embedding responses encoded as `base64`",
),
(
"openai-compatible embeddings, unsupported encoding format",
failure(
OpenAIConfig::with_key(&TOGETHER, "k")
.embedding("m", None)
.with_encoding_format(EncodingFormat::Float)
.encode(vec!["a".into()], Mode::Unary),
),
"RequestError: together embeddings do not support the `encoding_format` parameter",
),
(
"openai-compatible embeddings, unsupported user",
failure(
OpenAIConfig::with_key(&MISTRAL, "k")
.embedding("mistral-embed", None)
.with_user("u")
.encode(vec!["a".into()], Mode::Unary),
),
"RequestError: mistral embeddings do not support the `user` parameter",
),
(
"openai rerank without the endpoint",
failure(perplexity.rerank("m").encode(
crate::operation::RerankRequest {
query: "q".into(),
documents: vec!["d".into()],
},
Mode::Unary,
)),
"RequestError: perplexity offers no reranking endpoint",
),
(
"openai verify without the endpoint",
failure(perplexity.verify().encode((), Mode::Unary)),
"RequestError: perplexity offers no endpoint that checks a credential without \
consuming tokens",
),
(
"llama.cpp specific tool choice",
failure(
OpenAIConfig::with_key(&LLAMACPP, "k")
.completion("m")
.encode(specific_tool, Mode::Unary),
),
"RequestError: llama.cpp cannot force a specific tool: `llama-server` accepts only \
`auto`, `none` or `required` for tool_choice and silently treats anything else as \
`auto`, so requesting `f` would return whichever tool the model picked. Use \
`ToolChoice::Required` to force a call, or advertise only `f` in `tools`.",
),
(
"anthropic messages",
failure(
anthropic
.completion("claude-sonnet-4-5")
.encode(request(), Mode::Unary),
),
"RequestError: invalid uri character",
),
(
"anthropic verify",
failure(anthropic.verify().encode((), Mode::Unary)),
"RequestError: invalid uri character",
),
(
"anthropic models",
failure(anthropic.models().encode(None, Mode::Unary)),
"RequestError: invalid uri character",
),
(
"gemini generateContent",
failure(
gemini
.completion("gemini-2.5-flash")
.encode(request(), Mode::Unary),
),
"RequestError: invalid uri character",
),
(
"gemini verify",
failure(gemini.verify().encode((), Mode::Unary)),
"RequestError: invalid uri character",
),
(
"cohere embed",
failure(
CohereConfig::new("k")
.with_base_url(BAD)
.embedding("embed-english-v3.0", None)
.encode(vec!["a".into()], Mode::Unary),
),
"RequestError: invalid uri character",
),
(
"voyage embed",
failure(
VoyageAiConfig::new("k")
.with_base_url(BAD)
.embedding("voyage-3", None)
.encode(vec!["a".into()], Mode::Unary),
),
"RequestError: invalid uri character",
),
(
"ollama chat",
failure(
OllamaConfig::new()
.with_base_url(BAD)
.completion("llama3")
.encode(request(), Mode::Unary),
),
"RequestError: invalid uri character",
),
];
for (case, error, message) in cases {
assert_request_building(case, &error);
assert_eq!(error.to_string(), message, "{case}");
}
}
#[test]
fn an_unsupported_option_is_read_back_from_the_encode_error() {
let refusal =
crate::completion::UnsupportedOption::new("cache", "deepseek", "deepseek-v4", "no cache");
let error = EncodeError::unsupported(refusal.clone());
assert_eq!(error.unsupported_option(), Some(&refusal));
assert_eq!(EncodeError::request("other").unsupported_option(), None);
let error = ProviderError::from(error);
assert_request_building("unsupported option", &error);
assert!(matches!(&error, ProviderError::UnsupportedOption(option) if *option == refusal));
assert_eq!(
error.to_string(),
"RequestError: `cache` is not supported by deepseek model `deepseek-v4`: no cache"
);
}