Files
CLIProxyAPI/internal/runtime/executor/codex_response_model_test.go
2026-09-27 14:42:26 +08:00

108 lines
3.8 KiB
Go

package executor
import (
"bytes"
"context"
"testing"
"time"
"github.com/router-for-me/CLIProxyAPI/v8/internal/config"
"github.com/router-for-me/CLIProxyAPI/v8/internal/runtime/executor/helps"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
coreusage "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/usage"
)
const codexResponseModelTestStream = `event: response.created
data: {"type":"response.created","response":{"id":"resp_1","model":"gpt-5.6-luna"}}
data: {"type":"response.output_text.delta","delta":"he"}
data: {"type":"response.output_text.delta","delta":"llo"}
data: {"type":"response.completed","response":{"id":"resp_1","model":"gpt-5.6-luna","usage":{"input_tokens":5,"output_tokens":7,"total_tokens":12}}}
data: [DONE]`
// TestObserveCodexTokenEventRecordsResponseModel guards the wiring between the codex
// stream loops and the usage reporter: every transport funnels through observeCodexTokenEvent.
func TestObserveCodexTokenEventRecordsResponseModel(t *testing.T) {
reporter := helps.NewExecutorUsageReporter(context.Background(), NewCodexExecutor(&config.Config{}), "gpt-6-astra", nil)
for _, line := range bytes.Split([]byte(codexResponseModelTestStream), []byte("\n")) {
line = bytes.TrimSpace(line)
if !bytes.HasPrefix(line, dataTag) {
continue
}
observeCodexTokenEvent(reporter, bytes.TrimSpace(line[len(dataTag):]))
}
if got := reporter.ResponseModel(); got != "gpt-5.6-luna" {
t.Fatalf("reporter response model = %q, want %q", got, "gpt-5.6-luna")
}
}
type codexResponseModelUsageCapture struct {
alias string
records chan coreusage.Record
}
func (c *codexResponseModelUsageCapture) HandleUsage(_ context.Context, record coreusage.Record) {
if record.Alias != c.alias {
return
}
select {
case c.records <- record:
default:
}
}
type codexResponseModelNoopUsagePlugin struct{}
func (codexResponseModelNoopUsagePlugin) HandleUsage(context.Context, coreusage.Record) {}
func (c *codexResponseModelUsageCapture) await(t *testing.T) coreusage.Record {
t.Helper()
select {
case record := <-c.records:
return record
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for the usage record")
return coreusage.Record{}
}
}
// TestCodexUsageRecordsCarryResponseModelPerModel checks that the attempt record reports
// the served model while the image generation tool record must not claim it.
func TestCodexUsageRecordsCarryResponseModelPerModel(t *testing.T) {
const alias = "codex-response-model-wiring-test"
capture := &codexResponseModelUsageCapture{alias: alias, records: make(chan coreusage.Record, 4)}
coreusage.RegisterNamedPlugin(t.Name(), capture)
t.Cleanup(func() {
coreusage.RegisterNamedPlugin(t.Name(), codexResponseModelNoopUsagePlugin{})
})
ctx := coreusage.WithRequestedModelAlias(context.Background(), alias)
auth := &cliproxyauth.Auth{ID: "codex-auth-1", Index: "auth-index-7", Provider: "codex"}
reporter := helps.NewExecutorUsageReporter(ctx, NewCodexExecutor(&config.Config{}), "gpt-5.4-mini", auth)
observeCodexTokenEvent(reporter, []byte(`{"type":"response.completed","response":{"model":"gpt-5.4-mini","usage":{"total_tokens":12}}}`))
reporter.EnsurePublished(ctx)
reporter.PublishAdditionalModel(ctx, "gpt-image-1.5", coreusage.Detail{TotalTokens: 5})
attemptRecord := capture.await(t)
if attemptRecord.Model != "gpt-5.4-mini" {
t.Fatalf("attempt record model = %q, want %q", attemptRecord.Model, "gpt-5.4-mini")
}
if attemptRecord.ResponseModel != "gpt-5.4-mini" {
t.Fatalf("attempt record response model = %q, want %q", attemptRecord.ResponseModel, "gpt-5.4-mini")
}
imageRecord := capture.await(t)
if imageRecord.Model != "gpt-image-1.5" {
t.Fatalf("image record model = %q, want %q", imageRecord.Model, "gpt-image-1.5")
}
if imageRecord.ResponseModel != "" {
t.Fatalf("image record response model = %q, want empty", imageRecord.ResponseModel)
}
}