fix(providers): normalize OpenAI responses terminal statuses

Treat padded failed and incomplete statuses consistently so responses parsing preserves server errors and finish reasons instead of misclassifying terminal responses.
This commit is contained in:
Equent 2026-03-08 22:50:39 +08:00
parent 8b64a99131
commit d12fa3bfbc
2 changed files with 31 additions and 8 deletions

View file

@ -691,6 +691,13 @@ func parseResponsesResponse(body io.Reader) (*LLMResponse, error) {
if len(apiResponse.Output) == 0 {
return nil, errors.New("openai responses returned terminal status with empty output")
}
case "failed":
if apiResponse.Error != nil {
if msg := strings.TrimSpace(apiResponse.Error.Message); msg != "" {
return nil, errors.New(msg)
}
}
return nil, errors.New("openai responses request failed")
default:
return nil, fmt.Errorf("openai responses returned unexpected or non-terminal status: %q", status)
}
@ -760,17 +767,10 @@ func parseResponsesResponse(body io.Reader) (*LLMResponse, error) {
}
}
if apiResponse.Status == "failed" {
if apiResponse.Error != nil && apiResponse.Error.Message != "" {
return nil, errors.New(apiResponse.Error.Message)
}
return nil, errors.New("openai responses request failed")
}
finishReason := "stop"
if len(toolCalls) > 0 {
finishReason = "tool_calls"
} else if apiResponse.Status == "incomplete" {
} else if status == "incomplete" {
finishReason = "length"
if apiResponse.IncompleteDetails != nil && apiResponse.IncompleteDetails.Reason != "" && apiResponse.IncompleteDetails.Reason != "max_output_tokens" {
finishReason = apiResponse.IncompleteDetails.Reason

View file

@ -556,6 +556,29 @@ func TestProviderChat_ParsesRefusalFromResponses(t *testing.T) {
}
}
func TestParseResponsesResponse_FailedStatusUsesServerMessage(t *testing.T) {
_, err := parseResponsesResponse(strings.NewReader(`{"status":" failed ","error":{"message":"responses failed"}}`))
if err == nil {
t.Fatal("expected error, got nil")
}
if err.Error() != "responses failed" {
t.Fatalf("error = %q, want %q", err.Error(), "responses failed")
}
}
func TestParseResponsesResponse_UsesNormalizedIncompleteStatus(t *testing.T) {
out, err := parseResponsesResponse(strings.NewReader(`{"status":" incomplete ","output":[{"type":"message","content":[{"type":"output_text","text":"partial answer"}]}],"incomplete_details":{"reason":"content_filter"}}`))
if err != nil {
t.Fatalf("parseResponsesResponse() error = %v", err)
}
if out.Content != "partial answer" {
t.Fatalf("Content = %q, want %q", out.Content, "partial answer")
}
if out.FinishReason != "content_filter" {
t.Fatalf("FinishReason = %q, want %q", out.FinishReason, "content_filter")
}
}
func TestProviderChat_UsesMaxCompletionTokensForGLM(t *testing.T) {
var requestBody map[string]any