mirror of
https://github.com/Darkatse/TauriTavern.git
synced 2026-10-19 15:45:12 +00:00
feat(openai): extend additional parameters to native providers
This commit is contained in:
parent
7acd1b7635
commit
2b206be3c5
4 changed files with 181 additions and 7 deletions
|
|
@ -161,7 +161,7 @@ async fn resolve_api_config(
|
|||
base_url,
|
||||
api_key,
|
||||
authorization_header: None,
|
||||
extra_headers: source_extra_headers(source),
|
||||
extra_headers: source_extra_headers_with_overrides(source, custom_headers_raw)?,
|
||||
anthropic_beta_header_mode: source_anthropic_beta_header_mode(source),
|
||||
})
|
||||
}
|
||||
|
|
@ -434,6 +434,28 @@ fn source_extra_headers(source: ChatCompletionSource) -> HashMap<String, String>
|
|||
headers
|
||||
}
|
||||
|
||||
fn source_extra_headers_with_overrides(
|
||||
source: ChatCompletionSource,
|
||||
custom_headers_raw: &str,
|
||||
) -> Result<HashMap<String, String>, ApplicationError> {
|
||||
let mut extra_headers = source_extra_headers(source);
|
||||
if custom_headers_raw.trim().is_empty() {
|
||||
return Ok(extra_headers);
|
||||
}
|
||||
|
||||
let mut custom_headers = custom_parameters::parse_string_map(custom_headers_raw)?;
|
||||
for header_name in ["Authorization", "x-api-key", "api-key", "anthropic-version"] {
|
||||
if take_header_value(&mut custom_headers, header_name).is_some() {
|
||||
return Err(ApplicationError::ValidationError(format!(
|
||||
"Additional headers cannot override reserved header: {header_name}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
extra_headers.extend(custom_headers);
|
||||
Ok(extra_headers)
|
||||
}
|
||||
|
||||
fn is_zai_coding_endpoint(value: &str) -> bool {
|
||||
value.trim().eq_ignore_ascii_case(ZAI_ENDPOINT_CODING)
|
||||
}
|
||||
|
|
@ -449,6 +471,7 @@ mod tests {
|
|||
use crate::application::dto::chat_completion_dto::{
|
||||
ChatCompletionGenerateRequestDto, ChatCompletionStatusRequestDto,
|
||||
};
|
||||
use crate::application::errors::ApplicationError;
|
||||
use crate::domain::errors::DomainError;
|
||||
use crate::domain::models::secret::Secrets;
|
||||
use crate::domain::repositories::chat_completion_repository::ChatCompletionSource;
|
||||
|
|
@ -693,4 +716,62 @@ mod tests {
|
|||
Some("abc")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn native_generate_merges_additional_headers() {
|
||||
let secret_repository: Arc<dyn SecretRepository> = Arc::new(TestSecretRepository {
|
||||
secrets: HashMap::from([("api_key_claude".to_string(), "secret".to_string())]),
|
||||
});
|
||||
let dto = ChatCompletionGenerateRequestDto {
|
||||
payload: json!({
|
||||
"chat_completion_source": "claude",
|
||||
"custom_include_headers": "X-Trace: abc\nX-Debug: true"
|
||||
})
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("payload should be object"),
|
||||
};
|
||||
|
||||
let config =
|
||||
resolve_generate_api_config(ChatCompletionSource::Claude, &dto, &secret_repository)
|
||||
.await
|
||||
.expect("generate config should resolve");
|
||||
|
||||
assert_eq!(
|
||||
config.extra_headers.get("X-Trace").map(String::as_str),
|
||||
Some("abc")
|
||||
);
|
||||
assert_eq!(
|
||||
config.extra_headers.get("X-Debug").map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn native_generate_rejects_reserved_additional_headers() {
|
||||
let secret_repository: Arc<dyn SecretRepository> = Arc::new(TestSecretRepository {
|
||||
secrets: HashMap::from([("api_key_claude".to_string(), "secret".to_string())]),
|
||||
});
|
||||
let dto = ChatCompletionGenerateRequestDto {
|
||||
payload: json!({
|
||||
"chat_completion_source": "claude",
|
||||
"custom_include_headers": "Authorization: Bearer hacked"
|
||||
})
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("payload should be object"),
|
||||
};
|
||||
|
||||
let error =
|
||||
resolve_generate_api_config(ChatCompletionSource::Claude, &dto, &secret_repository)
|
||||
.await
|
||||
.expect_err("reserved headers should be rejected");
|
||||
|
||||
match error {
|
||||
ApplicationError::ValidationError(message) => {
|
||||
assert!(message.contains("Additional headers cannot override reserved header"));
|
||||
}
|
||||
other => panic!("unexpected error: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,12 +27,22 @@ pub(super) fn build_payload(
|
|||
payload: Map<String, Value>,
|
||||
) -> Result<(String, Value), ApplicationError> {
|
||||
let mut payload = payload;
|
||||
let include_body_raw = payload
|
||||
.get("custom_include_body")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let exclude_body_raw = payload
|
||||
.get("custom_exclude_body")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
if source != ChatCompletionSource::DeepSeek {
|
||||
prompt_post_processing::apply_custom_prompt_post_processing(&mut payload);
|
||||
}
|
||||
|
||||
match source {
|
||||
let (endpoint_path, mut upstream_payload) = match source {
|
||||
ChatCompletionSource::OpenAi
|
||||
| ChatCompletionSource::Groq
|
||||
| ChatCompletionSource::SiliconFlow => Ok(openai::build(payload)),
|
||||
|
|
@ -47,5 +57,77 @@ pub(super) fn build_payload(
|
|||
ChatCompletionSource::Claude => Ok(claude::build(payload)?),
|
||||
ChatCompletionSource::Makersuite => Ok(makersuite::build(payload)?),
|
||||
ChatCompletionSource::VertexAi => Ok(vertexai::build(payload)?),
|
||||
}?;
|
||||
|
||||
if matches!(
|
||||
source,
|
||||
ChatCompletionSource::Claude | ChatCompletionSource::DeepSeek
|
||||
) {
|
||||
shared::apply_custom_body_overrides(
|
||||
&mut upstream_payload,
|
||||
&include_body_raw,
|
||||
&exclude_body_raw,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok((endpoint_path, upstream_payload))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::build_payload;
|
||||
use crate::domain::repositories::chat_completion_repository::ChatCompletionSource;
|
||||
|
||||
#[test]
|
||||
fn deepseek_applies_additional_body_overrides() {
|
||||
let payload = json!({
|
||||
"chat_completion_source": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"custom_include_body": "{\"x_extra\":true}",
|
||||
"custom_exclude_body": "[\"temperature\"]"
|
||||
})
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("payload must be object");
|
||||
|
||||
let (_, upstream) =
|
||||
build_payload(ChatCompletionSource::DeepSeek, payload).expect("payload should build");
|
||||
let body = upstream.as_object().expect("body must be object");
|
||||
|
||||
assert_eq!(
|
||||
body.get("x_extra").and_then(serde_json::Value::as_bool),
|
||||
Some(true)
|
||||
);
|
||||
assert!(body.get("temperature").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_applies_additional_body_overrides() {
|
||||
let payload = json!({
|
||||
"chat_completion_source": "claude",
|
||||
"model": "claude-sonnet-4-5",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"custom_include_body": "{\"metadata\":{\"feature\":\"override\"}}",
|
||||
"custom_exclude_body": "[\"stream\"]"
|
||||
})
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("payload must be object");
|
||||
|
||||
let (_, upstream) =
|
||||
build_payload(ChatCompletionSource::Claude, payload).expect("payload should build");
|
||||
let body = upstream.as_object().expect("body must be object");
|
||||
|
||||
assert_eq!(
|
||||
body.get("metadata")
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.and_then(|value| value.get("feature"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("override")
|
||||
);
|
||||
assert!(body.get("stream").is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3981,7 +3981,7 @@
|
|||
<div class="flex-container flex">
|
||||
<div id="api_button_openai" class="api_button menu_button menu_button_icon" type="submit" data-i18n="Connect">Connect</div>
|
||||
<div class="api_loading menu_button menu_button_icon" data-i18n="Cancel">Cancel</div>
|
||||
<div data-source="custom" id="customize_additional_parameters" class="menu_button menu_button_icon" data-i18n="Additional Parameters">Additional Parameters</div>
|
||||
<div data-source="custom,claude,deepseek" id="customize_additional_parameters" class="menu_button menu_button_icon" data-i18n="Additional Parameters">Additional Parameters</div>
|
||||
<div data-source="openrouter" class="menu_button menu_button_icon openrouter_authorize" title="Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai" data-i18n="Authorize;[title]Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai">Authorize</div>
|
||||
<div id="test_api_button" class="menu_button menu_button_icon" title="Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!" data-i18n="Test Message;[title]Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!">Test Message</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2964,12 +2964,20 @@ export async function createGenerationParameters(settings, model, type, messages
|
|||
generate_data.stop = getCustomStoppingStrings(); // Mistral shouldn't have limits on stop strings.
|
||||
}
|
||||
|
||||
if (settings.chat_completion_source === chat_completion_sources.CUSTOM) {
|
||||
generate_data.custom_url = settings.custom_url;
|
||||
generate_data.custom_api_format = settings.custom_api_format;
|
||||
const additionalParameterSources = [
|
||||
chat_completion_sources.CUSTOM,
|
||||
chat_completion_sources.CLAUDE,
|
||||
chat_completion_sources.DEEPSEEK,
|
||||
];
|
||||
if (additionalParameterSources.includes(settings.chat_completion_source)) {
|
||||
generate_data.custom_include_body = settings.custom_include_body;
|
||||
generate_data.custom_exclude_body = settings.custom_exclude_body;
|
||||
generate_data.custom_include_headers = settings.custom_include_headers;
|
||||
}
|
||||
|
||||
if (settings.chat_completion_source === chat_completion_sources.CUSTOM) {
|
||||
generate_data.custom_url = settings.custom_url;
|
||||
generate_data.custom_api_format = settings.custom_api_format;
|
||||
generate_data.custom_claude_prompt_caching =
|
||||
settings.custom_api_format === custom_api_formats.CLAUDE_MESSAGES
|
||||
&& Boolean(settings.custom_claude_prompt_caching);
|
||||
|
|
@ -4628,11 +4636,14 @@ async function getStatusOpen() {
|
|||
await validateReverseProxy();
|
||||
}
|
||||
|
||||
if ([chat_completion_sources.CUSTOM, chat_completion_sources.CLAUDE, chat_completion_sources.DEEPSEEK].includes(oai_settings.chat_completion_source)) {
|
||||
data.custom_include_headers = oai_settings.custom_include_headers;
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source === chat_completion_sources.CUSTOM) {
|
||||
$('.model_custom_select').empty();
|
||||
data.custom_api_format = oai_settings.custom_api_format;
|
||||
data.custom_url = oai_settings.custom_url;
|
||||
data.custom_include_headers = oai_settings.custom_include_headers;
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue