mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-19 05:56:43 +08:00
Move shared Codex, Claude, and xAI provider options under upstream so they apply consistently to OAuth and API-key credentials. Migrate historical OAuth paths, preserve OAuth-only header scope, and expose the new upstream management API path.
120 lines
5.4 KiB
Go
120 lines
5.4 KiB
Go
package executor
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v8/internal/config"
|
|
"github.com/router-for-me/CLIProxyAPI/v8/internal/registry"
|
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
|
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executor"
|
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v8/sdk/translator"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
func TestV8SharedUpstreamRequestSettingsAffectBothAuthKinds(t *testing.T) {
|
|
for _, provider := range []string{"codex", "xai"} {
|
|
for _, kind := range []string{cliproxyauth.AuthKindAPIKey, cliproxyauth.AuthKindOAuth} {
|
|
for _, stream := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("%s/%s/stream=%t", provider, kind, stream), func(t *testing.T) {
|
|
captured := make(chan []byte, 1)
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
captured <- body
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_, _ = w.Write([]byte(`{"error":{"message":"captured request"}}`))
|
|
}))
|
|
defer server.Close()
|
|
cfg, err := config.ParseConfigBytes([]byte(`proxy-url: direct
|
|
disable-image-generation: true
|
|
client: {codex: {optimize-multi-agent-v2: true}}
|
|
upstream:
|
|
codex: {orphan-delegation-compatibility: true}
|
|
xai: {inject-x-search: true}
|
|
`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
auth := &cliproxyauth.Auth{ID: t.Name(), Provider: provider, Status: cliproxyauth.StatusActive, ProxyURL: "direct", Attributes: map[string]string{"auth_kind": kind, "base_url": server.URL}}
|
|
if kind == cliproxyauth.AuthKindAPIKey {
|
|
auth.Attributes["api_key"] = "test-key"
|
|
} else {
|
|
auth.Metadata = map[string]any{"access_token": "test-token"}
|
|
}
|
|
var executor cliproxyauth.ProviderExecutor = NewCodexExecutor(cfg)
|
|
if provider == "xai" {
|
|
executor = NewXAIExecutor(cfg)
|
|
}
|
|
manager := cliproxyauth.NewManager(nil, nil, nil)
|
|
manager.SetConfig(cfg)
|
|
manager.RegisterExecutor(executor)
|
|
if _, err = manager.Register(context.Background(), auth); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
registry.GetGlobalRegistry().RegisterClient(auth.ID, provider, []*registry.ModelInfo{{ID: "gpt-5.4"}})
|
|
t.Cleanup(func() { registry.GetGlobalRegistry().UnregisterClient(auth.ID) })
|
|
req := cliproxyexecutor.Request{Model: "gpt-5.4", Payload: []byte(`{
|
|
"input":[{"type":"function_call_output","name":"create_thread","namespace":"codex_app","output":"<codex_delegation>task</codex_delegation>"}],
|
|
"tools":[{"type":"namespace","name":"collaboration","tools":[{"type":"function","name":"spawn_agent","description":"Spawns an agent.","parameters":{"type":"object","properties":{"message":{"type":"string","encrypted":true}}}}]}]
|
|
}`)}
|
|
opts := cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatOpenAIResponse, Headers: http.Header{"User-Agent": {"codex_cli_rs/0.144.1"}, "X-Openai-Subagent": {"collab_spawn"}}}
|
|
if stream {
|
|
_, err = manager.ExecuteStream(context.Background(), []string{provider}, req, opts)
|
|
} else {
|
|
_, err = manager.Execute(context.Background(), []string{provider}, req, opts)
|
|
}
|
|
if err == nil {
|
|
t.Fatal("expected the test upstream's rejection")
|
|
}
|
|
var body []byte
|
|
select {
|
|
case body = <-captured:
|
|
default:
|
|
t.Fatalf("request did not reach upstream: %v", err)
|
|
}
|
|
if got := gjson.GetBytes(body, "input.0.type").String(); got != "message" {
|
|
t.Fatalf("shared orphan delegation type = %q; body=%s", got, body)
|
|
}
|
|
if provider == "codex" {
|
|
if got := gjson.GetBytes(body, "tools.0.name").String(); got != "collaboration-optimize" {
|
|
t.Fatalf("client collaboration namespace = %q, want collaboration-optimize", got)
|
|
}
|
|
} else if !gjson.GetBytes(body, `tools.#(type=="x_search")`).Exists() {
|
|
t.Fatal("shared x_search setting was not applied")
|
|
}
|
|
if !cfg.Client.Codex.OptimizeMultiAgentV2 || !cfg.Codex.OrphanDelegationCompatibility || !cfg.XAI.InjectXSearch {
|
|
t.Fatal("request changed shared configuration")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestV8ScopedExecutorsPreserveWebsocketStores(t *testing.T) {
|
|
cfg, err := config.ParseConfigBytes([]byte("upstream: {codex: {response-steering: true}, xai: {inject-x-search: true}}\noauth: {providers: {codex: {header-defaults: {user-agent: oauth-agent}}}}"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
codex := NewCodexAutoExecutor(cfg)
|
|
boundCodex := codex.ForAPIKey().(*CodexAutoExecutor)
|
|
if boundCodex.wsExec.store != codex.wsExec.store || !boundCodex.httpExec.cfg.Codex.ResponseSteering || !boundCodex.wsExec.cfg.Codex.ResponseSteering || boundCodex.httpExec.cfg.CodexHeaderDefaults.UserAgent != "" {
|
|
t.Fatal("scoped Codex executor lost sessions or inherited OAuth settings")
|
|
}
|
|
xai := NewXAIAutoExecutor(cfg)
|
|
boundXAI := xai.ForAPIKey().(*XAIAutoExecutor)
|
|
if boundXAI.wsExec.store != xai.wsExec.store || boundXAI.wsExec.idStore != xai.wsExec.idStore || !boundXAI.httpExec.cfg.XAI.InjectXSearch || !boundXAI.wsExec.cfg.XAI.InjectXSearch || boundXAI.httpExec.cfg.CodexHeaderDefaults.UserAgent != "" {
|
|
t.Fatal("scoped xAI executor lost sessions or inherited OAuth settings")
|
|
}
|
|
if codex.httpExec.cfg != cfg || codex.wsExec.cfg != cfg || xai.httpExec.cfg != cfg || xai.wsExec.cfg != cfg || !cfg.Codex.ResponseSteering || !cfg.XAI.InjectXSearch {
|
|
t.Fatal("scoping changed the registered executors")
|
|
}
|
|
}
|