mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-30 16:19:21 +00:00
Merge remote-tracking branch 'origin/staging' into refactor/architectural-hardening
# Conflicts: # src/agent/routine.rs
This commit is contained in:
+5
-2
@@ -176,8 +176,11 @@ impl LlmProvider for BedrockProvider {
|
||||
builder = builder.tool_config(tc);
|
||||
}
|
||||
|
||||
if let Some(config) = build_inference_config(request.temperature, request.max_tokens, None)
|
||||
{
|
||||
if let Some(config) = build_inference_config(
|
||||
request.temperature,
|
||||
request.max_tokens,
|
||||
request.stop_sequences.as_deref(),
|
||||
) {
|
||||
builder = builder.inference_config(config);
|
||||
}
|
||||
|
||||
|
||||
@@ -163,3 +163,42 @@ pub struct NearAiConfig {
|
||||
/// Enable cascade mode for smart routing. Default: true.
|
||||
pub smart_routing_cascade: bool,
|
||||
}
|
||||
|
||||
impl NearAiConfig {
|
||||
/// Create a minimal config suitable for listing available models.
|
||||
///
|
||||
/// Reads `NEARAI_API_KEY` from the environment and selects the
|
||||
/// appropriate base URL (cloud-api when API key is present,
|
||||
/// private.near.ai for session-token auth).
|
||||
pub(crate) fn for_model_discovery() -> Self {
|
||||
let api_key = std::env::var("NEARAI_API_KEY")
|
||||
.ok()
|
||||
.filter(|k| !k.is_empty())
|
||||
.map(SecretString::from);
|
||||
|
||||
let default_base = if api_key.is_some() {
|
||||
"https://cloud-api.near.ai"
|
||||
} else {
|
||||
"https://private.near.ai"
|
||||
};
|
||||
let base_url =
|
||||
std::env::var("NEARAI_BASE_URL").unwrap_or_else(|_| default_base.to_string());
|
||||
|
||||
Self {
|
||||
model: String::new(),
|
||||
cheap_model: None,
|
||||
base_url,
|
||||
api_key,
|
||||
fallback_model: None,
|
||||
max_retries: 3,
|
||||
circuit_breaker_threshold: None,
|
||||
circuit_breaker_recovery_secs: 30,
|
||||
response_cache_enabled: false,
|
||||
response_cache_ttl_secs: 3600,
|
||||
response_cache_max_entries: 1000,
|
||||
failover_cooldown_secs: 300,
|
||||
failover_cooldown_threshold: 3,
|
||||
smart_routing_cascade: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ pub mod session;
|
||||
pub mod smart_routing;
|
||||
|
||||
pub mod image_models;
|
||||
pub mod models;
|
||||
pub mod reasoning_models;
|
||||
pub mod vision_models;
|
||||
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
//! Model discovery and fetching for multiple LLM providers.
|
||||
|
||||
/// Fetch models from the Anthropic API.
|
||||
///
|
||||
/// Returns `(model_id, display_label)` pairs. Falls back to static defaults on error.
|
||||
pub(crate) async fn fetch_anthropic_models(cached_key: Option<&str>) -> Vec<(String, String)> {
|
||||
let static_defaults = vec![
|
||||
(
|
||||
"claude-opus-4-6".into(),
|
||||
"Claude Opus 4.6 (latest flagship)".into(),
|
||||
),
|
||||
("claude-sonnet-4-6".into(), "Claude Sonnet 4.6".into()),
|
||||
("claude-opus-4-5".into(), "Claude Opus 4.5".into()),
|
||||
("claude-sonnet-4-5".into(), "Claude Sonnet 4.5".into()),
|
||||
("claude-haiku-4-5".into(), "Claude Haiku 4.5 (fast)".into()),
|
||||
];
|
||||
|
||||
let api_key = cached_key
|
||||
.map(String::from)
|
||||
.or_else(|| std::env::var("ANTHROPIC_API_KEY").ok())
|
||||
.filter(|k| !k.is_empty() && k != crate::config::OAUTH_PLACEHOLDER);
|
||||
|
||||
// Fall back to OAuth token if no API key
|
||||
let oauth_token = if api_key.is_none() {
|
||||
crate::config::helpers::optional_env("ANTHROPIC_OAUTH_TOKEN")
|
||||
.ok()
|
||||
.flatten()
|
||||
.filter(|t| !t.is_empty())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (key_or_token, is_oauth) = match (api_key, oauth_token) {
|
||||
(Some(k), _) => (k, false),
|
||||
(None, Some(t)) => (t, true),
|
||||
(None, None) => return static_defaults,
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let mut request = client
|
||||
.get("https://api.anthropic.com/v1/models")
|
||||
.header("anthropic-version", "2023-06-01")
|
||||
.timeout(std::time::Duration::from_secs(5));
|
||||
|
||||
if is_oauth {
|
||||
request = request
|
||||
.bearer_auth(&key_or_token)
|
||||
.header("anthropic-beta", "oauth-2025-04-20");
|
||||
} else {
|
||||
request = request.header("x-api-key", &key_or_token);
|
||||
}
|
||||
|
||||
let resp = match request.send().await {
|
||||
Ok(r) if r.status().is_success() => r,
|
||||
_ => return static_defaults,
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ModelEntry {
|
||||
id: String,
|
||||
}
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ModelsResponse {
|
||||
data: Vec<ModelEntry>,
|
||||
}
|
||||
|
||||
match resp.json::<ModelsResponse>().await {
|
||||
Ok(body) => {
|
||||
let mut models: Vec<(String, String)> = body
|
||||
.data
|
||||
.into_iter()
|
||||
.filter(|m| !m.id.contains("embedding") && !m.id.contains("audio"))
|
||||
.map(|m| {
|
||||
let label = m.id.clone();
|
||||
(m.id, label)
|
||||
})
|
||||
.collect();
|
||||
if models.is_empty() {
|
||||
return static_defaults;
|
||||
}
|
||||
models.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
models
|
||||
}
|
||||
Err(_) => static_defaults,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch models from the OpenAI API.
|
||||
///
|
||||
/// Returns `(model_id, display_label)` pairs. Falls back to static defaults on error.
|
||||
pub(crate) async fn fetch_openai_models(cached_key: Option<&str>) -> Vec<(String, String)> {
|
||||
let static_defaults = vec![
|
||||
(
|
||||
"gpt-5.3-codex".into(),
|
||||
"GPT-5.3 Codex (latest flagship)".into(),
|
||||
),
|
||||
("gpt-5.2-codex".into(), "GPT-5.2 Codex".into()),
|
||||
("gpt-5.2".into(), "GPT-5.2".into()),
|
||||
(
|
||||
"gpt-5.1-codex-mini".into(),
|
||||
"GPT-5.1 Codex Mini (fast)".into(),
|
||||
),
|
||||
("gpt-5".into(), "GPT-5".into()),
|
||||
("gpt-5-mini".into(), "GPT-5 Mini".into()),
|
||||
("gpt-4.1".into(), "GPT-4.1".into()),
|
||||
("gpt-4.1-mini".into(), "GPT-4.1 Mini".into()),
|
||||
("o4-mini".into(), "o4-mini (fast reasoning)".into()),
|
||||
("o3".into(), "o3 (reasoning)".into()),
|
||||
];
|
||||
|
||||
let api_key = cached_key
|
||||
.map(String::from)
|
||||
.or_else(|| std::env::var("OPENAI_API_KEY").ok())
|
||||
.filter(|k| !k.is_empty());
|
||||
|
||||
let api_key = match api_key {
|
||||
Some(k) => k,
|
||||
None => return static_defaults,
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let resp = match client
|
||||
.get("https://api.openai.com/v1/models")
|
||||
.bearer_auth(&api_key)
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(r) if r.status().is_success() => r,
|
||||
_ => return static_defaults,
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ModelEntry {
|
||||
id: String,
|
||||
}
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ModelsResponse {
|
||||
data: Vec<ModelEntry>,
|
||||
}
|
||||
|
||||
match resp.json::<ModelsResponse>().await {
|
||||
Ok(body) => {
|
||||
let mut models: Vec<(String, String)> = body
|
||||
.data
|
||||
.into_iter()
|
||||
.filter(|m| is_openai_chat_model(&m.id))
|
||||
.map(|m| {
|
||||
let label = m.id.clone();
|
||||
(m.id, label)
|
||||
})
|
||||
.collect();
|
||||
if models.is_empty() {
|
||||
return static_defaults;
|
||||
}
|
||||
sort_openai_models(&mut models);
|
||||
models
|
||||
}
|
||||
Err(_) => static_defaults,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_openai_chat_model(model_id: &str) -> bool {
|
||||
let id = model_id.to_ascii_lowercase();
|
||||
|
||||
let is_chat_family = id.starts_with("gpt-")
|
||||
|| id.starts_with("chatgpt-")
|
||||
|| id.starts_with("o1")
|
||||
|| id.starts_with("o3")
|
||||
|| id.starts_with("o4")
|
||||
|| id.starts_with("o5");
|
||||
|
||||
let is_non_chat_variant = id.contains("realtime")
|
||||
|| id.contains("audio")
|
||||
|| id.contains("transcribe")
|
||||
|| id.contains("tts")
|
||||
|| id.contains("embedding")
|
||||
|| id.contains("moderation")
|
||||
|| id.contains("image");
|
||||
|
||||
is_chat_family && !is_non_chat_variant
|
||||
}
|
||||
|
||||
pub(crate) fn openai_model_priority(model_id: &str) -> usize {
|
||||
let id = model_id.to_ascii_lowercase();
|
||||
|
||||
const EXACT_PRIORITY: &[&str] = &[
|
||||
"gpt-5.3-codex",
|
||||
"gpt-5.2-codex",
|
||||
"gpt-5.2",
|
||||
"gpt-5.1-codex-mini",
|
||||
"gpt-5",
|
||||
"gpt-5-mini",
|
||||
"gpt-5-nano",
|
||||
"o4-mini",
|
||||
"o3",
|
||||
"o1",
|
||||
"gpt-4.1",
|
||||
"gpt-4.1-mini",
|
||||
"gpt-4o",
|
||||
"gpt-4o-mini",
|
||||
];
|
||||
if let Some(pos) = EXACT_PRIORITY.iter().position(|m| id == *m) {
|
||||
return pos;
|
||||
}
|
||||
|
||||
const PREFIX_PRIORITY: &[&str] = &[
|
||||
"gpt-5.", "gpt-5-", "o3-", "o4-", "o1-", "gpt-4.1-", "gpt-4o-", "gpt-3.5-", "chatgpt-",
|
||||
];
|
||||
if let Some(pos) = PREFIX_PRIORITY
|
||||
.iter()
|
||||
.position(|prefix| id.starts_with(prefix))
|
||||
{
|
||||
return EXACT_PRIORITY.len() + pos;
|
||||
}
|
||||
|
||||
EXACT_PRIORITY.len() + PREFIX_PRIORITY.len() + 1
|
||||
}
|
||||
|
||||
pub(crate) fn sort_openai_models(models: &mut [(String, String)]) {
|
||||
models.sort_by(|a, b| {
|
||||
openai_model_priority(&a.0)
|
||||
.cmp(&openai_model_priority(&b.0))
|
||||
.then_with(|| a.0.cmp(&b.0))
|
||||
});
|
||||
}
|
||||
|
||||
/// Fetch installed models from a local Ollama instance.
|
||||
///
|
||||
/// Returns `(model_name, display_label)` pairs. Falls back to static defaults on error.
|
||||
pub(crate) async fn fetch_ollama_models(base_url: &str) -> Vec<(String, String)> {
|
||||
let static_defaults = vec![
|
||||
("llama3".into(), "llama3".into()),
|
||||
("mistral".into(), "mistral".into()),
|
||||
("codellama".into(), "codellama".into()),
|
||||
];
|
||||
|
||||
let url = format!("{}/api/tags", base_url.trim_end_matches('/'));
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let resp = match client
|
||||
.get(&url)
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(r) if r.status().is_success() => r,
|
||||
Ok(_) => return static_defaults,
|
||||
Err(_) => {
|
||||
tracing::warn!(
|
||||
"Could not connect to Ollama at {base_url}. Is it running? Using static defaults."
|
||||
);
|
||||
return static_defaults;
|
||||
}
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ModelEntry {
|
||||
name: String,
|
||||
}
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TagsResponse {
|
||||
models: Vec<ModelEntry>,
|
||||
}
|
||||
|
||||
match resp.json::<TagsResponse>().await {
|
||||
Ok(body) => {
|
||||
let models: Vec<(String, String)> = body
|
||||
.models
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
let label = m.name.clone();
|
||||
(m.name, label)
|
||||
})
|
||||
.collect();
|
||||
if models.is_empty() {
|
||||
return static_defaults;
|
||||
}
|
||||
models
|
||||
}
|
||||
Err(_) => static_defaults,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch models from a generic OpenAI-compatible /v1/models endpoint.
|
||||
///
|
||||
/// Used for registry providers like Groq, NVIDIA NIM, etc.
|
||||
pub(crate) async fn fetch_openai_compatible_models(
|
||||
base_url: &str,
|
||||
cached_key: Option<&str>,
|
||||
) -> Vec<(String, String)> {
|
||||
if base_url.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let url = format!("{}/models", base_url.trim_end_matches('/'));
|
||||
let client = reqwest::Client::new();
|
||||
let mut req = client.get(&url).timeout(std::time::Duration::from_secs(5));
|
||||
if let Some(key) = cached_key {
|
||||
req = req.bearer_auth(key);
|
||||
}
|
||||
|
||||
let resp = match req.send().await {
|
||||
Ok(r) if r.status().is_success() => r,
|
||||
_ => return vec![],
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Model {
|
||||
id: String,
|
||||
}
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ModelsResponse {
|
||||
data: Vec<Model>,
|
||||
}
|
||||
|
||||
match resp.json::<ModelsResponse>().await {
|
||||
Ok(body) => body
|
||||
.data
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
let label = m.id.clone();
|
||||
(m.id, label)
|
||||
})
|
||||
.collect(),
|
||||
Err(_) => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the `LlmConfig` used by `fetch_nearai_models` to list available models.
|
||||
///
|
||||
/// Uses [`NearAiConfig::for_model_discovery()`] to construct a minimal NEAR AI
|
||||
/// config, then wraps it in an `LlmConfig` with session config for auth.
|
||||
pub(crate) fn build_nearai_model_fetch_config() -> crate::config::LlmConfig {
|
||||
let auth_base_url =
|
||||
std::env::var("NEARAI_AUTH_URL").unwrap_or_else(|_| "https://private.near.ai".to_string());
|
||||
|
||||
crate::config::LlmConfig {
|
||||
backend: "nearai".to_string(),
|
||||
session: crate::llm::session::SessionConfig {
|
||||
auth_base_url,
|
||||
session_path: crate::config::llm::default_session_path(),
|
||||
},
|
||||
nearai: crate::config::NearAiConfig::for_model_discovery(),
|
||||
provider: None,
|
||||
bedrock: None,
|
||||
request_timeout_secs: 120,
|
||||
}
|
||||
}
|
||||
@@ -475,6 +475,7 @@ impl LlmProvider for NearAiChatProvider {
|
||||
messages,
|
||||
temperature: req.temperature,
|
||||
max_tokens: req.max_tokens,
|
||||
stop: req.stop_sequences,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
};
|
||||
@@ -554,6 +555,7 @@ impl LlmProvider for NearAiChatProvider {
|
||||
messages,
|
||||
temperature: req.temperature,
|
||||
max_tokens: req.max_tokens,
|
||||
stop: req.stop_sequences,
|
||||
tools: if tools.is_empty() { None } else { Some(tools) },
|
||||
tool_choice: req.tool_choice,
|
||||
};
|
||||
@@ -680,6 +682,8 @@ struct ChatCompletionRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_tokens: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stop: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tools: Option<Vec<ChatCompletionTool>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tool_choice: Option<String>,
|
||||
@@ -1666,6 +1670,7 @@ mod tests {
|
||||
}],
|
||||
temperature: None,
|
||||
max_tokens: None,
|
||||
stop: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
};
|
||||
@@ -1687,6 +1692,7 @@ mod tests {
|
||||
messages: vec![],
|
||||
temperature: Some(0.7),
|
||||
max_tokens: Some(1024),
|
||||
stop: None,
|
||||
tools: Some(vec![ChatCompletionTool {
|
||||
tool_type: "function".to_string(),
|
||||
function: ChatCompletionFunction {
|
||||
|
||||
+24
-3
@@ -251,6 +251,7 @@ pub struct ToolCompletionRequest {
|
||||
pub model: Option<String>,
|
||||
pub max_tokens: Option<u32>,
|
||||
pub temperature: Option<f32>,
|
||||
pub stop_sequences: Option<Vec<String>>,
|
||||
/// How to handle tool use: "auto", "required", or "none".
|
||||
pub tool_choice: Option<String>,
|
||||
/// Opaque metadata passed through to the provider (e.g. thread_id for chaining).
|
||||
@@ -266,6 +267,7 @@ impl ToolCompletionRequest {
|
||||
model: None,
|
||||
max_tokens: None,
|
||||
temperature: None,
|
||||
stop_sequences: None,
|
||||
tool_choice: None,
|
||||
metadata: std::collections::HashMap::new(),
|
||||
}
|
||||
@@ -289,6 +291,12 @@ impl ToolCompletionRequest {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set stop sequences.
|
||||
pub fn with_stop_sequences(mut self, stop_sequences: Vec<String>) -> Self {
|
||||
self.stop_sequences = Some(stop_sequences);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set tool choice mode.
|
||||
pub fn with_tool_choice(mut self, choice: impl Into<String>) -> Self {
|
||||
self.tool_choice = Some(choice.into());
|
||||
@@ -504,8 +512,6 @@ pub fn strip_unsupported_completion_params(
|
||||
/// This is the single helper function used by all providers to remove
|
||||
/// parameters they don't support from tool calls, replacing duplicate stringly-typed logic.
|
||||
///
|
||||
/// Note: Only `Temperature` and `MaxTokens` are supported in `ToolCompletionRequest`.
|
||||
/// `StopSequences` is only available in `CompletionRequest` and is not applicable to tool calls.
|
||||
pub fn strip_unsupported_tool_params(
|
||||
unsupported: &std::collections::HashSet<String>,
|
||||
req: &mut ToolCompletionRequest,
|
||||
@@ -519,7 +525,9 @@ pub fn strip_unsupported_tool_params(
|
||||
if unsupported.contains(UnsupportedParam::MaxTokens.name()) {
|
||||
req.max_tokens = None;
|
||||
}
|
||||
// Note: StopSequences is not a field in ToolCompletionRequest, so no action needed
|
||||
if unsupported.contains(UnsupportedParam::StopSequences.name()) {
|
||||
req.stop_sequences = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -651,4 +659,17 @@ mod tests {
|
||||
assert!(messages[2].tool_call_id.is_none());
|
||||
assert!(messages[2].name.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_unsupported_tool_params_strips_stop_sequences() {
|
||||
let mut unsupported = std::collections::HashSet::new();
|
||||
unsupported.insert(UnsupportedParam::StopSequences.name().to_string());
|
||||
|
||||
let mut req = ToolCompletionRequest::new(vec![ChatMessage::user("hello")], vec![]);
|
||||
req.stop_sequences = Some(vec!["STOP".to_string()]);
|
||||
|
||||
strip_unsupported_tool_params(&unsupported, &mut req);
|
||||
|
||||
assert!(req.stop_sequences.is_none()); // safety: test assertion for explicit strip behavior
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,22 +155,22 @@ pub fn is_silent_reply(text: &str) -> bool {
|
||||
|
||||
/// Quick-check: bail early if no reasoning/final tags are present at all.
|
||||
static QUICK_TAG_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"(?i)<\s*/?\s*(?:think(?:ing)?|thought|thoughts|antthinking|reasoning|reflection|scratchpad|inner_monologue|final)\b").expect("QUICK_TAG_RE")
|
||||
Regex::new(r"(?i)<\s*/?\s*(?:think(?:ing)?|thought|thoughts|antthinking|reasoning|reflection|scratchpad|inner_monologue|final)\b").expect("QUICK_TAG_RE") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
/// Matches thinking/reasoning open and close tags. Capture group 1 is "/" for close tags.
|
||||
/// Whitespace-tolerant, case-insensitive, attribute-aware.
|
||||
static THINKING_TAG_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"(?i)<\s*(/?)\s*(?:think(?:ing)?|thought|thoughts|antthinking|reasoning|reflection|scratchpad|inner_monologue)\b[^<>]*>").expect("THINKING_TAG_RE")
|
||||
Regex::new(r"(?i)<\s*(/?)\s*(?:think(?:ing)?|thought|thoughts|antthinking|reasoning|reflection|scratchpad|inner_monologue)\b[^<>]*>").expect("THINKING_TAG_RE") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
/// Matches `<final>` / `</final>` tags. Capture group 1 is "/" for close tags.
|
||||
static FINAL_TAG_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"(?i)<\s*(/?)\s*final\b[^<>]*>").expect("FINAL_TAG_RE"));
|
||||
LazyLock::new(|| Regex::new(r"(?i)<\s*(/?)\s*final\b[^<>]*>").expect("FINAL_TAG_RE")); // safety: hardcoded literal
|
||||
|
||||
/// Matches pipe-delimited reasoning tags: `<|think|>...<|/think|>` etc.
|
||||
static PIPE_REASONING_TAG_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"(?i)<\|(/?)\s*(?:think(?:ing)?|thought|thoughts|antthinking|reasoning|reflection|scratchpad|inner_monologue)\|>").expect("PIPE_REASONING_TAG_RE")
|
||||
Regex::new(r"(?i)<\|(/?)\s*(?:think(?:ing)?|thought|thoughts|antthinking|reasoning|reflection|scratchpad|inner_monologue)\|>").expect("PIPE_REASONING_TAG_RE") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
/// Context for reasoning operations.
|
||||
|
||||
+1
-1
@@ -219,7 +219,7 @@ impl ProviderRegistry {
|
||||
pub fn load() -> Self {
|
||||
let builtins: Vec<ProviderDefinition> =
|
||||
serde_json::from_str(include_str!("../../providers.json"))
|
||||
.expect("built-in providers.json must be valid JSON");
|
||||
.expect("built-in providers.json must be valid JSON"); // safety: compile-time embedded file
|
||||
|
||||
let mut all = builtins;
|
||||
|
||||
|
||||
@@ -548,6 +548,7 @@ mod tests {
|
||||
model: None,
|
||||
max_tokens: None,
|
||||
temperature: None,
|
||||
stop_sequences: None,
|
||||
tool_choice: None,
|
||||
metadata: Default::default(),
|
||||
};
|
||||
|
||||
+22
-21
@@ -248,7 +248,7 @@ fn build_domain_regex(keywords: &[&str]) -> Regex {
|
||||
let pattern = format!(r"(?i)\b({})\b", keywords.join("|"));
|
||||
Regex::new(&pattern).unwrap_or_else(|e| {
|
||||
tracing::warn!(error = %e, "Invalid domain keywords pattern, using minimal fallback");
|
||||
Regex::new(r"(?i)\b(api|code|deploy)\b").expect("fallback regex is valid")
|
||||
Regex::new(r"(?i)\b(api|code|deploy)\b").expect("fallback regex is valid") // safety: hardcoded literal
|
||||
})
|
||||
}
|
||||
|
||||
@@ -274,71 +274,71 @@ use std::sync::LazyLock;
|
||||
static RE_REASONING: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"(?i)\b(why|how|explain|analyze|analyse|compare|contrast|evaluate|assess|reason|think|consider|implications?|consequences?|trade-?offs?|pros?\s*(and|&)\s*cons?|advantages?|disadvantages?|benefits?|drawbacks?|differs?|difference|versus|vs\.?|better|worse|optimal|best|worst)\b"
|
||||
).expect("RE_REASONING is a valid regex")
|
||||
).expect("RE_REASONING is a valid regex") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
static RE_MULTI_STEP: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"(?i)\b(first|then|next|after|before|finally|step|steps|phase|stages?|process|workflow|sequence|procedure|pipeline|chain|series|order|followed by)\b"
|
||||
).expect("RE_MULTI_STEP is a valid regex")
|
||||
).expect("RE_MULTI_STEP is a valid regex") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
static RE_CREATIVITY: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"(?i)\b(write|create|generate|compose|design|imagine|brainstorm|ideate|draft|invent|story|poem|essay|article|blog|content|narrative|script|summarize|summarise|rewrite|paraphrase|translate|adapt|tweet|post|thread|outline|structure|format|style|tone|voice)\b"
|
||||
).expect("RE_CREATIVITY is a valid regex")
|
||||
).expect("RE_CREATIVITY is a valid regex") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
static RE_PRECISION: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"(?i)\b(\d{4}|\d+\.\d+|exactly|precisely|specific|accurate|correct|verify|confirm|date|time|number|calculate|compute|measure|count)\b"
|
||||
).expect("RE_PRECISION is a valid regex")
|
||||
).expect("RE_PRECISION is a valid regex") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
static RE_CODE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"(?i)(`{1,3}|```|function|const|let|var|import|export|class|def |async|await|=>|\.ts|\.js|\.py|\.rs|\.go|\.sol|\(\)|\[\]|\{\}|<[A-Z][a-z]+>|useState|useEffect|npm|yarn|pnpm|cargo|pip|implement|rebase|merge|commit|branch|PR|pull.?request|columns?|migrations?|module|refactor|debug|fix|bug|error|schema|database|query)"
|
||||
).expect("RE_CODE is a valid regex")
|
||||
).expect("RE_CODE is a valid regex") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
static RE_TOOL: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"(?i)\b(file|read|write|search|fetch|run|execute|check|look up|find|open|save|send|post|get|download|upload|install|deploy|build|compile|test|add|update|remove|delete|modify|change|edit|create|resolve|push|pull|clone)\b"
|
||||
).expect("RE_TOOL is a valid regex")
|
||||
).expect("RE_TOOL is a valid regex") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
static RE_SAFETY: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"(?i)\b(password|secret|private|confidential|medical|legal|financial|personal|sensitive|ssn|credit.?card|auth|token|key|encrypt|decrypt|hash|vulnerability|exploit|attack|breach)\b"
|
||||
).expect("RE_SAFETY is a valid regex")
|
||||
).expect("RE_SAFETY is a valid regex") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
static RE_CONTEXT: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"(?i)\b(previous|earlier|above|before|last|that|those|it|they|we discussed|you said|mentioned|remember|recall|as I said|like I mentioned)\b"
|
||||
).expect("RE_CONTEXT is a valid regex")
|
||||
).expect("RE_CONTEXT is a valid regex") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
static RE_VAGUE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"(?i)\b(it|this|that|something|stuff|thing|things)\b")
|
||||
.expect("RE_VAGUE is a valid regex")
|
||||
.expect("RE_VAGUE is a valid regex") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
static RE_OPEN_ENDED: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"(?i)\b(why|how|what if|explain|describe|elaborate|discuss)\b")
|
||||
.expect("RE_OPEN_ENDED is a valid regex")
|
||||
.expect("RE_OPEN_ENDED is a valid regex") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
static RE_CONJUNCTIONS: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"(?i)\b(and|but|or|however|therefore|because|although|while|whereas|moreover|furthermore)\b",
|
||||
)
|
||||
.expect("RE_CONJUNCTIONS is a valid regex")
|
||||
.expect("RE_CONJUNCTIONS is a valid regex") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
static RE_TIER_HINT: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"(?i)\[tier:(flash|standard|pro|frontier)\]")
|
||||
.expect("RE_TIER_HINT is a valid regex")
|
||||
.expect("RE_TIER_HINT is a valid regex") // safety: hardcoded literal
|
||||
});
|
||||
|
||||
/// Default domain regex, compiled once from `DEFAULT_DOMAIN_KEYWORDS`.
|
||||
@@ -363,7 +363,7 @@ static DEFAULT_OVERRIDES: LazyLock<Vec<PatternOverride>> = LazyLock::new(|| {
|
||||
regex: Regex::new(
|
||||
r"(?i)^(hi|hello|hey|thanks|ok|sure|yes|no|yep|nope|cool|nice|great|got it)$",
|
||||
)
|
||||
.expect("greeting pattern is valid"),
|
||||
.expect("greeting pattern is valid"), // safety: hardcoded literal
|
||||
tier: Tier::Flash,
|
||||
},
|
||||
// Flash tier: quick lookups (end-anchored to avoid matching complex questions
|
||||
@@ -372,29 +372,29 @@ static DEFAULT_OVERRIDES: LazyLock<Vec<PatternOverride>> = LazyLock::new(|| {
|
||||
regex: Regex::new(
|
||||
r"(?i)^what(?:'s|\s+is)?\s+(?:the\s+)?(time|date|day|weather)\b(?:\s+(?:is\s+it|today|now|in\s+\S+))?[?.!]*$",
|
||||
)
|
||||
.expect("lookup pattern is valid"),
|
||||
.expect("lookup pattern is valid"), // safety: hardcoded literal
|
||||
tier: Tier::Flash,
|
||||
},
|
||||
// Frontier tier: security audits
|
||||
PatternOverride {
|
||||
regex: Regex::new(r"(?i)security.*(audit|review|scan)")
|
||||
.expect("security audit pattern is valid"),
|
||||
.expect("security audit pattern is valid"), // safety: hardcoded literal
|
||||
tier: Tier::Frontier,
|
||||
},
|
||||
PatternOverride {
|
||||
regex: Regex::new(r"(?i)vulnerabilit(y|ies).*(review|scan|check|audit)")
|
||||
.expect("vulnerability pattern is valid"),
|
||||
.expect("vulnerability pattern is valid"), // safety: hardcoded literal
|
||||
tier: Tier::Frontier,
|
||||
},
|
||||
// Pro tier: production deployments
|
||||
PatternOverride {
|
||||
regex: Regex::new(r"(?i)deploy.*(mainnet|production)")
|
||||
.expect("deploy pattern is valid"),
|
||||
.expect("deploy pattern is valid"), // safety: hardcoded literal
|
||||
tier: Tier::Pro,
|
||||
},
|
||||
PatternOverride {
|
||||
regex: Regex::new(r"(?i)production.*(deploy|release|push)")
|
||||
.expect("production pattern is valid"),
|
||||
.expect("production pattern is valid"), // safety: hardcoded literal
|
||||
tier: Tier::Pro,
|
||||
},
|
||||
]
|
||||
@@ -451,7 +451,7 @@ fn score_complexity_internal(
|
||||
|
||||
// Check for explicit tier hint (e.g. "[tier:flash]")
|
||||
if let Some(caps) = RE_TIER_HINT.captures(prompt) {
|
||||
let tier_str = caps.get(1).expect("capture group 1 exists").as_str();
|
||||
let tier_str = caps.get(1).expect("capture group 1 exists").as_str(); // safety: RE_TIER_HINT has group 1
|
||||
let tier = match tier_str.to_lowercase().as_str() {
|
||||
"flash" => Tier::Flash,
|
||||
"standard" => Tier::Standard,
|
||||
@@ -758,7 +758,8 @@ impl SmartRoutingProvider {
|
||||
|
||||
// Highest priority: explicit tier hints (e.g. "[tier:flash]")
|
||||
if let Some(caps) = RE_TIER_HINT.captures(last_user_msg) {
|
||||
let tier_str = caps.get(1).expect("capture group 1 exists").as_str();
|
||||
// SAFETY: RE_TIER_HINT has exactly one capture group; get(1) is guaranteed Some after match.
|
||||
let tier_str = caps.get(1).expect("capture group 1 exists").as_str(); // safety: RE_TIER_HINT has group 1
|
||||
let tier = match tier_str.to_lowercase().as_str() {
|
||||
"flash" => Tier::Flash,
|
||||
"standard" => Tier::Standard,
|
||||
|
||||
Reference in New Issue
Block a user