replace ContainerRuntime CLI to Bollard crate (default: docker socket)
ci/woodpecker/push/tests Pipeline was successful

limit tool result to open router tool response (with truncated info for
ai)

Hard kill if graceful shutdown is too long
This commit is contained in:
qpismont committed 2026-09-17 19:51:59 +00:00
1 parent 78ad2bf701
commit 624bc1e028
16 files changed
+1327 -354

No files matched your search

+40 -8
View File
@@ -17,8 +17,24 @@ use tracing::instrument;
/// OpenRouter API root, version prefix included.
const BASE_URL: &str = "https://openrouter.ai/api/v1";
/// The model decides on its own which tool to call.
const TOOL_CHOICE_AUTO: &str = "auto";
/// What the model is allowed to do on a given turn.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolChoice {
/// The model decides whether to call a tool.
Auto,
/// The model must answer, without calling any tool. The tools stay declared,
/// so the conversation remains the same shape as on the other turns.
None,
}
impl ToolChoice {
fn as_str(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::None => "none",
}
}
}
/// Result of a completion that may contain tool calls.
pub struct ToolChatResult {
@@ -250,13 +266,18 @@ impl OpenRouterClient {
/// Sends a conversation with tool definitions and returns either a final
/// message or the tool calls requested by the model.
///
/// The conversation is borrowed rather than taken by value: the caller keeps
/// appending to it between iterations, and a copy per iteration would be pure
/// waste.
#[instrument(skip(self, messages, tools), err)]
pub async fn chat_with_tools(
&self,
messages: Vec<Message>,
tools: Vec<Tool>,
messages: &[Message],
tools: &[Tool],
tool_choice: ToolChoice,
) -> anyhow::Result<ToolChatResult> {
let response = self.complete(&messages, &tools).await?;
let response = self.complete(messages, tools, tool_choice).await?;
let cost = response.usage.and_then(|usage| usage.cost);
let message = response
@@ -273,13 +294,18 @@ impl OpenRouterClient {
})
}
async fn complete(&self, messages: &[Message], tools: &[Tool]) -> anyhow::Result<ChatResponse> {
async fn complete(
&self,
messages: &[Message],
tools: &[Tool],
tool_choice: ToolChoice,
) -> anyhow::Result<ChatResponse> {
let request = ChatRequest {
model: &self.model,
messages,
reasoning: Reasoning { enabled: true },
tools,
tool_choice: TOOL_CHOICE_AUTO,
tool_choice: tool_choice.as_str(),
};
let response = self
@@ -364,7 +390,7 @@ mod tests {
messages: &messages,
reasoning: Reasoning { enabled: true },
tools: &tools,
tool_choice: TOOL_CHOICE_AUTO,
tool_choice: ToolChoice::Auto.as_str(),
})
.unwrap();
@@ -375,6 +401,12 @@ mod tests {
assert_eq!(request["messages"][0]["role"], "user");
}
#[test]
fn a_forced_answer_turn_disables_the_tools() {
assert_eq!(ToolChoice::None.as_str(), "none");
assert_eq!(ToolChoice::Auto.as_str(), "auto");
}
#[test]
fn response_parses_tool_calls_and_cost() {
let response: ChatResponse = serde_json::from_value(json!({