Skip to content

Commit dea400c

Browse files
committed
Add OpenAI Responses support and tighten PR comments
1 parent ecbd71c commit dea400c

File tree

7 files changed

+141
-7
lines changed

7 files changed

+141
-7
lines changed

.diffscope.yml.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ max_tokens: 4000
99
# API configuration (optional - can use environment variables)
1010
# api_key: your-api-key-here
1111
# base_url: https://api.openai.com/v1
12+
# openai_use_responses: true # Use OpenAI Responses API instead of chat completions
1213

1314
# Global exclude patterns
1415
exclude_patterns:
@@ -85,4 +86,4 @@ paths:
8586
plugins:
8687
eslint: true
8788
semgrep: true
88-
duplicate_filter: true
89+
duplicate_filter: true

.github/workflows/diffscope.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ jobs:
5353
body: `**${comment.severity}**: ${comment.content}`,
5454
commit_id: headSha,
5555
path: comment.file_path,
56-
line: comment.line_number
56+
line: comment.line_number,
57+
side: "RIGHT"
5758
});
5859
}

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ model: gpt-4o
182182
temperature: 0.2
183183
max_tokens: 4000
184184
system_prompt: "Focus on security vulnerabilities, performance issues, and best practices"
185+
openai_use_responses: true # Use OpenAI Responses API (recommended) instead of chat completions
185186
186187
# Built-in plugins (enabled by default)
187188
plugins:
@@ -617,4 +618,4 @@ All binaries are automatically built and uploaded with each release.
617618

618619
## Support
619620

620-
- GitHub Issues: [github.com/Haasonsaas/diffscope/issues](https://github.com/Haasonsaas/diffscope/issues)
621+
- GitHub Issues: [github.com/Haasonsaas/diffscope/issues](https://github.com/Haasonsaas/diffscope/issues)

src/adapters/llm.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub struct ModelConfig {
99
pub base_url: Option<String>,
1010
pub temperature: f32,
1111
pub max_tokens: usize,
12+
pub openai_use_responses: Option<bool>,
1213
}
1314

1415
impl Default for ModelConfig {
@@ -19,6 +20,7 @@ impl Default for ModelConfig {
1920
base_url: None,
2021
temperature: 0.2,
2122
max_tokens: 4000,
23+
openai_use_responses: None,
2224
}
2325
}
2426
}

src/adapters/openai.rs

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ struct OpenAIRequest {
2121
max_tokens: usize,
2222
}
2323

24+
#[derive(Serialize)]
25+
struct OpenAIResponsesRequest {
26+
model: String,
27+
input: String,
28+
instructions: String,
29+
temperature: f32,
30+
max_output_tokens: usize,
31+
}
32+
2433
#[derive(Serialize, Deserialize)]
2534
struct Message {
2635
role: String,
@@ -34,6 +43,29 @@ struct OpenAIResponse {
3443
model: String,
3544
}
3645

46+
#[derive(Deserialize)]
47+
struct OpenAIResponsesResponse {
48+
output: Vec<OpenAIResponseOutput>,
49+
model: String,
50+
#[serde(default)]
51+
usage: Option<OpenAIResponsesUsage>,
52+
}
53+
54+
#[derive(Deserialize)]
55+
struct OpenAIResponseOutput {
56+
#[serde(rename = "type")]
57+
output_type: String,
58+
#[serde(default)]
59+
content: Vec<OpenAIResponseContent>,
60+
}
61+
62+
#[derive(Deserialize)]
63+
struct OpenAIResponseContent {
64+
#[serde(rename = "type")]
65+
content_type: String,
66+
text: Option<String>,
67+
}
68+
3769
#[derive(Deserialize)]
3870
struct Choice {
3971
message: Message,
@@ -46,6 +78,13 @@ struct OpenAIUsage {
4678
total_tokens: usize,
4779
}
4880

81+
#[derive(Deserialize)]
82+
struct OpenAIResponsesUsage {
83+
input_tokens: usize,
84+
output_tokens: usize,
85+
total_tokens: usize,
86+
}
87+
4988
impl OpenAIAdapter {
5089
pub fn new(config: ModelConfig) -> Result<Self> {
5190
let api_key = config.api_key.clone()
@@ -109,6 +148,32 @@ impl OpenAIAdapter {
109148
#[async_trait]
110149
impl LLMAdapter for OpenAIAdapter {
111150
async fn complete(&self, request: LLMRequest) -> Result<LLMResponse> {
151+
if should_use_responses_api(&self.config) {
152+
return self.complete_responses(request).await;
153+
}
154+
155+
self.complete_chat_completions(request).await
156+
}
157+
158+
fn _model_name(&self) -> &str {
159+
&self.config.model_name
160+
}
161+
}
162+
163+
fn is_retryable_status(status: StatusCode) -> bool {
164+
status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()
165+
}
166+
167+
fn should_use_responses_api(config: &ModelConfig) -> bool {
168+
if let Some(flag) = config.openai_use_responses {
169+
return flag;
170+
}
171+
172+
!config.model_name.starts_with("gpt-3.5")
173+
}
174+
175+
impl OpenAIAdapter {
176+
async fn complete_chat_completions(&self, request: LLMRequest) -> Result<LLMResponse> {
112177
let messages = vec![
113178
Message {
114179
role: "system".to_string(),
@@ -161,11 +226,65 @@ impl LLMAdapter for OpenAIAdapter {
161226
})
162227
}
163228

164-
fn _model_name(&self) -> &str {
165-
&self.config.model_name
229+
async fn complete_responses(&self, request: LLMRequest) -> Result<LLMResponse> {
230+
let openai_request = OpenAIResponsesRequest {
231+
model: self.config.model_name.clone(),
232+
input: request.user_prompt,
233+
instructions: request.system_prompt,
234+
temperature: request.temperature.unwrap_or(self.config.temperature),
235+
max_output_tokens: request.max_tokens.unwrap_or(self.config.max_tokens),
236+
};
237+
238+
let url = format!("{}/responses", self.base_url);
239+
let response = self
240+
.send_with_retry(|| {
241+
self.client
242+
.post(&url)
243+
.header("Authorization", format!("Bearer {}", self.api_key))
244+
.header("Content-Type", "application/json")
245+
.json(&openai_request)
246+
})
247+
.await
248+
.context("Failed to send request to OpenAI")?;
249+
250+
let openai_response: OpenAIResponsesResponse = response
251+
.json()
252+
.await
253+
.context("Failed to parse OpenAI response")?;
254+
255+
let content = extract_response_text(&openai_response);
256+
let usage = openai_response.usage.map(|usage| Usage {
257+
prompt_tokens: usage.input_tokens,
258+
completion_tokens: usage.output_tokens,
259+
total_tokens: usage.total_tokens,
260+
});
261+
262+
Ok(LLMResponse {
263+
content,
264+
model: openai_response.model,
265+
usage,
266+
})
166267
}
167268
}
168269

169-
fn is_retryable_status(status: StatusCode) -> bool {
170-
status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()
270+
fn extract_response_text(response: &OpenAIResponsesResponse) -> String {
271+
let mut combined = String::new();
272+
273+
for item in &response.output {
274+
if item.output_type != "message" {
275+
continue;
276+
}
277+
for content in &item.content {
278+
if content.content_type == "output_text" {
279+
if let Some(text) = &content.text {
280+
if !combined.is_empty() {
281+
combined.push('\n');
282+
}
283+
combined.push_str(text);
284+
}
285+
}
286+
}
287+
}
288+
289+
combined
171290
}

src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub struct Config {
1818
pub api_key: Option<String>,
1919
pub base_url: Option<String>,
2020

21+
#[serde(default)]
22+
pub openai_use_responses: Option<bool>,
23+
2124
#[serde(default)]
2225
pub plugins: PluginConfig,
2326

@@ -78,6 +81,7 @@ impl Default for Config {
7881
system_prompt: None,
7982
api_key: None,
8083
base_url: None,
84+
openai_use_responses: None,
8185
plugins: PluginConfig::default(),
8286
exclude_patterns: Vec::new(),
8387
paths: HashMap::new(),

src/main.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ async fn review_command(
240240
base_url: config.base_url.clone(),
241241
temperature: config.temperature,
242242
max_tokens: config.max_tokens,
243+
openai_use_responses: config.openai_use_responses,
243244
};
244245

245246
let adapter = adapters::llm::create_adapter(&model_config)?;
@@ -498,6 +499,7 @@ async fn pr_command(
498499
base_url: config.base_url.clone(),
499500
temperature: config.temperature,
500501
max_tokens: config.max_tokens,
502+
openai_use_responses: config.openai_use_responses,
501503
};
502504

503505
let adapter = adapters::llm::create_adapter(&model_config)?;
@@ -560,6 +562,7 @@ async fn suggest_commit_message(config: config::Config) -> Result<()> {
560562
base_url: config.base_url.clone(),
561563
temperature: config.temperature,
562564
max_tokens: config.max_tokens,
565+
openai_use_responses: config.openai_use_responses,
563566
};
564567

565568
let adapter = adapters::llm::create_adapter(&model_config)?;
@@ -608,6 +611,7 @@ async fn suggest_pr_title(config: config::Config) -> Result<()> {
608611
base_url: config.base_url.clone(),
609612
temperature: config.temperature,
610613
max_tokens: config.max_tokens,
614+
openai_use_responses: config.openai_use_responses,
611615
};
612616

613617
let adapter = adapters::llm::create_adapter(&model_config)?;
@@ -742,6 +746,7 @@ async fn review_diff_content_raw(
742746
base_url: config.base_url.clone(),
743747
temperature: config.temperature,
744748
max_tokens: config.max_tokens,
749+
openai_use_responses: config.openai_use_responses,
745750
};
746751

747752
let adapter = adapters::llm::create_adapter(&model_config)?;
@@ -1164,6 +1169,7 @@ async fn smart_review_command(
11641169
base_url: config.base_url.clone(),
11651170
temperature: config.temperature,
11661171
max_tokens: config.max_tokens,
1172+
openai_use_responses: config.openai_use_responses,
11671173
};
11681174

11691175
let adapter = adapters::llm::create_adapter(&model_config)?;

0 commit comments

Comments
 (0)