Replace openrouter-rs with in-tree client and require sandbox

Remove the openrouter-rs dependency in favor of a minimal in-tree
OpenRouter chat-completions client, and drop the BOT_NAME and
SANDBOX_ENABLED config options. Reviews now always run inside the
sandbox, and the review prompt asks the model to read files with the
available tools instead of embedding the diff.
This commit is contained in:
qpismont committed 2026-09-17 14:48:42 +00:00
1 parent ee221b0954
commit 04cc172848
15 files changed
+1301 -740

No files matched your search

+126 -13
View File
@@ -1,10 +1,23 @@
use std::time::Duration;
use futures_util::stream::TryStreamExt;
use serde::Deserialize;
use serde_json::{Value, json};
use tracing::instrument;
use tokio::io::AsyncReadExt;
use tokio_util::io::StreamReader;
use tracing::{instrument, warn};
use crate::{bot::ReviewResult, errors::AppError};
use crate::{
bot::{ReviewResult, ReviewSide},
consts::MAX_DIFF_SIZE,
errors::AppError,
};
/// Page size requested when listing the files of a pull request.
const FILE_PAGE_SIZE: u64 = 50;
/// Maximum number of pages fetched for a pull request file list.
const MAX_FILE_PAGES: u64 = 10;
#[derive(Clone)]
pub struct GiteaAPI {
@@ -135,6 +148,70 @@ impl GiteaAPI {
Ok(())
}
/// Raw unified diff of a pull request.
///
/// The API endpoint is used rather than the `diff_url` carried by the
/// webhook: that one points at a web route, which is session authenticated
/// and therefore does not serve private repositories to an API token.
#[instrument(skip(self))]
pub async fn pull_request_diff(&self, full_name: &str, index: u64) -> anyhow::Result<String> {
let url = format!(
"{}/api/v1/repos/{}/pulls/{}.diff",
self.base_url, full_name, index
);
let res = self.client.get(url).send().await?;
if !res.status().is_success() {
return Err(anyhow::anyhow!(
"Failed to download pull request diff: {}",
res.status()
));
}
read_capped(res).await
}
/// Files changed by a pull request, with their exact path and status.
#[instrument(skip(self))]
pub async fn pull_request_files(
&self,
full_name: &str,
index: u64,
) -> anyhow::Result<Vec<PullRequestFile>> {
let mut files = Vec::new();
for page in 1..=MAX_FILE_PAGES {
let url = format!(
"{}/api/v1/repos/{}/pulls/{}/files?limit={FILE_PAGE_SIZE}&page={page}",
self.base_url, full_name, index
);
let res = self.client.get(url).send().await?;
if !res.status().is_success() {
return Err(anyhow::anyhow!(
"Failed to list pull request files: {}",
res.status()
));
}
let page_files = res.json::<Vec<PullRequestFile>>().await?;
// The instance may cap the page size below the requested one, so a
// short page is not the end of the list: an empty one is.
if page_files.is_empty() {
return Ok(files);
}
files.extend(page_files);
}
warn!(files = files.len(), "Pull request file list was truncated");
Ok(files)
}
#[instrument(skip(self, review_result))]
pub async fn post_pull_request_review(
&self,
@@ -148,19 +225,27 @@ impl GiteaAPI {
self.base_url, full_name, index
);
let comments = &review_result
let comments = review_result
.reviews
.iter()
.filter(|r| r.line.is_some())
.map(|r| {
let path = r.filename.clone();
let line = r.line.unwrap_or(0);
let body = r.message.clone();
.filter_map(|review| {
let line = review.line?;
let path = review.filename.clone();
let body = review.message.clone();
json!({
"path": path,
"new_position": line,
"body": body
// A line removed by the pull request only exists in the old
// version of the file, so it is anchored with `old_position`.
Some(match review.side {
Some(ReviewSide::Removed) => json!({
"path": path,
"old_position": line,
"body": body
}),
_ => json!({
"path": path,
"new_position": line,
"body": body
}),
})
})
.collect::<Vec<_>>();
@@ -184,6 +269,23 @@ impl GiteaAPI {
}
}
/// Reads a response body, refusing to buffer more than [`MAX_DIFF_SIZE`].
async fn read_capped(response: reqwest::Response) -> anyhow::Result<String> {
let stream = response.bytes_stream().map_err(std::io::Error::other);
let mut buf = Vec::with_capacity(MAX_DIFF_SIZE);
StreamReader::new(stream)
.take((MAX_DIFF_SIZE + 1) as u64)
.read_to_end(&mut buf)
.await?;
if buf.len() > MAX_DIFF_SIZE {
anyhow::bail!("Pull request diff exceeds the maximum allowed size of 1 MiB");
}
Ok(String::from_utf8_lossy(&buf).into_owned())
}
#[derive(Debug)]
pub enum WebhookType {
Review(ReviewPayload),
@@ -213,7 +315,6 @@ pub struct ReviewPayload {
#[derive(Deserialize, Debug)]
pub struct PullRequest {
pub diff_url: String,
pub number: u64,
pub title: String,
}
@@ -234,6 +335,18 @@ pub struct Repository {
pub full_name: String,
}
/// A file changed by a pull request, as reported by the API.
#[derive(Deserialize, Debug)]
pub struct PullRequestFile {
/// Path of the file in the new version of the repository.
pub filename: String,
/// Previous path, for a renamed file.
#[serde(default)]
pub previous_filename: Option<String>,
/// `added`, `modified`, `deleted`, `renamed`…
pub status: String,
}
impl WebhookType {
pub fn from_event(event: &str, bot_name: &str, json: Value) -> Result<Self, AppError> {
let wb = match event {