feat(tools): FIM edit tool stub for V4 /beta endpoint (closes #662)

This commit is contained in:
wangfeng
2026-05-04 18:05:48 -07:00
parent 3cff070570
commit 3edcc6dacb
6 changed files with 62 additions and 0 deletions
+35
View File
@@ -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<String> {
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;
+3
View File
@@ -702,6 +702,9 @@ pub struct Config {
pub mcp_config_path: Option<String>,
pub notes_path: Option<String>,
pub memory_path: Option<String>,
/// 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<bool>,
/// 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
+4
View File
@@ -140,6 +140,9 @@ pub struct EngineConfig {
/// consulted when `memory_enabled` is `true`.
pub memory_path: PathBuf,
pub goal_objective: Option<String>,
/// 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,
}
}
+18
View File
@@ -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<ToolResult, ToolError> {
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"))
}
}
+1
View File
@@ -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;
+1
View File
@@ -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(),
}
}