mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-26 02:56:11 +00:00
fix(executor): ensure empty stream completions use output_item.done as fallback
Fixed: #2583
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -167,22 +168,63 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
|
|
||||||
lines := bytes.Split(data, []byte("\n"))
|
lines := bytes.Split(data, []byte("\n"))
|
||||||
|
outputItemsByIndex := make(map[int64][]byte)
|
||||||
|
var outputItemsFallback [][]byte
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if !bytes.HasPrefix(line, dataTag) {
|
if !bytes.HasPrefix(line, dataTag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
line = bytes.TrimSpace(line[5:])
|
eventData := bytes.TrimSpace(line[5:])
|
||||||
if gjson.GetBytes(line, "type").String() != "response.completed" {
|
eventType := gjson.GetBytes(eventData, "type").String()
|
||||||
|
|
||||||
|
if eventType == "response.output_item.done" {
|
||||||
|
itemResult := gjson.GetBytes(eventData, "item")
|
||||||
|
if !itemResult.Exists() || itemResult.Type != gjson.JSON {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
outputIndexResult := gjson.GetBytes(eventData, "output_index")
|
||||||
|
if outputIndexResult.Exists() {
|
||||||
|
outputItemsByIndex[outputIndexResult.Int()] = []byte(itemResult.Raw)
|
||||||
|
} else {
|
||||||
|
outputItemsFallback = append(outputItemsFallback, []byte(itemResult.Raw))
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if detail, ok := helps.ParseCodexUsage(line); ok {
|
if eventType != "response.completed" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if detail, ok := helps.ParseCodexUsage(eventData); ok {
|
||||||
reporter.Publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
completedData := eventData
|
||||||
|
outputResult := gjson.GetBytes(completedData, "response.output")
|
||||||
|
shouldPatchOutput := (!outputResult.Exists() || !outputResult.IsArray() || len(outputResult.Array()) == 0) && (len(outputItemsByIndex) > 0 || len(outputItemsFallback) > 0)
|
||||||
|
if shouldPatchOutput {
|
||||||
|
completedDataPatched := completedData
|
||||||
|
completedDataPatched, _ = sjson.SetRawBytes(completedDataPatched, "response.output", []byte(`[]`))
|
||||||
|
|
||||||
|
indexes := make([]int64, 0, len(outputItemsByIndex))
|
||||||
|
for idx := range outputItemsByIndex {
|
||||||
|
indexes = append(indexes, idx)
|
||||||
|
}
|
||||||
|
sort.Slice(indexes, func(i, j int) bool {
|
||||||
|
return indexes[i] < indexes[j]
|
||||||
|
})
|
||||||
|
for _, idx := range indexes {
|
||||||
|
completedDataPatched, _ = sjson.SetRawBytes(completedDataPatched, "response.output.-1", outputItemsByIndex[idx])
|
||||||
|
}
|
||||||
|
for _, item := range outputItemsFallback {
|
||||||
|
completedDataPatched, _ = sjson.SetRawBytes(completedDataPatched, "response.output.-1", item)
|
||||||
|
}
|
||||||
|
completedData = completedDataPatched
|
||||||
|
}
|
||||||
|
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, line, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, completedData, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
|
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator"
|
||||||
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCodexExecutorExecute_EmptyStreamCompletionOutputUsesOutputItemDone(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
_, _ = w.Write([]byte("data: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"ok\"}]},\"output_index\":0}\n"))
|
||||||
|
_, _ = w.Write([]byte("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"object\":\"response\",\"created_at\":1775555723,\"status\":\"completed\",\"model\":\"gpt-5.4-mini-2026-03-17\",\"output\":[],\"usage\":{\"input_tokens\":8,\"output_tokens\":28,\"total_tokens\":36}}}\n\n"))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
executor := NewCodexExecutor(&config.Config{})
|
||||||
|
auth := &cliproxyauth.Auth{Attributes: map[string]string{
|
||||||
|
"base_url": server.URL,
|
||||||
|
"api_key": "test",
|
||||||
|
}}
|
||||||
|
|
||||||
|
resp, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
||||||
|
Model: "gpt-5.4-mini",
|
||||||
|
Payload: []byte(`{"model":"gpt-5.4-mini","messages":[{"role":"user","content":"Say ok"}]}`),
|
||||||
|
}, cliproxyexecutor.Options{
|
||||||
|
SourceFormat: sdktranslator.FromString("openai"),
|
||||||
|
Stream: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Execute error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotContent := gjson.GetBytes(resp.Payload, "choices.0.message.content").String()
|
||||||
|
if gotContent != "ok" {
|
||||||
|
t.Fatalf("choices.0.message.content = %q, want %q; payload=%s", gotContent, "ok", string(resp.Payload))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user