Skip to content

Commit 4872048

Browse files
committed
fix: unify OpenAI-compatible provider response parsing with GPT-5.1 format
Updated OpenAI-compatible provider to use the same nested response structure as the OpenAI GPT-5.1 provider for consistency and compatibility. Response Structure Changes: - Changed ResponseAPIResponse to match GPT-5.1 format - `type` → `object` field - Nested output: output[{type: "message", content: [{type: "output_text", text: "..."}]}] - Updated token fields: input_tokens/output_tokens Content Extraction: - Updated generate_chat() to extract text from nested structure - Filter for "message" type items → "output_text" type content - Join multiple content items with newlines Chat Completions Fallback: - Convert Chat Completions response to unified output array structure - Create proper nested ResponseOutputItem with ResponseOutputContent - Ensures consistent parsing regardless of API endpoint used Tests Updated: - Fixed test assertions for use_responses_api (now false for Ollama/LM Studio) - Added comments explaining API choice This ensures all providers (OpenAI GPT-5.1, Ollama, LM Studio, custom endpoints) use the same response parsing logic, reducing code duplication and bugs.
1 parent fc432a9 commit 4872048

File tree

1 file changed

+42
-33
lines changed

1 file changed

+42
-33
lines changed

crates/codegraph-ai/src/openai_compatible_provider.rs

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -301,17 +301,24 @@ impl OpenAICompatibleProvider {
301301
.first()
302302
.ok_or_else(|| anyhow!("No choices in response"))?;
303303

304+
// Create output array structure matching GPT-5.1 format
305+
let output = vec![ResponseOutputItem {
306+
output_type: "message".to_string(),
307+
content: vec![ResponseOutputContent {
308+
content_type: "output_text".to_string(),
309+
text: choice.message.content.clone(),
310+
}],
311+
}];
312+
304313
Ok(ResponseAPIResponse {
305314
id: chat_response.id,
306-
response_type: "response".to_string(),
315+
object: "response".to_string(),
307316
status: choice.finish_reason.clone(),
308-
output_text: choice.message.content.clone(),
309-
output: Vec::new(), // Chat Completions uses output_text, not output array
310-
usage: chat_response.usage.map(|u| Usage {
311-
prompt_tokens: u.prompt_tokens,
317+
output,
318+
usage: chat_response.usage.map(|u| ResponseUsage {
319+
input_tokens: u.prompt_tokens,
312320
output_tokens: u.completion_tokens,
313321
total_tokens: u.total_tokens,
314-
reasoning_tokens: None,
315322
}),
316323
})
317324
}
@@ -326,22 +333,22 @@ impl LLMProvider for OpenAICompatibleProvider {
326333
) -> LLMResult<LLMResponse> {
327334
let response = self.send_request(messages, config).await?;
328335

329-
// Handle both old output_text field and new output array format
330-
let content = if !response.output_text.is_empty() {
331-
response.output_text
332-
} else if !response.output.is_empty() {
333-
response.output.iter()
334-
.map(|o| o.content.as_str())
335-
.collect::<Vec<_>>()
336-
.join("\n")
337-
} else {
338-
String::new()
339-
};
336+
// Extract text from output array (matches OpenAI GPT-5.1 structure)
337+
// Response format: output[{type: "message", content: [{type: "output_text", text: "..."}]}]
338+
let content = response
339+
.output
340+
.iter()
341+
.filter(|item| item.output_type == "message")
342+
.flat_map(|item| &item.content)
343+
.filter(|c| c.content_type == "output_text")
344+
.map(|c| c.text.as_str())
345+
.collect::<Vec<_>>()
346+
.join("\n");
340347

341348
Ok(LLMResponse {
342349
content,
343350
total_tokens: response.usage.as_ref().map(|u| u.total_tokens),
344-
prompt_tokens: response.usage.as_ref().map(|u| u.prompt_tokens),
351+
prompt_tokens: response.usage.as_ref().map(|u| u.input_tokens),
345352
completion_tokens: response.usage.as_ref().map(|u| u.output_tokens),
346353
finish_reason: response.status.clone(),
347354
model: self.config.model.clone(),
@@ -469,34 +476,36 @@ struct ResponsesAPIRequest {
469476
#[derive(Debug, Deserialize)]
470477
struct ResponseAPIResponse {
471478
id: String,
472-
#[serde(rename = "type")]
473-
response_type: String,
479+
object: String,
474480
#[serde(default)]
475481
status: Option<String>,
476482
#[serde(default)]
477-
output_text: String,
478-
#[serde(default)]
479-
output: Vec<ResponseOutput>,
483+
output: Vec<ResponseOutputItem>,
480484
#[serde(default)]
481-
usage: Option<Usage>,
485+
usage: Option<ResponseUsage>,
482486
}
483487

484488
#[derive(Debug, Deserialize)]
485-
struct ResponseOutput {
489+
struct ResponseOutputItem {
486490
#[serde(rename = "type")]
487491
output_type: String,
488492
#[serde(default)]
489-
content: String,
493+
content: Vec<ResponseOutputContent>,
490494
}
491495

492496
#[derive(Debug, Deserialize)]
493-
struct Usage {
494-
prompt_tokens: usize,
495-
#[serde(alias = "completion_tokens")]
497+
struct ResponseOutputContent {
498+
#[serde(rename = "type")]
499+
content_type: String,
500+
#[serde(default)]
501+
text: String,
502+
}
503+
504+
#[derive(Debug, Deserialize)]
505+
struct ResponseUsage {
506+
input_tokens: usize,
496507
output_tokens: usize,
497508
total_tokens: usize,
498-
#[serde(default)]
499-
reasoning_tokens: Option<usize>,
500509
}
501510

502511
// API request/response types for Chat Completions API (fallback)
@@ -553,14 +562,14 @@ mod tests {
553562
let config = OpenAICompatibleConfig::lm_studio("test-model".to_string());
554563
assert_eq!(config.base_url, "http://localhost:1234/v1");
555564
assert_eq!(config.provider_name, "lmstudio");
556-
assert!(config.use_responses_api);
565+
assert!(!config.use_responses_api); // LM Studio uses Chat Completions API
557566
}
558567

559568
#[test]
560569
fn test_ollama_config() {
561570
let config = OpenAICompatibleConfig::ollama("llama3".to_string());
562571
assert_eq!(config.base_url, "http://localhost:11434/v1");
563572
assert_eq!(config.provider_name, "ollama");
564-
assert!(config.use_responses_api);
573+
assert!(!config.use_responses_api); // Ollama uses Chat Completions API
565574
}
566575
}

0 commit comments

Comments
 (0)