use crate::config::registry::AppRegistry;
use crate::domain::launch_blueprint::LaunchTarget;
use crate::launcher::args;
use crate::launcher::env_schema::EnvMap;
use super::stages::{CleanupHandle, ModelResolution, ModelSource, OverlayMaterialization};
type EnvResult = (Vec<String>, Box<dyn FnOnce()>);
pub fn prepare_provider_env(
target: &LaunchTarget,
args: &[String],
env: &[String],
config: &AppRegistry,
) -> anyhow::Result<EnvResult> {
if target.family == "claude_strict" {
return Ok((env.to_vec(), Box::new(|| {})));
}
let mut env_map = EnvMap::from_env_slice_lenient(env);
let resolution = resolve_model(target, args, &env_map);
if let Some(override_model) = args::model_override(args) {
env_map.set("ANTHROPIC_MODEL", &override_model);
for key in &[
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"CLAUDE_CODE_SUBAGENT_MODEL",
] {
env_map.set(key, &override_model);
}
}
let has_claude_vars = env_map.contains_prefix("ANTHROPIC_")
|| env_map.get("CLAUDE_CODE_SUBAGENT_MODEL").is_some();
if !has_claude_vars {
return Ok((env_map.to_env_slice(), Box::new(|| {})));
}
if resolution.session_model.is_empty() {
return Ok((env_map.to_env_slice(), Box::new(|| {})));
}
env_map.set("ANTHROPIC_MODEL", &resolution.session_model);
let overlay = materialize_overlay(&resolution.session_model, config);
if let Some(override_json) = overlay.config_override_json {
env_map.set("ANTHROPIC_CONFIG_OVERRIDE", &override_json);
}
let _cleanup = CleanupHandle::noop();
Ok((env_map.to_env_slice(), Box::new(|| {})))
}
pub fn resolve_model(target: &LaunchTarget, args: &[String], env_map: &EnvMap) -> ModelResolution {
if let Some(override_model) = args::model_override(args) {
return ModelResolution {
session_model: override_model.clone(),
source: ModelSource::CliOverride(override_model),
};
}
for key in &[
"ANTHROPIC_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
"CLAUDE_CODE_SUBAGENT_MODEL",
] {
if let Some(model) = env_map.get(key) {
let trimmed = model.trim();
if !trimmed.is_empty() {
return ModelResolution {
session_model: trimmed.to_string(),
source: ModelSource::EnvVar(trimmed.to_string()),
};
}
}
}
let model = target.model.trim();
if !model.is_empty() {
return ModelResolution {
session_model: model.to_string(),
source: ModelSource::TargetDefault(model.to_string()),
};
}
for key in &["opus", "sonnet", "haiku", "small"] {
if let Some(model) = target.model_tiers.get(*key) {
let trimmed = model.trim();
if !trimmed.is_empty() {
return ModelResolution {
session_model: trimmed.to_string(),
source: ModelSource::TierFallback {
tier: key.to_string(),
model: trimmed.to_string(),
},
};
}
}
}
ModelResolution {
session_model: String::new(),
source: ModelSource::TargetDefault(String::new()),
}
}
pub fn materialize_overlay(model: &str, config: &AppRegistry) -> OverlayMaterialization {
OverlayMaterialization {
config_override_json: build_anthropic_config_override(model, config),
}
}
fn build_anthropic_config_override(model: &str, config: &AppRegistry) -> Option<String> {
let settings = config.model_settings.get(model);
let global_compaction = &config.compaction;
let mut map = serde_json::Map::new();
if global_compaction.auto_compact {
let threshold = settings
.and_then(|s| s.compaction_threshold)
.unwrap_or(global_compaction.threshold);
map.insert(
"autoCompactThreshold".to_string(),
serde_json::Value::from(threshold),
);
}
if let Some(max_tokens) = settings.and_then(|s| s.max_context_tokens) {
map.insert(
"maxContextTokens".to_string(),
serde_json::Value::from(max_tokens),
);
}
if map.is_empty() {
None
} else {
serde_json::to_string(&serde_json::Value::Object(map)).ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::launch_blueprint::LaunchTarget;
use std::collections::HashMap;
fn make_target(family: &str, model: &str) -> LaunchTarget {
LaunchTarget {
profile: "test".to_string(),
display_name: String::new(),
description: String::new(),
category: String::new(),
family: family.to_string(),
base_url: String::new(),
model: model.to_string(),
model_tiers: HashMap::new(),
auth_mode: "secret".to_string(),
secret_key: String::new(),
literal_auth_token: String::new(),
test_url: String::new(),
}
}
fn env_to_map(env: &[String]) -> HashMap<String, String> {
env.iter()
.filter_map(|s| s.split_once('='))
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn test_prepare_env_skips_native_claude() {
let target = make_target("claude_strict", "");
let env = vec!["PATH=/usr/bin".to_string()];
let cfg = AppRegistry::default();
let (result_env, cleanup) = prepare_provider_env(&target, &[], &env, &cfg).expect("env");
cleanup();
let map = env_to_map(&result_env);
assert_eq!(map.get("CLAUDE_CONFIG_DIR").map(|s| s.as_str()), None);
}
#[test]
fn test_prepare_env_sets_anthropic_model() {
let target = make_target("anthropic_compatible_non_claude", "glm-5");
let env = vec![
"PATH=/usr/bin".to_string(),
"ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic".to_string(),
"ANTHROPIC_AUTH_TOKEN=test-key".to_string(),
];
let cfg = AppRegistry::default();
let (result_env, cleanup) = prepare_provider_env(&target, &[], &env, &cfg).expect("env");
cleanup();
let map = env_to_map(&result_env);
assert_eq!(
map.get("ANTHROPIC_MODEL").map(|s| s.as_str()),
Some("glm-5")
);
assert_eq!(map.get("CLAUDE_CONFIG_DIR").map(|s| s.as_str()), None);
}
#[test]
fn test_prepare_env_resolves_model_from_tiers() {
let mut target = make_target("anthropic_compatible_non_claude", "");
target.model_tiers = HashMap::from([("opus".to_string(), "glm-5".to_string())]);
let env = vec![
"PATH=/usr/bin".to_string(),
"ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic".to_string(),
"ANTHROPIC_DEFAULT_OPUS_MODEL=glm-5".to_string(),
"ANTHROPIC_AUTH_TOKEN=test-key".to_string(),
];
let cfg = AppRegistry::default();
let (result_env, cleanup) = prepare_provider_env(&target, &[], &env, &cfg).expect("env");
cleanup();
let map = env_to_map(&result_env);
assert_eq!(
map.get("ANTHROPIC_MODEL").map(|s| s.as_str()),
Some("glm-5")
);
}
#[test]
fn test_resolve_model_from_target_default() {
let target = make_target("anthropic_compatible_non_claude", "glm-5");
let env_map = EnvMap::from_env_slice_lenient(&[]);
let result = resolve_model(&target, &[], &env_map);
assert_eq!(result.session_model, "glm-5");
assert!(matches!(result.source, ModelSource::TargetDefault(_)));
}
#[test]
fn test_resolve_model_from_tier_fallback() {
let mut target = make_target("anthropic_compatible_non_claude", "");
target.model_tiers = HashMap::from([("opus".to_string(), "glm-5".to_string())]);
let env_map = EnvMap::from_env_slice_lenient(&[]);
let result = resolve_model(&target, &[], &env_map);
assert_eq!(result.session_model, "glm-5");
assert!(matches!(result.source, ModelSource::TierFallback { .. }));
}
#[test]
fn test_overlay_auto_compact_off_omits_threshold() {
let cfg = AppRegistry {
compaction: crate::config::registry::ContextWindowPolicy {
auto_compact: false,
threshold: 0.8,
},
..AppRegistry::default()
};
let result = build_anthropic_config_override("any-model", &cfg);
assert!(
result.is_none(),
"auto_compact=false should produce no override"
);
}
#[test]
fn test_overlay_auto_compact_on_includes_threshold() {
let cfg = AppRegistry {
compaction: crate::config::registry::ContextWindowPolicy {
auto_compact: true,
threshold: 0.7,
},
..AppRegistry::default()
};
let json =
build_anthropic_config_override("any-model", &cfg).expect("should produce override");
assert!(json.contains("autoCompactThreshold"));
assert!(json.contains("0.7"));
}
#[test]
fn test_overlay_max_context_tokens_applied() {
let mut cfg = AppRegistry::default();
cfg.model_settings.insert(
"glm-5".to_string(),
crate::config::registry::PerModelOverrides {
max_context_tokens: Some(128000),
compaction_threshold: None,
},
);
let json = build_anthropic_config_override("glm-5", &cfg).expect("override");
assert!(json.contains("maxContextTokens"));
assert!(json.contains("128000"));
}
#[test]
fn test_overlay_per_model_threshold_overrides_global() {
let mut cfg = AppRegistry {
compaction: crate::config::registry::ContextWindowPolicy {
auto_compact: true,
threshold: 0.8,
},
..AppRegistry::default()
};
cfg.model_settings.insert(
"glm-5".to_string(),
crate::config::registry::PerModelOverrides {
max_context_tokens: None,
compaction_threshold: Some(0.5),
},
);
let json = build_anthropic_config_override("glm-5", &cfg).expect("override");
assert!(json.contains("0.5"));
assert!(!json.contains("0.8"));
}
}