mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-11 05:30:19 +08:00
Add fast model directive service tier
This commit is contained in:
@@ -113,7 +113,7 @@ pub(crate) const ADMIN_MODULE_DEFINITIONS: &[AdminModuleDefinition] = &[
|
||||
AdminModuleDefinition {
|
||||
name: "model_directives",
|
||||
display_name: "模型后缀参数",
|
||||
description: "允许通过模型名后缀覆盖推理参数",
|
||||
description: "允许通过模型名后缀覆盖推理参数或服务层级",
|
||||
category: "integration",
|
||||
env_key: "MODEL_DIRECTIVES_AVAILABLE",
|
||||
default_available: true,
|
||||
|
||||
@@ -88,15 +88,16 @@ pub(crate) async fn reasoning_model_directive_enabled_for_api_format_and_model(
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(suffix) = requested_model.and_then(reasoning_suffix_from_model) else {
|
||||
let Some(suffixes) = requested_model.and_then(model_directive_suffixes_from_model) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
settings
|
||||
let mappings = settings
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.api_format_mappings(&api_format))
|
||||
.map(|mappings| mappings.contains_key(&suffix))
|
||||
.unwrap_or_else(|| DEFAULT_REASONING_SUFFIXES.contains(&suffix.as_str()))
|
||||
.and_then(|settings| settings.api_format_mappings(&api_format));
|
||||
suffixes
|
||||
.iter()
|
||||
.all(|suffix| suffix_supported_for_api_format(&api_format, suffix, mappings.as_ref()))
|
||||
}
|
||||
|
||||
pub(crate) async fn reasoning_model_directive_mapping_for_api_format_and_model(
|
||||
@@ -113,17 +114,17 @@ pub(crate) async fn reasoning_model_directive_mapping_for_api_format_and_model(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let suffix = requested_model.and_then(reasoning_suffix_from_model)?;
|
||||
let suffixes = requested_model.and_then(model_directive_suffixes_from_model)?;
|
||||
let api_format = crate::ai_serving::normalize_api_format_alias(api_format);
|
||||
let settings = read_reasoning_model_directive_settings(state).await;
|
||||
settings
|
||||
let mappings = settings
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.api_format_mappings(&api_format))
|
||||
.and_then(|mappings| mappings.get(&suffix).cloned())
|
||||
.or_else(|| default_reasoning_mapping(&api_format, &suffix))
|
||||
.and_then(|settings| settings.api_format_mappings(&api_format));
|
||||
model_directive_mapping_for_suffixes(&api_format, &suffixes, mappings.as_ref())
|
||||
}
|
||||
|
||||
const DEFAULT_REASONING_SUFFIXES: &[&str] = &["low", "medium", "high", "xhigh", "max"];
|
||||
const DEFAULT_MODEL_DIRECTIVE_SUFFIXES: &[&str] =
|
||||
&["low", "medium", "high", "xhigh", "max", "fast"];
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct ReasoningModelDirectiveSettings {
|
||||
@@ -183,22 +184,66 @@ impl ReasoningModelDirectiveSettings {
|
||||
}
|
||||
}
|
||||
|
||||
fn reasoning_suffix_from_model(model: &str) -> Option<String> {
|
||||
let model = model.trim();
|
||||
let (base_model, suffix) = model.rsplit_once('-')?;
|
||||
if base_model.trim().is_empty() {
|
||||
fn model_directive_suffixes_from_model(model: &str) -> Option<Vec<String>> {
|
||||
let mut base_model = model.trim();
|
||||
let mut suffixes = Vec::new();
|
||||
let mut has_reasoning_effort = false;
|
||||
let mut has_service_tier = false;
|
||||
while let Some((candidate_base, suffix)) = base_model.rsplit_once('-') {
|
||||
let Some(suffix) = normalize_reasoning_suffix(suffix) else {
|
||||
break;
|
||||
};
|
||||
match model_directive_suffix_kind(&suffix)? {
|
||||
ModelDirectiveSuffixKind::ReasoningEffort => {
|
||||
if has_reasoning_effort {
|
||||
return None;
|
||||
}
|
||||
has_reasoning_effort = true;
|
||||
}
|
||||
ModelDirectiveSuffixKind::ServiceTier => {
|
||||
if has_service_tier {
|
||||
return None;
|
||||
}
|
||||
has_service_tier = true;
|
||||
}
|
||||
}
|
||||
suffixes.push(suffix);
|
||||
base_model = candidate_base.trim();
|
||||
}
|
||||
if base_model.is_empty() || suffixes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
normalize_reasoning_suffix(suffix)
|
||||
suffixes.sort_by_key(|suffix| {
|
||||
DEFAULT_MODEL_DIRECTIVE_SUFFIXES
|
||||
.iter()
|
||||
.position(|value| value == suffix)
|
||||
.unwrap_or(usize::MAX)
|
||||
});
|
||||
Some(suffixes)
|
||||
}
|
||||
|
||||
fn normalize_reasoning_suffix(suffix: &str) -> Option<String> {
|
||||
let normalized = suffix.trim().to_ascii_lowercase();
|
||||
DEFAULT_REASONING_SUFFIXES
|
||||
DEFAULT_MODEL_DIRECTIVE_SUFFIXES
|
||||
.contains(&normalized.as_str())
|
||||
.then_some(normalized)
|
||||
}
|
||||
|
||||
enum ModelDirectiveSuffixKind {
|
||||
ReasoningEffort,
|
||||
ServiceTier,
|
||||
}
|
||||
|
||||
fn model_directive_suffix_kind(suffix: &str) -> Option<ModelDirectiveSuffixKind> {
|
||||
match suffix {
|
||||
"low" | "medium" | "high" | "xhigh" | "max" => {
|
||||
Some(ModelDirectiveSuffixKind::ReasoningEffort)
|
||||
}
|
||||
"fast" => Some(ModelDirectiveSuffixKind::ServiceTier),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_reasoning_mappings(
|
||||
mappings: &serde_json::Map<String, serde_json::Value>,
|
||||
) -> serde_json::Map<String, serde_json::Value> {
|
||||
@@ -212,10 +257,16 @@ fn normalize_reasoning_mappings(
|
||||
|
||||
fn default_reasoning_mapping(api_format: &str, suffix: &str) -> Option<serde_json::Value> {
|
||||
match api_format {
|
||||
"openai:chat" if suffix == "fast" => {
|
||||
Some(serde_json::json!({ "service_tier": "priority" }))
|
||||
}
|
||||
"openai:chat" => {
|
||||
let effort = openai_reasoning_effort_value(suffix)?;
|
||||
Some(serde_json::json!({ "reasoning_effort": effort }))
|
||||
}
|
||||
"openai:responses" | "openai:responses:compact" if suffix == "fast" => {
|
||||
Some(serde_json::json!({ "service_tier": "priority" }))
|
||||
}
|
||||
"openai:responses" | "openai:responses:compact" => {
|
||||
let effort = openai_reasoning_effort_value(suffix)?;
|
||||
Some(serde_json::json!({ "reasoning": { "effort": effort } }))
|
||||
@@ -251,6 +302,54 @@ fn default_reasoning_mapping(api_format: &str, suffix: &str) -> Option<serde_jso
|
||||
}
|
||||
}
|
||||
|
||||
fn suffix_supported_for_api_format(
|
||||
api_format: &str,
|
||||
suffix: &str,
|
||||
mappings: Option<&serde_json::Map<String, serde_json::Value>>,
|
||||
) -> bool {
|
||||
mappings
|
||||
.map(|mappings| mappings.contains_key(suffix))
|
||||
.unwrap_or_else(|| default_reasoning_mapping(api_format, suffix).is_some())
|
||||
}
|
||||
|
||||
fn model_directive_mapping_for_suffixes(
|
||||
api_format: &str,
|
||||
suffixes: &[String],
|
||||
mappings: Option<&serde_json::Map<String, serde_json::Value>>,
|
||||
) -> Option<serde_json::Value> {
|
||||
let mut combined = serde_json::json!({});
|
||||
for suffix in suffixes {
|
||||
let mapping = mappings
|
||||
.and_then(|mappings| mappings.get(suffix).cloned())
|
||||
.or_else(|| {
|
||||
mappings
|
||||
.is_none()
|
||||
.then(|| default_reasoning_mapping(api_format, suffix))
|
||||
.flatten()
|
||||
})?;
|
||||
deep_merge_json(&mut combined, &mapping);
|
||||
}
|
||||
Some(combined)
|
||||
}
|
||||
|
||||
fn deep_merge_json(target: &mut serde_json::Value, patch: &serde_json::Value) {
|
||||
match (target, patch) {
|
||||
(serde_json::Value::Object(target_object), serde_json::Value::Object(patch_object)) => {
|
||||
for (key, patch_value) in patch_object {
|
||||
match target_object.get_mut(key) {
|
||||
Some(target_value) => deep_merge_json(target_value, patch_value),
|
||||
None => {
|
||||
target_object.insert(key.clone(), patch_value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(target, patch) => {
|
||||
*target = patch.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn openai_reasoning_effort_value(suffix: &str) -> Option<&'static str> {
|
||||
match suffix {
|
||||
"low" => Some("low"),
|
||||
@@ -297,7 +396,11 @@ fn parse_reasoning_model_directive_settings(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_reasoning_model_directive_settings;
|
||||
use super::{
|
||||
default_reasoning_mapping, model_directive_mapping_for_suffixes,
|
||||
model_directive_suffixes_from_model, parse_reasoning_model_directive_settings,
|
||||
suffix_supported_for_api_format,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
@@ -332,4 +435,64 @@ mod tests {
|
||||
);
|
||||
assert_eq!(settings.api_format_enabled("gemini:generate_content"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_fast_suffix_maps_to_openai_priority_service_tier() {
|
||||
assert_eq!(
|
||||
default_reasoning_mapping("openai:chat", "fast"),
|
||||
Some(json!({ "service_tier": "priority" }))
|
||||
);
|
||||
assert_eq!(
|
||||
default_reasoning_mapping("openai:responses", "fast"),
|
||||
Some(json!({ "service_tier": "priority" }))
|
||||
);
|
||||
assert_eq!(
|
||||
default_reasoning_mapping("openai:responses:compact", "fast"),
|
||||
Some(json!({ "service_tier": "priority" }))
|
||||
);
|
||||
assert_eq!(default_reasoning_mapping("claude:messages", "fast"), None);
|
||||
assert_eq!(
|
||||
default_reasoning_mapping("gemini:generate_content", "fast"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combined_suffixes_are_order_insensitive() {
|
||||
let expected = Some(vec!["xhigh".to_string(), "fast".to_string()]);
|
||||
assert_eq!(
|
||||
model_directive_suffixes_from_model("gpt-5.4-fast-xhigh"),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
model_directive_suffixes_from_model("gpt-5.4-xhigh-fast"),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
model_directive_mapping_for_suffixes(
|
||||
"openai:chat",
|
||||
expected.as_ref().expect("suffixes should parse"),
|
||||
None,
|
||||
),
|
||||
Some(json!({
|
||||
"reasoning_effort": "xhigh",
|
||||
"service_tier": "priority"
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combined_suffix_requires_each_suffix_to_support_api_format() {
|
||||
let suffixes = model_directive_suffixes_from_model("gpt-5.4-xhigh-fast").expect("suffixes");
|
||||
assert!(suffixes
|
||||
.iter()
|
||||
.all(|suffix| { suffix_supported_for_api_format("openai:responses", suffix, None) }));
|
||||
assert!(!suffixes
|
||||
.iter()
|
||||
.all(|suffix| { suffix_supported_for_api_format("claude:messages", suffix, None) }));
|
||||
assert_eq!(
|
||||
model_directive_mapping_for_suffixes("claude:messages", &suffixes, None),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1729,7 +1729,8 @@ pub fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value>
|
||||
"medium": { "reasoning_effort": "medium" },
|
||||
"high": { "reasoning_effort": "high" },
|
||||
"xhigh": { "reasoning_effort": "xhigh" },
|
||||
"max": { "reasoning_effort": "xhigh" }
|
||||
"max": { "reasoning_effort": "xhigh" },
|
||||
"fast": { "service_tier": "priority" }
|
||||
}
|
||||
},
|
||||
"openai:responses": {
|
||||
@@ -1739,7 +1740,8 @@ pub fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value>
|
||||
"medium": { "reasoning": { "effort": "medium" } },
|
||||
"high": { "reasoning": { "effort": "high" } },
|
||||
"xhigh": { "reasoning": { "effort": "xhigh" } },
|
||||
"max": { "reasoning": { "effort": "xhigh" } }
|
||||
"max": { "reasoning": { "effort": "xhigh" } },
|
||||
"fast": { "service_tier": "priority" }
|
||||
}
|
||||
},
|
||||
"openai:responses:compact": {
|
||||
@@ -1749,7 +1751,8 @@ pub fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value>
|
||||
"medium": { "reasoning": { "effort": "medium" } },
|
||||
"high": { "reasoning": { "effort": "high" } },
|
||||
"xhigh": { "reasoning": { "effort": "xhigh" } },
|
||||
"max": { "reasoning": { "effort": "xhigh" } }
|
||||
"max": { "reasoning": { "effort": "xhigh" } },
|
||||
"fast": { "service_tier": "priority" }
|
||||
}
|
||||
},
|
||||
"claude:messages": {
|
||||
|
||||
@@ -77,7 +77,7 @@ pub use crate::formats::shared::model_directives::{
|
||||
apply_model_directive_overrides_from_request, claude_model_uses_adaptive_effort,
|
||||
extract_gemini_model_from_path, gemini_model_uses_thinking_level, model_directive_base_model,
|
||||
normalize_model_directive_model, parse_model_directive, ModelDirective, ModelOverride,
|
||||
ReasoningEffort,
|
||||
ReasoningEffort, ServiceTier,
|
||||
};
|
||||
pub use crate::formats::shared::passthrough::{
|
||||
resolve_stream_spec as resolve_local_same_format_stream_spec,
|
||||
|
||||
@@ -9,6 +9,7 @@ pub struct ModelDirective {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ModelOverride {
|
||||
ReasoningEffort(ReasoningEffort),
|
||||
ServiceTier(ServiceTier),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -79,19 +80,90 @@ impl ReasoningEffort {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ServiceTier {
|
||||
Priority,
|
||||
}
|
||||
|
||||
impl ServiceTier {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"fast" => Some(Self::Priority),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_openai_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::Priority => "priority",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_model_directive(model: &str) -> Option<ModelDirective> {
|
||||
let model = model.trim();
|
||||
let (base_model, suffix) = model.rsplit_once('-')?;
|
||||
let base_model = base_model.trim();
|
||||
let (base_model, overrides) = parse_model_directive_parts(model)?;
|
||||
Some(ModelDirective {
|
||||
base_model,
|
||||
overrides,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_model_directive_parts(model: &str) -> Option<(String, Vec<ModelOverride>)> {
|
||||
let mut base_model = model.trim();
|
||||
let mut overrides = ModelOverrideAccumulator::default();
|
||||
while let Some((candidate_base, suffix)) = base_model.rsplit_once('-') {
|
||||
let Some(override_item) = parse_model_override(suffix) else {
|
||||
break;
|
||||
};
|
||||
overrides.insert(override_item)?;
|
||||
base_model = candidate_base.trim();
|
||||
}
|
||||
if base_model.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let overrides = overrides.into_overrides()?;
|
||||
Some((base_model.to_string(), overrides))
|
||||
}
|
||||
|
||||
let reasoning_effort = ReasoningEffort::parse(suffix)?;
|
||||
Some(ModelDirective {
|
||||
base_model: base_model.to_string(),
|
||||
overrides: vec![ModelOverride::ReasoningEffort(reasoning_effort)],
|
||||
})
|
||||
fn parse_model_override(suffix: &str) -> Option<ModelOverride> {
|
||||
ReasoningEffort::parse(suffix)
|
||||
.map(ModelOverride::ReasoningEffort)
|
||||
.or_else(|| ServiceTier::parse(suffix).map(ModelOverride::ServiceTier))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ModelOverrideAccumulator {
|
||||
reasoning_effort: Option<ReasoningEffort>,
|
||||
service_tier: Option<ServiceTier>,
|
||||
}
|
||||
|
||||
impl ModelOverrideAccumulator {
|
||||
fn insert(&mut self, override_item: ModelOverride) -> Option<()> {
|
||||
match override_item {
|
||||
ModelOverride::ReasoningEffort(value) => {
|
||||
if self.reasoning_effort.replace(value).is_some() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
ModelOverride::ServiceTier(value) => {
|
||||
if self.service_tier.replace(value).is_some() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn into_overrides(self) -> Option<Vec<ModelOverride>> {
|
||||
let mut overrides = Vec::new();
|
||||
if let Some(reasoning_effort) = self.reasoning_effort {
|
||||
overrides.push(ModelOverride::ReasoningEffort(reasoning_effort));
|
||||
}
|
||||
if let Some(service_tier) = self.service_tier {
|
||||
overrides.push(ModelOverride::ServiceTier(service_tier));
|
||||
}
|
||||
(!overrides.is_empty()).then_some(overrides)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn model_directive_base_model(model: &str) -> Option<String> {
|
||||
@@ -149,18 +221,23 @@ pub fn apply_model_directive_overrides_from_model(
|
||||
source_model: &str,
|
||||
) -> Option<ModelDirective> {
|
||||
let directive = parse_model_directive(source_model)?;
|
||||
let mut patched_body = provider_request_body.clone();
|
||||
for override_item in &directive.overrides {
|
||||
match override_item {
|
||||
ModelOverride::ReasoningEffort(effort) => {
|
||||
apply_reasoning_effort_override(
|
||||
provider_request_body,
|
||||
&mut patched_body,
|
||||
provider_api_format,
|
||||
provider_model,
|
||||
*effort,
|
||||
)?;
|
||||
}
|
||||
ModelOverride::ServiceTier(tier) => {
|
||||
apply_service_tier_override(&mut patched_body, provider_api_format, *tier)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
*provider_request_body = patched_body;
|
||||
Some(directive)
|
||||
}
|
||||
|
||||
@@ -215,6 +292,21 @@ fn apply_reasoning_effort_override(
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_service_tier_override(
|
||||
provider_request_body: &mut Value,
|
||||
provider_api_format: &str,
|
||||
tier: ServiceTier,
|
||||
) -> Option<()> {
|
||||
match crate::normalize_api_format_alias(provider_api_format).as_str() {
|
||||
"openai:chat" | "openai:responses" | "openai:responses:compact" => set_object_string(
|
||||
provider_request_body,
|
||||
"service_tier",
|
||||
tier.as_openai_value(),
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_object_string(body: &mut Value, key: &str, value: &str) -> Option<()> {
|
||||
body.as_object_mut()?
|
||||
.insert(key.to_string(), Value::String(value.to_string()));
|
||||
@@ -368,7 +460,7 @@ mod tests {
|
||||
|
||||
use super::{
|
||||
apply_model_directive_overrides_from_model, parse_model_directive, ModelDirective,
|
||||
ModelOverride, ReasoningEffort,
|
||||
ModelOverride, ReasoningEffort, ServiceTier,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -389,12 +481,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_supported_service_tier_suffixes() {
|
||||
assert_eq!(
|
||||
parse_model_directive("gpt-5.4-fast"),
|
||||
Some(ModelDirective {
|
||||
base_model: "gpt-5.4".to_string(),
|
||||
overrides: vec![ModelOverride::ServiceTier(ServiceTier::Priority)],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_combined_suffixes_in_canonical_order() {
|
||||
let expected = Some(ModelDirective {
|
||||
base_model: "gpt-5.4".to_string(),
|
||||
overrides: vec![
|
||||
ModelOverride::ReasoningEffort(ReasoningEffort::XHigh),
|
||||
ModelOverride::ServiceTier(ServiceTier::Priority),
|
||||
],
|
||||
});
|
||||
assert_eq!(parse_model_directive("gpt-5.4-fast-xhigh"), expected);
|
||||
assert_eq!(parse_model_directive("gpt-5.4-xhigh-fast"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_unknown_or_incomplete_suffixes() {
|
||||
assert_eq!(parse_model_directive("gpt-5.4-ultra"), None);
|
||||
assert_eq!(parse_model_directive("gpt-5.4"), None);
|
||||
assert_eq!(parse_model_directive("-high"), None);
|
||||
assert_eq!(parse_model_directive("gpt-5.4-high-json"), None);
|
||||
assert_eq!(parse_model_directive("gpt-5.4-low-high"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -446,4 +563,65 @@ mod tests {
|
||||
2048
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applies_fast_suffix_to_openai_service_tier() {
|
||||
let mut openai_chat = json!({"model": "gpt-5-upstream"});
|
||||
apply_model_directive_overrides_from_model(
|
||||
&mut openai_chat,
|
||||
"openai:chat",
|
||||
"gpt-5-upstream",
|
||||
"gpt-5.4-fast",
|
||||
)
|
||||
.expect("directive should apply");
|
||||
assert_eq!(openai_chat["service_tier"], "priority");
|
||||
|
||||
let mut responses = json!({"model": "gpt-5-upstream"});
|
||||
apply_model_directive_overrides_from_model(
|
||||
&mut responses,
|
||||
"openai:responses",
|
||||
"gpt-5-upstream",
|
||||
"gpt-5.4-fast",
|
||||
)
|
||||
.expect("directive should apply");
|
||||
assert_eq!(responses["service_tier"], "priority");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applies_combined_suffixes_to_openai_body() {
|
||||
let mut openai_chat = json!({"model": "gpt-5-upstream", "reasoning_effort": "low"});
|
||||
apply_model_directive_overrides_from_model(
|
||||
&mut openai_chat,
|
||||
"openai:chat",
|
||||
"gpt-5-upstream",
|
||||
"gpt-5.4-fast-xhigh",
|
||||
)
|
||||
.expect("directive should apply");
|
||||
assert_eq!(openai_chat["reasoning_effort"], "xhigh");
|
||||
assert_eq!(openai_chat["service_tier"], "priority");
|
||||
|
||||
let mut reversed = json!({"model": "gpt-5-upstream", "reasoning_effort": "low"});
|
||||
apply_model_directive_overrides_from_model(
|
||||
&mut reversed,
|
||||
"openai:chat",
|
||||
"gpt-5-upstream",
|
||||
"gpt-5.4-xhigh-fast",
|
||||
)
|
||||
.expect("directive should apply");
|
||||
assert_eq!(reversed, openai_chat);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_combined_suffix_leaves_body_unchanged() {
|
||||
let mut claude = json!({"model": "claude-sonnet-4-5"});
|
||||
let original = claude.clone();
|
||||
assert!(apply_model_directive_overrides_from_model(
|
||||
&mut claude,
|
||||
"claude:messages",
|
||||
"claude-sonnet-4-5",
|
||||
"gpt-5.4-fast-xhigh",
|
||||
)
|
||||
.is_none());
|
||||
assert_eq!(claude, original);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ pub use formats::shared::model_directives::{
|
||||
apply_model_directive_overrides_from_request, claude_model_uses_adaptive_effort,
|
||||
extract_gemini_model_from_path, gemini_model_uses_thinking_level, model_directive_base_model,
|
||||
normalize_model_directive_model, parse_model_directive, ModelDirective, ModelOverride,
|
||||
ReasoningEffort,
|
||||
ReasoningEffort, ServiceTier,
|
||||
};
|
||||
pub use formats::shared::request::{
|
||||
endpoint_config_forces_upstream_stream_policy, enforce_request_body_stream_field,
|
||||
|
||||
@@ -548,6 +548,24 @@ mod tests {
|
||||
.as_deref(),
|
||||
Some("gpt-5.4")
|
||||
);
|
||||
|
||||
let row = sample_row("gpt-5.4", "gpt-5.4-upstream");
|
||||
assert!(row_supports_requested_model_with_model_directives(
|
||||
&row,
|
||||
"gpt-5.4-fast-xhigh",
|
||||
"openai:chat",
|
||||
true
|
||||
));
|
||||
assert_eq!(
|
||||
resolve_requested_global_model_name_with_model_directives(
|
||||
&[row],
|
||||
"gpt-5.4-xhigh-fast",
|
||||
"openai:chat",
|
||||
true
|
||||
)
|
||||
.as_deref(),
|
||||
Some("gpt-5.4")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user