Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions providers.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,14 @@
"aliases": [
"open_router"
],
"protocol": "open_ai_completions",
"default_base_url": "https://openrouter.ai/api/v1",
"protocol": "open_router",
"default_base_url": "",
"api_key_env": "OPENROUTER_API_KEY",
"api_key_required": true,
"model_env": "OPENROUTER_MODEL",
Comment on lines +127 to 131

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3a0b3a5 — providers.json now exposes "extra_headers_env": "OPENROUTER_EXTRA_HEADERS". Users can now configure HTTP-Referer:…,X-Title:… for the built-in openrouter backend.

"extra_headers_env": "OPENROUTER_EXTRA_HEADERS",
"default_model": "openai/gpt-4o",
"description": "OpenRouter multi-provider gateway (200+ models)",
"description": "OpenRouter multi-provider gateway (200+ models, preserves reasoning across turns)",
"setup": {
"kind": "api_key",
"secret_name": "llm_openrouter_api_key",
Expand Down Expand Up @@ -246,13 +247,13 @@
"aliases": [
"deep_seek"
],
"protocol": "open_ai_completions",
"default_base_url": "https://api.deepseek.com/v1",
"protocol": "deep_seek",
"default_base_url": "",
"api_key_env": "DEEPSEEK_API_KEY",
"api_key_required": true,
"model_env": "DEEPSEEK_MODEL",
"default_model": "deepseek-chat",
"description": "DeepSeek inference API",
"description": "DeepSeek inference API (preserves reasoning_content for thinking-mode models)",
"setup": {
"kind": "api_key",
"secret_name": "llm_deepseek_api_key",
Expand Down Expand Up @@ -325,19 +326,19 @@
"google_gemini",
"google"
],
"protocol": "open_ai_completions",
"default_base_url": "https://generativelanguage.googleapis.com/v1beta/openai",
"protocol": "gemini",
"default_base_url": "",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium Severity

Gemini is still advertised as list-models capable, but the UI/setup list-models paths do not speak native Gemini.

This registry entry now exposes adapter gemini with an empty default base URL while keeping can_list_models: true. The web configure surface shows the fetch button, then blocks on a missing base URL before calling /api/llm/list_models; if a base URL is supplied, the handler falls through to generic GET {base}/models with Bearer auth rather than Gemini's native /v1beta/models?key=... shape. The setup wizard has the same issue: it falls through to fetch_openai_compatible_models(def.default_base_url.unwrap_or("")), which returns no models for the new empty default. So users configuring Gemini are told model listing is supported, but it reliably fails/falls back to manual entry.

Please either add native Gemini model-listing support in the web handler/setup wizard or set can_list_models to false for Gemini until that path exists. Add a test covering adapter: "gemini" with empty/native default base URL.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3a0b3a5 by setting "can_list_models": false on Gemini in providers.json. The setup wizard's fall-through path (fetch_openai_compatible_models(def.default_base_url.unwrap_or(""), …)) wouldn't speak the native /v1beta/models?key=… shape, and the web list-models handler is the same. Native Gemini list-models support is filed as a follow-up rather than blocking this PR.

"api_key_env": "GEMINI_API_KEY",
"api_key_required": true,
"model_env": "GEMINI_MODEL",
"default_model": "gemini-2.5-flash",
"description": "Google Gemini (via OpenAI-compatible endpoint)",
"description": "Google Gemini native API (preserves thought_signature on tool calls)",
"setup": {
"kind": "api_key",
"secret_name": "llm_gemini_api_key",
"key_url": "https://aistudio.google.com/app/apikey",
"display_name": "Google Gemini",
"can_list_models": true
"can_list_models": false
}
},
{
Expand Down
1 change: 1 addition & 0 deletions src/agent/agent_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2223,6 +2223,7 @@ mod tests {
finish_reason: FinishReason::Stop,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
reasoning: None,
})
}
}
Expand Down
25 changes: 24 additions & 1 deletion src/agent/agentic_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ pub trait LoopDelegate: Send + Sync {
tool_calls: Vec<crate::llm::ToolCall>,
content: Option<String>,
reason_ctx: &mut ReasoningContext,
reasoning: Option<String>,
) -> Result<Option<LoopOutcome>, Error>;

/// Called when the LLM expresses tool intent without actually calling a tool.
Expand Down Expand Up @@ -253,6 +254,7 @@ pub async fn run_agentic_loop(
RespondResult::ToolCalls {
tool_calls,
content,
reasoning: _,
} => {
let names: Vec<&str> = tool_calls.iter().map(|tc| tc.name.as_str()).collect();
tracing::debug!(
Expand Down Expand Up @@ -307,6 +309,7 @@ pub async fn run_agentic_loop(
RespondResult::ToolCalls {
tool_calls,
content,
reasoning,
} => {
// If the response was truncated, tool call parameters are likely
// incomplete. Discard them and tell the LLM to try a different
Expand Down Expand Up @@ -345,7 +348,7 @@ pub async fn run_agentic_loop(
reason_ctx.last_tool_batch_all_failed = false;

if let Some(outcome) = delegate
.execute_tool_calls(tool_calls, content, reason_ctx)
.execute_tool_calls(tool_calls, content, reason_ctx, reasoning)
.await?
{
return Ok(outcome);
Expand Down Expand Up @@ -434,6 +437,7 @@ mod tests {
result: RespondResult::ToolCalls {
tool_calls: calls,
content: None,
reasoning: None,
},
usage: zero_usage(),
finish_reason: FinishReason::ToolUse,
Expand Down Expand Up @@ -529,6 +533,7 @@ mod tests {
_tool_calls: Vec<ToolCall>,
_content: Option<String>,
reason_ctx: &mut ReasoningContext,
_reasoning: Option<String>,
) -> Result<Option<LoopOutcome>, crate::error::Error> {
self.tool_exec_count.fetch_add(1, Ordering::SeqCst);
reason_ctx
Expand Down Expand Up @@ -577,6 +582,7 @@ mod tests {
name: "echo".to_string(),
arguments: serde_json::json!({}),
reasoning: None,
signature: None,
};
let delegate = MockDelegate::new(vec![
tool_calls_output(vec![tool_call]),
Expand Down Expand Up @@ -688,6 +694,7 @@ mod tests {
_: Vec<ToolCall>,
_: Option<String>,
_: &mut ReasoningContext,
_: Option<String>,
) -> Result<Option<LoopOutcome>, crate::error::Error> {
Ok(None)
}
Expand Down Expand Up @@ -748,6 +755,7 @@ mod tests {
_: Vec<ToolCall>,
_: Option<String>,
_: &mut ReasoningContext,
_: Option<String>,
) -> Result<Option<LoopOutcome>, crate::error::Error> {
Ok(None)
}
Expand Down Expand Up @@ -866,11 +874,13 @@ mod tests {
name: "memory_write".to_string(),
arguments: serde_json::json!({}), // empty — truncated
reasoning: None,
signature: None,
};
let truncated_output = RespondOutput {
result: RespondResult::ToolCalls {
tool_calls: vec![truncated_tool_call],
content: Some("I'll write the report.".to_string()),
reasoning: None,
},
usage: zero_usage(),
finish_reason: FinishReason::Length, // response was truncated
Expand Down Expand Up @@ -918,8 +928,10 @@ mod tests {
name: "memory_write".to_string(),
arguments: serde_json::json!({}),
reasoning: None,
signature: None,
}],
content: None,
reasoning: None,
},
usage: zero_usage(),
finish_reason: FinishReason::Length,
Expand Down Expand Up @@ -962,6 +974,7 @@ mod tests {
name: "echo".into(),
arguments: serde_json::json!({"msg": "hi"}),
reasoning: None,
signature: None,
}];
let fp = DuplicateToolCallTracker::fingerprint(&calls);
// Tool succeeded — count stays at 0
Expand All @@ -977,6 +990,7 @@ mod tests {
name: "http_get".into(),
arguments: serde_json::json!({"url": "https://example.com"}),
reasoning: None,
signature: None,
}];
let fp = DuplicateToolCallTracker::fingerprint(&calls);
assert_eq!(tracker.record_with_fingerprint(fp, true), 1);
Expand All @@ -992,6 +1006,7 @@ mod tests {
name: "http_get".into(),
arguments: serde_json::json!({"url": "https://example.com"}),
reasoning: None,
signature: None,
}];
let fp = DuplicateToolCallTracker::fingerprint(&calls);
assert_eq!(tracker.record_with_fingerprint(fp, true), 1);
Expand All @@ -1010,12 +1025,14 @@ mod tests {
name: "http_get".into(),
arguments: serde_json::json!({"url": "https://a.com"}),
reasoning: None,
signature: None,
}];
let calls_b = vec![ToolCall {
id: "c1".into(),
name: "http_get".into(),
arguments: serde_json::json!({"url": "https://b.com"}),
reasoning: None,
signature: None,
}];
let fp_a = DuplicateToolCallTracker::fingerprint(&calls_a);
let fp_b = DuplicateToolCallTracker::fingerprint(&calls_b);
Expand All @@ -1033,12 +1050,14 @@ mod tests {
name: "echo".into(),
arguments: serde_json::json!({"a": 1, "b": 2}),
reasoning: None,
signature: None,
}];
let calls_b = vec![ToolCall {
id: "c1".into(),
name: "echo".into(),
arguments: serde_json::json!({"b": 2, "a": 1}),
reasoning: None,
signature: None,
}];
assert_eq!(
DuplicateToolCallTracker::fingerprint(&calls_a),
Expand All @@ -1055,6 +1074,7 @@ mod tests {
name: "http_get".to_string(),
arguments: serde_json::json!({"url": "https://broken.example.com"}),
reasoning: None,
signature: None,
};
// 3 identical failing tool calls, then text response
let mut delegate = MockDelegate::new(vec![
Expand Down Expand Up @@ -1103,6 +1123,7 @@ mod tests {
name: "http_get".to_string(),
arguments: serde_json::json!({"url": "https://broken.example.com"}),
reasoning: None,
signature: None,
};
// 5 identical failing tool calls, then text response
let mut delegate = MockDelegate::new(vec![
Expand Down Expand Up @@ -1140,6 +1161,7 @@ mod tests {
name: "http_get".to_string(),
arguments: serde_json::json!({"url": "https://broken.example.com"}),
reasoning: None,
signature: None,
};
// 2 failing calls, then a text continuation, then 2 more of the same failing calls
// The text response in the middle should reset the streak, so we never hit 3.
Expand Down Expand Up @@ -1193,6 +1215,7 @@ mod tests {
_: Vec<ToolCall>,
_: Option<String>,
reason_ctx: &mut ReasoningContext,
_reasoning: Option<String>,
) -> Result<Option<LoopOutcome>, crate::error::Error> {
self.tool_exec_count.fetch_add(1, Ordering::SeqCst);
reason_ctx.messages.push(ChatMessage::user("tool error"));
Expand Down
Loading
Loading