mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 06:32:17 +00:00
batch fixes
This commit is contained in:
Generated
+14
-14
@@ -3866,7 +3866,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-api"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3902,7 +3902,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-channels"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3933,7 +3933,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-cli"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
@@ -3960,7 +3960,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-desktop"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"open",
|
||||
@@ -3986,7 +3986,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-extensions"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
@@ -4014,7 +4014,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-hands"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dashmap",
|
||||
@@ -4031,7 +4031,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-kernel"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4067,7 +4067,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-memory"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4086,7 +4086,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-migrate"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 6.0.0",
|
||||
@@ -4105,7 +4105,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-runtime"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4137,7 +4137,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-skills"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
@@ -4159,7 +4159,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-types"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4178,7 +4178,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-wire"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -8790,7 +8790,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.9"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0 OR MIT"
|
||||
repository = "https://github.com/RightNow-AI/openfang"
|
||||
|
||||
@@ -82,6 +82,10 @@ pub async fn auth(
|
||||
// Public endpoints that don't require auth (dashboard needs these)
|
||||
let path = request.uri().path();
|
||||
if path == "/"
|
||||
|| path == "/logo.png"
|
||||
|| path == "/favicon.ico"
|
||||
|| path == "/.well-known/agent.json"
|
||||
|| path.starts_with("/a2a/")
|
||||
|| path == "/api/health"
|
||||
|| path == "/api/health/detail"
|
||||
|| path == "/api/status"
|
||||
|
||||
@@ -1123,7 +1123,7 @@ const CHANNEL_REGISTRY: &[ChannelMeta] = &[
|
||||
ChannelField { key: "bot_token_env", label: "Bot Token", field_type: FieldType::Secret, env_var: Some("DISCORD_BOT_TOKEN"), required: true, placeholder: "MTIz...", advanced: false },
|
||||
ChannelField { key: "allowed_guilds", label: "Allowed Guild IDs", field_type: FieldType::List, env_var: None, required: false, placeholder: "123456789, 987654321", advanced: true },
|
||||
ChannelField { key: "default_agent", label: "Default Agent", field_type: FieldType::Text, env_var: None, required: false, placeholder: "assistant", advanced: true },
|
||||
ChannelField { key: "intents", label: "Intents Bitmask", field_type: FieldType::Number, env_var: None, required: false, placeholder: "33280", advanced: true },
|
||||
ChannelField { key: "intents", label: "Intents Bitmask", field_type: FieldType::Number, env_var: None, required: false, placeholder: "37376", advanced: true },
|
||||
],
|
||||
setup_steps: &["Go to discord.com/developers/applications", "Create a bot and copy the token", "Paste it below"],
|
||||
config_template: "[channels.discord]\nbot_token_env = \"DISCORD_BOT_TOKEN\"",
|
||||
@@ -3022,13 +3022,18 @@ pub async fn clawhub_install(
|
||||
)
|
||||
}
|
||||
Err(e) => {
|
||||
let status = if e.to_string().contains("SecurityBlocked") {
|
||||
let msg = format!("{e}");
|
||||
let status = if msg.contains("SecurityBlocked") {
|
||||
StatusCode::FORBIDDEN
|
||||
} else if msg.contains("429") || msg.contains("rate limit") {
|
||||
StatusCode::TOO_MANY_REQUESTS
|
||||
} else if msg.contains("Network error") || msg.contains("returned 4") || msg.contains("returned 5") {
|
||||
StatusCode::BAD_GATEWAY
|
||||
} else {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
};
|
||||
tracing::warn!("ClawHub install failed: {e}");
|
||||
(status, Json(serde_json::json!({"error": format!("{e}")})))
|
||||
tracing::warn!("ClawHub install failed: {msg}");
|
||||
(status, Json(serde_json::json!({"error": msg})))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6987,6 +6992,29 @@ pub async fn create_schedule(
|
||||
}
|
||||
|
||||
let agent_id_str = req["agent_id"].as_str().unwrap_or("").to_string();
|
||||
if agent_id_str.is_empty() {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({"error": "Missing required field: agent_id"})),
|
||||
);
|
||||
}
|
||||
// Validate agent exists (UUID or name lookup)
|
||||
let agent_exists = if let Ok(aid) = agent_id_str.parse::<AgentId>() {
|
||||
state.kernel.registry.get(aid).is_some()
|
||||
} else {
|
||||
state
|
||||
.kernel
|
||||
.registry
|
||||
.list()
|
||||
.iter()
|
||||
.any(|a| a.name == agent_id_str)
|
||||
};
|
||||
if !agent_exists {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({"error": format!("Agent not found: {agent_id_str}")})),
|
||||
);
|
||||
}
|
||||
let message = req["message"].as_str().unwrap_or("").to_string();
|
||||
let enabled = req.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
|
||||
|
||||
@@ -7160,10 +7188,14 @@ pub async fn run_schedule(
|
||||
.unwrap_or("Scheduled task triggered manually.");
|
||||
let name = schedule["name"].as_str().unwrap_or("(unnamed)");
|
||||
|
||||
// Find the target agent
|
||||
// Find the target agent — require explicit agent_id, no silent fallback
|
||||
let target_agent = if !agent_id_str.is_empty() {
|
||||
if let Ok(aid) = agent_id_str.parse::<AgentId>() {
|
||||
Some(aid)
|
||||
if state.kernel.registry.get(aid).is_some() {
|
||||
Some(aid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
state
|
||||
.kernel
|
||||
@@ -7174,7 +7206,7 @@ pub async fn run_schedule(
|
||||
.map(|a| a.id)
|
||||
}
|
||||
} else {
|
||||
state.kernel.registry.list().first().map(|a| a.id)
|
||||
None
|
||||
};
|
||||
|
||||
let target_agent = match target_agent {
|
||||
|
||||
@@ -684,7 +684,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_discord_adapter_creation() {
|
||||
let adapter = DiscordAdapter::new("test-token".to_string(), vec!["123".to_string(), "456".to_string()], 33280);
|
||||
let adapter = DiscordAdapter::new("test-token".to_string(), vec!["123".to_string(), "456".to_string()], 37376);
|
||||
assert_eq!(adapter.name(), "discord");
|
||||
assert_eq!(adapter.channel_type(), ChannelType::Discord);
|
||||
}
|
||||
|
||||
@@ -405,6 +405,26 @@ enum HandCommands {
|
||||
/// Hand ID.
|
||||
id: String,
|
||||
},
|
||||
/// Check dependency status for a hand.
|
||||
CheckDeps {
|
||||
/// Hand ID.
|
||||
id: String,
|
||||
},
|
||||
/// Install missing dependencies for a hand.
|
||||
InstallDeps {
|
||||
/// Hand ID.
|
||||
id: String,
|
||||
},
|
||||
/// Pause a running hand instance.
|
||||
Pause {
|
||||
/// Instance ID (from `hand active`).
|
||||
id: String,
|
||||
},
|
||||
/// Resume a paused hand instance.
|
||||
Resume {
|
||||
/// Instance ID (from `hand active`).
|
||||
id: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -612,6 +632,9 @@ enum CronCommands {
|
||||
spec: String,
|
||||
/// Prompt to send when the job fires.
|
||||
prompt: String,
|
||||
/// Optional job name (auto-generated if omitted).
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
},
|
||||
/// Delete a scheduled job.
|
||||
Delete {
|
||||
@@ -890,6 +913,10 @@ fn main() {
|
||||
HandCommands::Activate { id } => cmd_hand_activate(&id),
|
||||
HandCommands::Deactivate { id } => cmd_hand_deactivate(&id),
|
||||
HandCommands::Info { id } => cmd_hand_info(&id),
|
||||
HandCommands::CheckDeps { id } => cmd_hand_check_deps(&id),
|
||||
HandCommands::InstallDeps { id } => cmd_hand_install_deps(&id),
|
||||
HandCommands::Pause { id } => cmd_hand_pause(&id),
|
||||
HandCommands::Resume { id } => cmd_hand_resume(&id),
|
||||
},
|
||||
Some(Commands::Config(sub)) => match sub {
|
||||
ConfigCommands::Show => cmd_config_show(),
|
||||
@@ -940,7 +967,8 @@ fn main() {
|
||||
agent,
|
||||
spec,
|
||||
prompt,
|
||||
} => cmd_cron_create(&agent, &spec, &prompt),
|
||||
name,
|
||||
} => cmd_cron_create(&agent, &spec, &prompt, name.as_deref()),
|
||||
CronCommands::Delete { id } => cmd_cron_delete(&id),
|
||||
CronCommands::Enable { id } => cmd_cron_toggle(&id, true),
|
||||
CronCommands::Disable { id } => cmd_cron_toggle(&id, false),
|
||||
@@ -3952,6 +3980,87 @@ fn cmd_hand_info(id: &str) {
|
||||
);
|
||||
}
|
||||
|
||||
fn cmd_hand_check_deps(id: &str) {
|
||||
let base = require_daemon("hand check-deps");
|
||||
let client = daemon_client();
|
||||
let body = daemon_json(
|
||||
client
|
||||
.post(format!("{base}/api/hands/{id}/check-deps"))
|
||||
.send(),
|
||||
);
|
||||
if body.get("error").is_some() {
|
||||
ui::error(&format!(
|
||||
"Failed: {}",
|
||||
body["error"].as_str().unwrap_or("?")
|
||||
));
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&body).unwrap_or_default()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_hand_install_deps(id: &str) {
|
||||
let base = require_daemon("hand install-deps");
|
||||
let client = daemon_client();
|
||||
let body = daemon_json(
|
||||
client
|
||||
.post(format!("{base}/api/hands/{id}/install-deps"))
|
||||
.send(),
|
||||
);
|
||||
if body.get("error").is_some() {
|
||||
ui::error(&format!(
|
||||
"Failed: {}",
|
||||
body["error"].as_str().unwrap_or("?")
|
||||
));
|
||||
} else {
|
||||
ui::success(&format!("Dependencies installed for hand '{id}'."));
|
||||
if let Some(results) = body.get("results") {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(results).unwrap_or_default()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_hand_pause(id: &str) {
|
||||
let base = require_daemon("hand pause");
|
||||
let client = daemon_client();
|
||||
let body = daemon_json(
|
||||
client
|
||||
.post(format!("{base}/api/hands/instances/{id}/pause"))
|
||||
.send(),
|
||||
);
|
||||
if body.get("error").is_some() {
|
||||
ui::error(&format!(
|
||||
"Failed: {}",
|
||||
body["error"].as_str().unwrap_or("?")
|
||||
));
|
||||
} else {
|
||||
ui::success(&format!("Hand instance '{id}' paused."));
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_hand_resume(id: &str) {
|
||||
let base = require_daemon("hand resume");
|
||||
let client = daemon_client();
|
||||
let body = daemon_json(
|
||||
client
|
||||
.post(format!("{base}/api/hands/instances/{id}/resume"))
|
||||
.send(),
|
||||
);
|
||||
if body.get("error").is_some() {
|
||||
ui::error(&format!(
|
||||
"Failed: {}",
|
||||
body["error"].as_str().unwrap_or("?")
|
||||
));
|
||||
} else {
|
||||
ui::success(&format!("Hand instance '{id}' resumed."));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider / API key helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -5149,16 +5258,40 @@ fn cmd_cron_list(json: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_cron_create(agent: &str, spec: &str, prompt: &str) {
|
||||
fn cmd_cron_create(agent: &str, spec: &str, prompt: &str, explicit_name: Option<&str>) {
|
||||
let base = require_daemon("cron create");
|
||||
let client = daemon_client();
|
||||
|
||||
// Use explicit name if provided, otherwise derive from agent + prompt
|
||||
let name = if let Some(n) = explicit_name {
|
||||
n.to_string()
|
||||
} else {
|
||||
let short_prompt: String = prompt
|
||||
.split_whitespace()
|
||||
.take(4)
|
||||
.collect::<Vec<_>>()
|
||||
.join("-")
|
||||
.chars()
|
||||
.filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
|
||||
.take(64)
|
||||
.collect();
|
||||
format!("{}-{}", agent, if short_prompt.is_empty() { "job" } else { &short_prompt })
|
||||
};
|
||||
|
||||
let body = daemon_json(
|
||||
client
|
||||
.post(format!("{base}/api/cron/jobs"))
|
||||
.json(&serde_json::json!({
|
||||
"agent_id": agent,
|
||||
"cron_expr": spec,
|
||||
"prompt": prompt,
|
||||
"name": name,
|
||||
"schedule": {
|
||||
"kind": "cron",
|
||||
"expr": spec
|
||||
},
|
||||
"action": {
|
||||
"kind": "agent_turn",
|
||||
"message": prompt
|
||||
}
|
||||
}))
|
||||
.send(),
|
||||
);
|
||||
|
||||
@@ -532,7 +532,11 @@ impl OpenFangKernel {
|
||||
let driver_config = DriverConfig {
|
||||
provider: config.default_model.provider.clone(),
|
||||
api_key: std::env::var(&config.default_model.api_key_env).ok(),
|
||||
base_url: config.default_model.base_url.clone(),
|
||||
base_url: config
|
||||
.default_model
|
||||
.base_url
|
||||
.clone()
|
||||
.or_else(|| config.provider_urls.get(&config.default_model.provider).cloned()),
|
||||
};
|
||||
let primary_driver = drivers::create_driver(&driver_config)
|
||||
.map_err(|e| KernelError::BootFailed(format!("LLM driver init failed: {e}")))?;
|
||||
@@ -548,7 +552,10 @@ impl OpenFangKernel {
|
||||
} else {
|
||||
std::env::var(&fb.api_key_env).ok()
|
||||
},
|
||||
base_url: fb.base_url.clone(),
|
||||
base_url: fb
|
||||
.base_url
|
||||
.clone()
|
||||
.or_else(|| config.provider_urls.get(&fb.provider).cloned()),
|
||||
};
|
||||
match drivers::create_driver(&fb_config) {
|
||||
Ok(d) => {
|
||||
@@ -3698,10 +3705,14 @@ impl OpenFangKernel {
|
||||
let base_url = if has_custom_url {
|
||||
manifest.model.base_url.clone()
|
||||
} else if agent_provider == default_provider {
|
||||
self.config.default_model.base_url.clone()
|
||||
self.config
|
||||
.default_model
|
||||
.base_url
|
||||
.clone()
|
||||
.or_else(|| self.config.provider_urls.get(agent_provider.as_str()).cloned())
|
||||
} else {
|
||||
// Let create_driver() use the target provider's default base URL
|
||||
None
|
||||
// Check provider_urls before falling back to hardcoded defaults
|
||||
self.config.provider_urls.get(agent_provider.as_str()).cloned()
|
||||
};
|
||||
|
||||
let driver_config = DriverConfig {
|
||||
@@ -3727,7 +3738,10 @@ impl OpenFangKernel {
|
||||
.api_key_env
|
||||
.as_ref()
|
||||
.and_then(|env| std::env::var(env).ok()),
|
||||
base_url: fb.base_url.clone(),
|
||||
base_url: fb
|
||||
.base_url
|
||||
.clone()
|
||||
.or_else(|| self.config.provider_urls.get(&fb.provider).cloned()),
|
||||
};
|
||||
match drivers::create_driver(&config) {
|
||||
Ok(d) => chain.push((d, fb.model.clone())),
|
||||
|
||||
@@ -33,7 +33,12 @@ impl OpenAIDriver {
|
||||
struct OaiRequest {
|
||||
model: String,
|
||||
messages: Vec<OaiMessage>,
|
||||
max_tokens: u32,
|
||||
/// Classic token limit field (used by most models).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_tokens: Option<u32>,
|
||||
/// New token limit field required by GPT-5 and o-series reasoning models.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_completion_tokens: Option<u32>,
|
||||
temperature: f32,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
tools: Vec<OaiTool>,
|
||||
@@ -43,6 +48,16 @@ struct OaiRequest {
|
||||
stream: bool,
|
||||
}
|
||||
|
||||
/// Returns true if a model uses `max_completion_tokens` instead of `max_tokens`.
|
||||
fn uses_completion_tokens(model: &str) -> bool {
|
||||
let m = model.to_lowercase();
|
||||
m.starts_with("gpt-5")
|
||||
|| m.starts_with("gpt5")
|
||||
|| m.starts_with("o1")
|
||||
|| m.starts_with("o3")
|
||||
|| m.starts_with("o4")
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct OaiMessage {
|
||||
role: String,
|
||||
@@ -276,10 +291,16 @@ impl LlmDriver for OpenAIDriver {
|
||||
Some(serde_json::json!("auto"))
|
||||
};
|
||||
|
||||
let (mt, mct) = if uses_completion_tokens(&request.model) {
|
||||
(None, Some(request.max_tokens))
|
||||
} else {
|
||||
(Some(request.max_tokens), None)
|
||||
};
|
||||
let mut oai_request = OaiRequest {
|
||||
model: request.model.clone(),
|
||||
messages: oai_messages,
|
||||
max_tokens: request.max_tokens,
|
||||
max_tokens: mt,
|
||||
max_completion_tokens: mct,
|
||||
temperature: request.temperature,
|
||||
tools: oai_tools,
|
||||
tool_choice,
|
||||
@@ -339,16 +360,31 @@ impl LlmDriver for OpenAIDriver {
|
||||
}
|
||||
}
|
||||
|
||||
// GPT-5 / o-series: switch from max_tokens to max_completion_tokens
|
||||
if status == 400
|
||||
&& body.contains("max_tokens")
|
||||
&& (body.contains("unsupported_parameter")
|
||||
|| body.contains("max_completion_tokens"))
|
||||
&& oai_request.max_tokens.is_some()
|
||||
&& attempt < max_retries
|
||||
{
|
||||
let val = oai_request.max_tokens.unwrap();
|
||||
warn!(model = %oai_request.model, "Switching to max_completion_tokens for this model");
|
||||
oai_request.max_tokens = None;
|
||||
oai_request.max_completion_tokens = Some(val);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Auto-cap max_tokens when model rejects our value (e.g. Groq Maverick limit 8192)
|
||||
if status == 400 && body.contains("max_tokens") && attempt < max_retries {
|
||||
// Extract the limit from error: "must be less than or equal to `8192`"
|
||||
let cap = extract_max_tokens_limit(&body).unwrap_or(oai_request.max_tokens / 2);
|
||||
warn!(
|
||||
old = oai_request.max_tokens,
|
||||
new = cap,
|
||||
"Auto-capping max_tokens to model limit"
|
||||
);
|
||||
oai_request.max_tokens = cap;
|
||||
let current = oai_request.max_tokens.or(oai_request.max_completion_tokens).unwrap_or(4096);
|
||||
let cap = extract_max_tokens_limit(&body).unwrap_or(current / 2);
|
||||
warn!(old = current, new = cap, "Auto-capping max_tokens to model limit");
|
||||
if oai_request.max_completion_tokens.is_some() {
|
||||
oai_request.max_completion_tokens = Some(cap);
|
||||
} else {
|
||||
oai_request.max_tokens = Some(cap);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -555,10 +591,16 @@ impl LlmDriver for OpenAIDriver {
|
||||
Some(serde_json::json!("auto"))
|
||||
};
|
||||
|
||||
let (mt, mct) = if uses_completion_tokens(&request.model) {
|
||||
(None, Some(request.max_tokens))
|
||||
} else {
|
||||
(Some(request.max_tokens), None)
|
||||
};
|
||||
let mut oai_request = OaiRequest {
|
||||
model: request.model.clone(),
|
||||
messages: oai_messages,
|
||||
max_tokens: request.max_tokens,
|
||||
max_tokens: mt,
|
||||
max_completion_tokens: mct,
|
||||
temperature: request.temperature,
|
||||
tools: oai_tools,
|
||||
tool_choice,
|
||||
@@ -620,15 +662,31 @@ impl LlmDriver for OpenAIDriver {
|
||||
}
|
||||
}
|
||||
|
||||
// GPT-5 / o-series: switch from max_tokens to max_completion_tokens
|
||||
if status == 400
|
||||
&& body.contains("max_tokens")
|
||||
&& (body.contains("unsupported_parameter")
|
||||
|| body.contains("max_completion_tokens"))
|
||||
&& oai_request.max_tokens.is_some()
|
||||
&& attempt < max_retries
|
||||
{
|
||||
let val = oai_request.max_tokens.unwrap();
|
||||
warn!(model = %oai_request.model, "Switching to max_completion_tokens for this model (stream)");
|
||||
oai_request.max_tokens = None;
|
||||
oai_request.max_completion_tokens = Some(val);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Auto-cap max_tokens when model rejects our value
|
||||
if status == 400 && body.contains("max_tokens") && attempt < max_retries {
|
||||
let cap = extract_max_tokens_limit(&body).unwrap_or(oai_request.max_tokens / 2);
|
||||
warn!(
|
||||
old = oai_request.max_tokens,
|
||||
new = cap,
|
||||
"Auto-capping max_tokens (stream)"
|
||||
);
|
||||
oai_request.max_tokens = cap;
|
||||
let current = oai_request.max_tokens.or(oai_request.max_completion_tokens).unwrap_or(4096);
|
||||
let cap = extract_max_tokens_limit(&body).unwrap_or(current / 2);
|
||||
warn!(old = current, new = cap, "Auto-capping max_tokens (stream)");
|
||||
if oai_request.max_completion_tokens.is_some() {
|
||||
oai_request.max_completion_tokens = Some(cap);
|
||||
} else {
|
||||
oai_request.max_tokens = Some(cap);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1556,7 +1556,7 @@ pub struct DiscordConfig {
|
||||
pub allowed_guilds: Vec<String>,
|
||||
/// Default agent name to route messages to.
|
||||
pub default_agent: Option<String>,
|
||||
/// Gateway intents bitmask (default: 33280 = GUILD_MESSAGES | MESSAGE_CONTENT).
|
||||
/// Gateway intents bitmask (default: 37376 = GUILD_MESSAGES | DIRECT_MESSAGES | MESSAGE_CONTENT).
|
||||
pub intents: u64,
|
||||
/// Per-channel behavior overrides.
|
||||
#[serde(default)]
|
||||
@@ -1569,7 +1569,7 @@ impl Default for DiscordConfig {
|
||||
bot_token_env: "DISCORD_BOT_TOKEN".to_string(),
|
||||
allowed_guilds: vec![],
|
||||
default_agent: None,
|
||||
intents: 33280,
|
||||
intents: 37376,
|
||||
overrides: ChannelOverrides::default(),
|
||||
}
|
||||
}
|
||||
@@ -3226,7 +3226,7 @@ mod tests {
|
||||
let dc = DiscordConfig::default();
|
||||
assert_eq!(dc.bot_token_env, "DISCORD_BOT_TOKEN");
|
||||
assert!(dc.allowed_guilds.is_empty());
|
||||
assert_eq!(dc.intents, 33280);
|
||||
assert_eq!(dc.intents, 37376);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user