diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index 26eff653..ce23b36c 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -949,6 +949,41 @@ pub(super) fn parse_usage(usage: Option<&Value>) -> Usage { } } +/// Call the DeepSeek `/beta/completions` FIM endpoint. +/// +/// Returns the generated text (the "middle" between `prompt` and `suffix`). +pub async fn fim_completion( + &self, + model: &str, + prompt: &str, + suffix: &str, + max_tokens: u32, +) -> anyhow::Result { + let url = api_url(&self.base_url, "beta/completions"); + let body = json!({ + "model": model, + "prompt": prompt, + "suffix": suffix, + "max_tokens": max_tokens, + }); + let response = self + .send_with_retry(|| self.http_client.post(&url).json(&body)) + .await?; + let status = response.status(); + if !status.is_success() { + let error_text = bounded_error_text(response, ERROR_BODY_MAX_BYTES).await; + anyhow::bail!("FIM API error: HTTP {status}: {error_text}"); + } + let response_text = response.text().await.unwrap_or_default(); + let value: serde_json::Value = + serde_json::from_str(&response_text).context("Failed to parse FIM API response")?; + let text = value + .pointer("/choices/0/text") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| anyhow::anyhow!("FIM response missing choices[0].text"))?; + Ok(text.to_string()) +} + mod chat; mod responses; diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index a57d27d9..c7a0a6c4 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -702,6 +702,9 @@ pub struct Config { pub mcp_config_path: Option, pub notes_path: Option, pub memory_path: Option, + /// When true, set `tool_choice: "required"` in all API requests so the + /// model MUST call a tool on every step (V4 strict tool-following mode). + pub strict_tool_mode: Option, /// Additional system-prompt sources concatenated in declared order /// (#454). Paths are expanded via `expand_path` so `~` and env /// vars work. Project config overrides user config (replace, not diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index ff053451..e7a6d89f 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -140,6 +140,9 @@ pub struct EngineConfig { /// consulted when `memory_enabled` is `true`. pub memory_path: PathBuf, pub goal_objective: Option, + /// When true, force `tool_choice: "required"` so the model always calls + /// a tool on every turn step (V4 strict tool-following mode). + pub strict_tool_mode: bool, } impl Default for EngineConfig { @@ -169,6 +172,7 @@ impl Default for EngineConfig { subagent_model_overrides: HashMap::new(), memory_enabled: false, memory_path: PathBuf::from("./memory.md"), + strict_tool_mode: false, goal_objective: None, } } diff --git a/crates/tui/src/tools/fim.rs b/crates/tui/src/tools/fim.rs new file mode 100644 index 00000000..05028359 --- /dev/null +++ b/crates/tui/src/tools/fim.rs @@ -0,0 +1,18 @@ +use serde_json::{Value, json}; +use crate::tools::spec::{ToolContext, ToolError, ToolResult, ToolSpec}; + +pub struct FimEditTool; + +#[async_trait::async_trait] +impl ToolSpec for FimEditTool { + fn name(&self) -> &'static str { "fim_edit" } + fn description(&self) -> &'static str { "Fill-in-the-middle edit via DeepSeek /beta FIM endpoint" } + fn input_schema(&self) -> Value { + json!({"type":"object","properties":{"path":{"type":"string"},"prefix_anchor":{"type":"string"},"suffix_anchor":{"type":"string"}},"required":["path","prefix_anchor","suffix_anchor"]}) + } + async fn execute(&self, input: Value, _ctx: &ToolContext) -> Result { + let path = input["path"].as_str().ok_or_else(|| ToolError::invalid_input("missing path"))?; + let _ = (path, &input["prefix_anchor"], &input["suffix_anchor"]); + Ok(ToolResult::text("FIM edit stub — wire to /beta endpoint in follow-up")) + } +} diff --git a/crates/tui/src/tools/mod.rs b/crates/tui/src/tools/mod.rs index cbb0911d..d1affc11 100644 --- a/crates/tui/src/tools/mod.rs +++ b/crates/tui/src/tools/mod.rs @@ -10,6 +10,7 @@ pub mod file_search; pub mod finance; pub mod fetch_url; +pub mod fim; pub mod git; pub mod git_history; pub mod github; diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 62bc845f..17d57b98 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -541,6 +541,7 @@ fn build_engine_config(app: &App, config: &Config) -> EngineConfig { subagent_model_overrides: config.subagent_model_overrides(), memory_enabled: config.memory_enabled(), memory_path: config.memory_path(), + strict_tool_mode: config.strict_tool_mode.unwrap_or(false), goal_objective: app.goal.goal_objective.clone(), } }