fix(openai): enhance Responses API integration with full parameters and improved parsing
This commit is contained in:
@@ -149,11 +149,34 @@ impl super::Provider for OpenAIProvider {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut body = serde_json::json!({
|
||||||
|
"model": request.model,
|
||||||
|
"input": input_parts,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add standard parameters
|
||||||
|
if let Some(temp) = request.temperature {
|
||||||
|
body["temperature"] = serde_json::json!(temp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Newer models (gpt-5, o1) prefer max_completion_tokens
|
||||||
|
if let Some(max_tokens) = request.max_tokens {
|
||||||
|
if request.model.contains("gpt-5") || request.model.starts_with("o1-") || request.model.starts_with("o3-") {
|
||||||
|
body["max_completion_tokens"] = serde_json::json!(max_tokens);
|
||||||
|
} else {
|
||||||
|
body["max_tokens"] = serde_json::json!(max_tokens);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(tools) = &request.tools {
|
||||||
|
body["tools"] = serde_json::json!(tools);
|
||||||
|
}
|
||||||
|
|
||||||
let resp = self
|
let resp = self
|
||||||
.client
|
.client
|
||||||
.post(format!("{}/responses", self.config.base_url))
|
.post(format!("{}/responses", self.config.base_url))
|
||||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||||
.json(&serde_json::json!({ "model": request.model, "input": input_parts }))
|
.json(&body)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::ProviderError(e.to_string()))?;
|
.map_err(|e| AppError::ProviderError(e.to_string()))?;
|
||||||
@@ -173,8 +196,8 @@ impl super::Provider for OpenAIProvider {
|
|||||||
// Normalize Responses API output into ProviderResponse
|
// Normalize Responses API output into ProviderResponse
|
||||||
let mut content_text = String::new();
|
let mut content_text = String::new();
|
||||||
if let Some(output) = resp_json.get("output").and_then(|o| o.as_array()) {
|
if let Some(output) = resp_json.get("output").and_then(|o| o.as_array()) {
|
||||||
if let Some(first) = output.get(0) {
|
for out in output {
|
||||||
if let Some(contents) = first.get("content").and_then(|c| c.as_array()) {
|
if let Some(contents) = out.get("content").and_then(|c| c.as_array()) {
|
||||||
for item in contents {
|
for item in contents {
|
||||||
if let Some(text) = item.get("text").and_then(|t| t.as_str()) {
|
if let Some(text) = item.get("text").and_then(|t| t.as_str()) {
|
||||||
if !content_text.is_empty() { content_text.push_str("\n"); }
|
if !content_text.is_empty() { content_text.push_str("\n"); }
|
||||||
@@ -388,12 +411,27 @@ impl super::Provider for OpenAIProvider {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = serde_json::json!({
|
let mut body = serde_json::json!({
|
||||||
"model": request.model,
|
"model": request.model,
|
||||||
"input": input_parts,
|
"input": input_parts,
|
||||||
"stream": true
|
"stream": true,
|
||||||
|
"stream_options": { "include_usage": true }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add standard parameters
|
||||||
|
if let Some(temp) = request.temperature {
|
||||||
|
body["temperature"] = serde_json::json!(temp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Newer models (gpt-5, o1) prefer max_completion_tokens
|
||||||
|
if let Some(max_tokens) = request.max_tokens {
|
||||||
|
if request.model.contains("gpt-5") || request.model.starts_with("o1-") || request.model.starts_with("o3-") {
|
||||||
|
body["max_completion_tokens"] = serde_json::json!(max_tokens);
|
||||||
|
} else {
|
||||||
|
body["max_tokens"] = serde_json::json!(max_tokens);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let url = format!("{}/responses", self.config.base_url);
|
let url = format!("{}/responses", self.config.base_url);
|
||||||
let api_key = self.api_key.clone();
|
let api_key = self.api_key.clone();
|
||||||
let model = request.model.clone();
|
let model = request.model.clone();
|
||||||
@@ -420,32 +458,33 @@ impl super::Provider for OpenAIProvider {
|
|||||||
let chunk: serde_json::Value = serde_json::from_str(&msg.data)
|
let chunk: serde_json::Value = serde_json::from_str(&msg.data)
|
||||||
.map_err(|e| AppError::ProviderError(format!("Failed to parse Responses stream chunk: {}", e)))?;
|
.map_err(|e| AppError::ProviderError(format!("Failed to parse Responses stream chunk: {}", e)))?;
|
||||||
|
|
||||||
// Try standard OpenAI parsing first
|
// Try standard OpenAI parsing first (choices/usage)
|
||||||
if let Some(p_chunk) = helpers::parse_openai_stream_chunk(&chunk, &model, None) {
|
if let Some(p_chunk) = helpers::parse_openai_stream_chunk(&chunk, &model, None) {
|
||||||
yield p_chunk?;
|
yield p_chunk?;
|
||||||
} else {
|
} else {
|
||||||
// Responses API specific parsing for streaming
|
// Responses API specific parsing for streaming
|
||||||
// Often it follows a similar structure to the non-streaming response but in chunks
|
|
||||||
let mut content = String::new();
|
let mut content = String::new();
|
||||||
|
|
||||||
// Check for output[0].content[0].text (similar to non-stream)
|
// Check for output[0].content[0].text (similar to non-stream)
|
||||||
if let Some(output) = chunk.get("output").and_then(|o| o.as_array()) {
|
if let Some(output) = chunk.get("output").and_then(|o| o.as_array()) {
|
||||||
if let Some(first) = output.get(0) {
|
for out in output {
|
||||||
if let Some(contents) = first.get("content").and_then(|c| c.as_array()) {
|
if let Some(contents) = out.get("content").and_then(|c| c.as_array()) {
|
||||||
for item in contents {
|
for item in contents {
|
||||||
if let Some(text) = item.get("text").and_then(|t| t.as_str()) {
|
if let Some(text) = item.get("text").and_then(|t| t.as_str()) {
|
||||||
content.push_str(text);
|
content.push_str(text);
|
||||||
|
} else if let Some(delta) = item.get("delta").and_then(|d| d.get("text")).and_then(|t| t.as_str()) {
|
||||||
|
content.push_str(delta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for candidates[0].content.parts[0].text (Gemini-like, which OpenAI sometimes uses for v1/responses)
|
// Check for candidates[0].content.parts[0].text
|
||||||
if content.is_empty() {
|
if content.is_empty() {
|
||||||
if let Some(cands) = chunk.get("candidates").and_then(|c| c.as_array()) {
|
if let Some(cands) = chunk.get("candidates").and_then(|c| c.as_array()) {
|
||||||
if let Some(c0) = cands.get(0) {
|
for c in cands {
|
||||||
if let Some(content_obj) = c0.get("content") {
|
if let Some(content_obj) = c.get("content") {
|
||||||
if let Some(parts) = content_obj.get("parts").and_then(|p| p.as_array()) {
|
if let Some(parts) = content_obj.get("parts").and_then(|p| p.as_array()) {
|
||||||
for p in parts {
|
for p in parts {
|
||||||
if let Some(t) = p.get("text").and_then(|v| v.as_str()) {
|
if let Some(t) = p.get("text").and_then(|v| v.as_str()) {
|
||||||
@@ -488,6 +527,8 @@ impl super::Provider for OpenAIProvider {
|
|||||||
Err(AppError::ProviderError(format!("OpenAI Responses API error ({}): {}", status, error_body)))?;
|
Err(AppError::ProviderError(format!("OpenAI Responses API error ({}): {}", status, error_body)))?;
|
||||||
}
|
}
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
// If the probe returned 200, but the stream ended, it might be a silent failure or timeout.
|
||||||
|
tracing::warn!("Responses stream ended prematurely (probe returned 200)");
|
||||||
Err(AppError::ProviderError(format!("Responses stream error (probe returned 200): {}", e)))?;
|
Err(AppError::ProviderError(format!("Responses stream error (probe returned 200): {}", e)))?;
|
||||||
}
|
}
|
||||||
Err(probe_err) => {
|
Err(probe_err) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user