mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-19 05:56:43 +08:00
- Handle apply_patch custom tool call streaming deltas and snapshots for Responses API translation - Track and resolve step indexes, function call identities, and error states across stream events - Propagate apply_patch tool input errors and upstream failure states in stream and non-stream responses
127 lines
5.6 KiB
Go
127 lines
5.6 KiB
Go
package helps
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
_ "github.com/router-for-me/CLIProxyAPI/v8/internal/translator"
|
|
"github.com/router-for-me/CLIProxyAPI/v8/internal/translator/common"
|
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executor"
|
|
"github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/usage"
|
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v8/sdk/translator"
|
|
)
|
|
|
|
func TestApplyPatchTranslationError(t *testing.T) {
|
|
state := &common.ApplyPatchErrorState{}
|
|
want := errors.New("invalid wrapper")
|
|
state.SetToolInputError(want)
|
|
if got := ApplyPatchTranslationError(state); got != want {
|
|
t.Fatalf("translation error = %v, want %v", got, want)
|
|
}
|
|
if ApplyPatchTranslationError(struct{}{}) != nil || ApplyPatchTranslationError(nil) != nil {
|
|
t.Fatal("unrelated translator state was rejected")
|
|
}
|
|
}
|
|
|
|
func TestApplyPatchFinalizeOptionalCanonicalState(t *testing.T) {
|
|
original := []byte(`{"tools":[{"type":"custom","name":"apply_patch"}]}`)
|
|
for _, source := range []sdktranslator.Format{sdktranslator.FormatOpenAI, sdktranslator.FormatClaude, sdktranslator.FormatGemini, sdktranslator.FormatInteractions} {
|
|
t.Run(source.String(), func(t *testing.T) {
|
|
var param any
|
|
InitializeApplyPatchStream(context.Background(), source, sdktranslator.FormatOpenAIResponse, "model", original, original, ¶m)
|
|
frames := FinalizeApplyPatchStream(param)
|
|
if len(frames) != 1 || ApplyPatchTranslationError(param) == nil || !bytes.Contains(frames[0], []byte(`"type":"response.failed"`)) {
|
|
t.Fatalf("EOF did not fail canonically: %s", frames)
|
|
}
|
|
if len(FinalizeApplyPatchStream(param)) != 0 {
|
|
t.Fatal("EOF failure emitted twice")
|
|
}
|
|
late := []byte(`data: {"type":"message_stop","event_type":"interaction.completed","candidates":[{"finishReason":"STOP"}],"choices":[{"delta":{},"finish_reason":"stop"}]}`)
|
|
if len(sdktranslator.TranslateStream(context.Background(), source, sdktranslator.FormatOpenAIResponse, "model", original, original, late, ¶m)) != 0 {
|
|
t.Fatal("EOF failure reopened into success")
|
|
}
|
|
if len(sdktranslator.TranslateStream(context.Background(), source, sdktranslator.FormatOpenAIResponse, "model", original, original, []byte("[DONE]"), ¶m)) != 0 {
|
|
t.Fatal("failure forwarded transport success")
|
|
}
|
|
})
|
|
}
|
|
if len(FinalizeApplyPatchStream(struct{}{})) != 0 || len(FinalizeApplyPatchStream(nil)) != 0 {
|
|
t.Fatal("unrelated states received an EOF failure")
|
|
}
|
|
}
|
|
|
|
func TestApplyPatchTokenUsageHelperRetainsFailureState(t *testing.T) {
|
|
original := []byte(`{"tools":[{"type":"custom","name":"apply_patch"}]}`)
|
|
var param any
|
|
tokens := NewClaudeInputTokenState(sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAI, sdktranslator.FormatOpenAIResponse, original)
|
|
chunks := TranslateStreamWithClaudeInputTokens(context.Background(), sdktranslator.FormatOpenAI, sdktranslator.FormatOpenAIResponse, "model", original, original, []byte(`data: {"id":"r","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"c","function":{"name":"apply_patch","arguments":"{\"input\":7}"}}]}}]}`), ¶m, tokens)
|
|
if ApplyPatchTranslationError(param) == nil {
|
|
t.Fatal("token helper discarded the same retained error state")
|
|
}
|
|
failed := 0
|
|
for _, chunk := range chunks {
|
|
if bytes.Contains(chunk, []byte(`"type":"response.failed"`)) {
|
|
failed++
|
|
if bytes.Contains(chunk, []byte(`"usage"`)) {
|
|
t.Fatal("usage normalization altered a failure frame")
|
|
}
|
|
}
|
|
}
|
|
if failed != 1 {
|
|
t.Fatalf("failure frames=%d", failed)
|
|
}
|
|
}
|
|
|
|
func TestApplyPatchInteractionsOnlyValidSourceDoneClosesTransport(t *testing.T) {
|
|
var param any
|
|
translate := func(payload string) [][]byte {
|
|
return sdktranslator.TranslateStream(context.Background(), sdktranslator.FormatInteractions, sdktranslator.FormatOpenAIResponse, "model", nil, nil, []byte(payload), ¶m)
|
|
}
|
|
_ = translate(`{"event_type":"interaction.completed","interaction":{"id":"r"}}`)
|
|
if chunks := translate(`{"event_type":"done",`); len(chunks) != 0 {
|
|
t.Fatalf("malformed post-terminal event was accepted as a sentinel: %s", chunks)
|
|
}
|
|
chunks := translate("[DONE]")
|
|
if len(chunks) != 1 || !bytes.Equal(chunks[0], []byte("data: [DONE]")) {
|
|
t.Fatalf("first real source sentinel was swallowed: %s", chunks)
|
|
}
|
|
if len(translate("[DONE]")) != 0 || len(translate(`{"event_type":"done"}`)) != 0 {
|
|
t.Fatal("duplicate source sentinel was forwarded")
|
|
}
|
|
}
|
|
|
|
type patchCanceledEOFCapture struct{ records chan usage.Record }
|
|
|
|
func (p *patchCanceledEOFCapture) HandleUsage(_ context.Context, record usage.Record) {
|
|
if record.Provider == "task6-canceled-eof" {
|
|
p.records <- record
|
|
}
|
|
}
|
|
|
|
func TestApplyPatchCanceledEOFStillRecordsFailure(t *testing.T) {
|
|
capture := &patchCanceledEOFCapture{records: make(chan usage.Record, 1)}
|
|
usage.RegisterNamedPlugin("task6-canceled-eof", capture)
|
|
var param any
|
|
original := []byte(`{"tools":[{"type":"custom","name":"apply_patch"}]}`)
|
|
InitializeApplyPatchStream(context.Background(), sdktranslator.FormatGemini, sdktranslator.FormatOpenAIResponse, "m", original, original, ¶m)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
reporter := NewUsageReporter(ctx, "task6-canceled-eof", "m", nil)
|
|
out := make(chan cliproxyexecutor.StreamChunk)
|
|
if !EndApplyPatchStream(ctx, param, reporter, out, errors.New("clean failure")) {
|
|
t.Fatal("EOF failure was discarded")
|
|
}
|
|
reporter.EnsurePublished(ctx)
|
|
select {
|
|
case record := <-capture.records:
|
|
if !record.Failed {
|
|
t.Fatal("cancellation recovered a retained EOF failure as success usage")
|
|
}
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("canceled EOF usage was not dispatched")
|
|
}
|
|
}
|