mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-06 21:08:39 +00:00
Restore Claude continuity after the continuity refactor, keep auth-affinity keys out of upstream Codex session identifiers, and only persist affinity after successful execution so retries can still rotate to healthy credentials when the first auth fails.
196 lines
7.2 KiB
Go
196 lines
7.2 KiB
Go
package executor
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
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 TestCodexExecutorCacheHelper_OpenAIChatCompletions_StablePromptCacheKeyFromAPIKey(t *testing.T) {
|
|
recorder := httptest.NewRecorder()
|
|
ginCtx, _ := gin.CreateTestContext(recorder)
|
|
ginCtx.Set("apiKey", "test-api-key")
|
|
|
|
ctx := context.WithValue(context.Background(), "gin", ginCtx)
|
|
executor := &CodexExecutor{}
|
|
rawJSON := []byte(`{"model":"gpt-5.3-codex","stream":true}`)
|
|
req := cliproxyexecutor.Request{
|
|
Model: "gpt-5.3-codex",
|
|
Payload: []byte(`{"model":"gpt-5.3-codex"}`),
|
|
}
|
|
url := "https://example.com/responses"
|
|
|
|
httpReq, _, err := executor.cacheHelper(ctx, nil, sdktranslator.FromString("openai"), url, req, cliproxyexecutor.Options{}, rawJSON)
|
|
if err != nil {
|
|
t.Fatalf("cacheHelper error: %v", err)
|
|
}
|
|
|
|
body, errRead := io.ReadAll(httpReq.Body)
|
|
if errRead != nil {
|
|
t.Fatalf("read request body: %v", errRead)
|
|
}
|
|
|
|
expectedKey := uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:test-api-key")).String()
|
|
gotKey := gjson.GetBytes(body, "prompt_cache_key").String()
|
|
if gotKey != expectedKey {
|
|
t.Fatalf("prompt_cache_key = %q, want %q", gotKey, expectedKey)
|
|
}
|
|
if gotSession := httpReq.Header.Get("session_id"); gotSession != expectedKey {
|
|
t.Fatalf("session_id = %q, want %q", gotSession, expectedKey)
|
|
}
|
|
if got := httpReq.Header.Get("Conversation_id"); got != "" {
|
|
t.Fatalf("Conversation_id = %q, want empty", got)
|
|
}
|
|
|
|
httpReq2, _, err := executor.cacheHelper(ctx, nil, sdktranslator.FromString("openai"), url, req, cliproxyexecutor.Options{}, rawJSON)
|
|
if err != nil {
|
|
t.Fatalf("cacheHelper error (second call): %v", err)
|
|
}
|
|
body2, errRead2 := io.ReadAll(httpReq2.Body)
|
|
if errRead2 != nil {
|
|
t.Fatalf("read request body (second call): %v", errRead2)
|
|
}
|
|
gotKey2 := gjson.GetBytes(body2, "prompt_cache_key").String()
|
|
if gotKey2 != expectedKey {
|
|
t.Fatalf("prompt_cache_key (second call) = %q, want %q", gotKey2, expectedKey)
|
|
}
|
|
}
|
|
|
|
func TestCodexExecutorCacheHelper_OpenAIResponses_PreservesPromptCacheRetention(t *testing.T) {
|
|
executor := &CodexExecutor{}
|
|
url := "https://example.com/responses"
|
|
req := cliproxyexecutor.Request{
|
|
Model: "gpt-5.3-codex",
|
|
Payload: []byte(`{"model":"gpt-5.3-codex","prompt_cache_key":"cache-key-1","prompt_cache_retention":"persistent"}`),
|
|
}
|
|
rawJSON := []byte(`{"model":"gpt-5.3-codex","stream":true,"prompt_cache_retention":"persistent"}`)
|
|
|
|
httpReq, _, err := executor.cacheHelper(context.Background(), nil, sdktranslator.FromString("openai-response"), url, req, cliproxyexecutor.Options{}, rawJSON)
|
|
if err != nil {
|
|
t.Fatalf("cacheHelper error: %v", err)
|
|
}
|
|
|
|
body, err := io.ReadAll(httpReq.Body)
|
|
if err != nil {
|
|
t.Fatalf("read request body: %v", err)
|
|
}
|
|
|
|
if got := gjson.GetBytes(body, "prompt_cache_key").String(); got != "cache-key-1" {
|
|
t.Fatalf("prompt_cache_key = %q, want %q", got, "cache-key-1")
|
|
}
|
|
if got := gjson.GetBytes(body, "prompt_cache_retention").String(); got != "persistent" {
|
|
t.Fatalf("prompt_cache_retention = %q, want %q", got, "persistent")
|
|
}
|
|
if got := httpReq.Header.Get("session_id"); got != "cache-key-1" {
|
|
t.Fatalf("session_id = %q, want %q", got, "cache-key-1")
|
|
}
|
|
if got := httpReq.Header.Get("Conversation_id"); got != "" {
|
|
t.Fatalf("Conversation_id = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestCodexExecutorCacheHelper_OpenAIChatCompletions_UsesExecutionSessionForContinuity(t *testing.T) {
|
|
executor := &CodexExecutor{}
|
|
rawJSON := []byte(`{"model":"gpt-5.4","stream":true}`)
|
|
req := cliproxyexecutor.Request{
|
|
Model: "gpt-5.4",
|
|
Payload: []byte(`{"model":"gpt-5.4"}`),
|
|
}
|
|
opts := cliproxyexecutor.Options{Metadata: map[string]any{cliproxyexecutor.ExecutionSessionMetadataKey: "exec-session-1"}}
|
|
|
|
httpReq, _, err := executor.cacheHelper(context.Background(), nil, sdktranslator.FromString("openai"), "https://example.com/responses", req, opts, rawJSON)
|
|
if err != nil {
|
|
t.Fatalf("cacheHelper error: %v", err)
|
|
}
|
|
|
|
body, err := io.ReadAll(httpReq.Body)
|
|
if err != nil {
|
|
t.Fatalf("read request body: %v", err)
|
|
}
|
|
|
|
if got := gjson.GetBytes(body, "prompt_cache_key").String(); got != "exec-session-1" {
|
|
t.Fatalf("prompt_cache_key = %q, want %q", got, "exec-session-1")
|
|
}
|
|
if got := httpReq.Header.Get("session_id"); got != "exec-session-1" {
|
|
t.Fatalf("session_id = %q, want %q", got, "exec-session-1")
|
|
}
|
|
}
|
|
|
|
func TestCodexExecutorCacheHelper_OpenAIChatCompletions_FallsBackToStableAuthID(t *testing.T) {
|
|
executor := &CodexExecutor{}
|
|
rawJSON := []byte(`{"model":"gpt-5.4","stream":true}`)
|
|
req := cliproxyexecutor.Request{
|
|
Model: "gpt-5.4",
|
|
Payload: []byte(`{"model":"gpt-5.4"}`),
|
|
}
|
|
auth := &cliproxyauth.Auth{ID: "codex-auth-1", Provider: "codex"}
|
|
|
|
httpReq, _, err := executor.cacheHelper(context.Background(), auth, sdktranslator.FromString("openai"), "https://example.com/responses", req, cliproxyexecutor.Options{}, rawJSON)
|
|
if err != nil {
|
|
t.Fatalf("cacheHelper error: %v", err)
|
|
}
|
|
|
|
body, err := io.ReadAll(httpReq.Body)
|
|
if err != nil {
|
|
t.Fatalf("read request body: %v", err)
|
|
}
|
|
|
|
expected := uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:auth:codex-auth-1")).String()
|
|
if got := gjson.GetBytes(body, "prompt_cache_key").String(); got != expected {
|
|
t.Fatalf("prompt_cache_key = %q, want %q", got, expected)
|
|
}
|
|
if got := httpReq.Header.Get("session_id"); got != expected {
|
|
t.Fatalf("session_id = %q, want %q", got, expected)
|
|
}
|
|
}
|
|
|
|
func TestCodexExecutorCacheHelper_ClaudePreservesCacheContinuity(t *testing.T) {
|
|
executor := &CodexExecutor{}
|
|
req := cliproxyexecutor.Request{
|
|
Model: "claude-3-7-sonnet",
|
|
Payload: []byte(`{"metadata":{"user_id":"user-1"}}`),
|
|
}
|
|
rawJSON := []byte(`{"model":"gpt-5.4","stream":true}`)
|
|
|
|
httpReq, continuity, err := executor.cacheHelper(context.Background(), nil, sdktranslator.FromString("claude"), "https://example.com/responses", req, cliproxyexecutor.Options{}, rawJSON)
|
|
if err != nil {
|
|
t.Fatalf("cacheHelper error: %v", err)
|
|
}
|
|
if continuity.Key == "" {
|
|
t.Fatal("continuity.Key = empty, want non-empty")
|
|
}
|
|
body, err := io.ReadAll(httpReq.Body)
|
|
if err != nil {
|
|
t.Fatalf("read request body: %v", err)
|
|
}
|
|
if got := gjson.GetBytes(body, "prompt_cache_key").String(); got != continuity.Key {
|
|
t.Fatalf("prompt_cache_key = %q, want %q", got, continuity.Key)
|
|
}
|
|
if got := httpReq.Header.Get("session_id"); got != continuity.Key {
|
|
t.Fatalf("session_id = %q, want %q", got, continuity.Key)
|
|
}
|
|
}
|
|
|
|
func TestResolveCodexContinuity_DoesNotForwardAuthAffinityKey(t *testing.T) {
|
|
req := cliproxyexecutor.Request{Payload: []byte(`{"model":"gpt-5.4"}`)}
|
|
opts := cliproxyexecutor.Options{Metadata: map[string]any{"auth_affinity_key": "principal:raw-client-secret"}}
|
|
auth := &cliproxyauth.Auth{ID: "codex-auth-1", Provider: "codex"}
|
|
|
|
continuity := resolveCodexContinuity(context.Background(), auth, req, opts)
|
|
|
|
if continuity.Source != "auth_id" {
|
|
t.Fatalf("continuity.Source = %q, want %q", continuity.Source, "auth_id")
|
|
}
|
|
if continuity.Key == "principal:raw-client-secret" {
|
|
t.Fatal("continuity.Key leaked raw auth affinity key")
|
|
}
|
|
}
|