mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-05-23 21:01:01 +08:00
- Introduced Codex client models framework in `openai` package. - Added JSON-based model definitions (`codex_client_models.json`) for Codex, including metadata, reasoning levels, and configuration options. - Implemented handlers to load, clone, and build Codex client models with support for visibility overrides and metadata application. - Enabled sorting and prioritization of models based on configuration or runtime criteria. - Added utility functions for managing and validating model attributes.
483 lines
17 KiB
Go
483 lines
17 KiB
Go
package executor
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
|
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator"
|
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
|
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
func TestXAIExecutorExecuteShapesResponsesRequest(t *testing.T) {
|
|
var gotPath string
|
|
var gotAuth string
|
|
var gotGrokConvID string
|
|
var gotOriginator string
|
|
var gotAccountID string
|
|
var gotBody []byte
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.Path
|
|
gotAuth = r.Header.Get("Authorization")
|
|
gotGrokConvID = r.Header.Get("x-grok-conv-id")
|
|
gotOriginator = r.Header.Get("Originator")
|
|
gotAccountID = r.Header.Get("Chatgpt-Account-Id")
|
|
var errRead error
|
|
gotBody, errRead = io.ReadAll(r.Body)
|
|
if errRead != nil {
|
|
t.Fatalf("read body: %v", errRead)
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
_, _ = w.Write([]byte("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"object\":\"response\",\"created_at\":0,\"status\":\"completed\",\"model\":\"grok-4.3\",\"output\":[{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"ok\"}]}],\"usage\":{\"input_tokens\":1,\"output_tokens\":1,\"total_tokens\":2}}}\n\n"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
exec := NewXAIExecutor(&config.Config{})
|
|
auth := &cliproxyauth.Auth{
|
|
ID: "xai-auth",
|
|
Provider: "xai",
|
|
Attributes: map[string]string{
|
|
"base_url": server.URL,
|
|
"auth_kind": "oauth",
|
|
},
|
|
Metadata: map[string]any{
|
|
"access_token": "xai-token",
|
|
"email": "user@example.com",
|
|
},
|
|
}
|
|
|
|
_, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
|
Model: "grok-4.3",
|
|
Payload: []byte(`{"model":"grok-4.3","input":"hello","include":["reasoning.encrypted_content"],"reasoning":{"effort":"high"},"tools":[{"type":"tool_search"},{"type":"image_generation"},{"type":"custom","name":"apply_patch"},{"type":"custom","name":"custom_lookup"},{"type":"function","name":"lookup"},{"type":"web_search","external_web_access":true,"search_content_types":["text","image"]}]}`),
|
|
}, cliproxyexecutor.Options{
|
|
SourceFormat: sdktranslator.FormatOpenAIResponse,
|
|
Stream: false,
|
|
Metadata: map[string]any{
|
|
cliproxyexecutor.ExecutionSessionMetadataKey: "conv-xai-1",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
|
|
if gotPath != "/responses" {
|
|
t.Fatalf("path = %q, want /responses", gotPath)
|
|
}
|
|
if gotAuth != "Bearer xai-token" {
|
|
t.Fatalf("Authorization = %q, want Bearer xai-token", gotAuth)
|
|
}
|
|
if gotGrokConvID != "conv-xai-1" {
|
|
t.Fatalf("x-grok-conv-id = %q, want conv-xai-1", gotGrokConvID)
|
|
}
|
|
if gotOriginator != "" {
|
|
t.Fatalf("Originator = %q, want empty", gotOriginator)
|
|
}
|
|
if gotAccountID != "" {
|
|
t.Fatalf("Chatgpt-Account-Id = %q, want empty", gotAccountID)
|
|
}
|
|
if gjson.GetBytes(gotBody, "prompt_cache_key").String() != "conv-xai-1" {
|
|
t.Fatalf("prompt_cache_key missing from body: %s", string(gotBody))
|
|
}
|
|
if !gjson.GetBytes(gotBody, "stream").Bool() {
|
|
t.Fatalf("stream = false, want true; body=%s", string(gotBody))
|
|
}
|
|
if gjson.GetBytes(gotBody, "reasoning.effort").String() != "high" {
|
|
t.Fatalf("reasoning.effort = %q, want high; body=%s", gjson.GetBytes(gotBody, "reasoning.effort").String(), string(gotBody))
|
|
}
|
|
tools := gjson.GetBytes(gotBody, "tools").Array()
|
|
if len(tools) != 3 {
|
|
t.Fatalf("tools length = %d, want 3; body=%s", len(tools), string(gotBody))
|
|
}
|
|
for i, tool := range tools {
|
|
toolType := tool.Get("type").String()
|
|
if toolType == "image_generation" {
|
|
t.Fatalf("tools.%d.type = image_generation, want removed; body=%s", i, string(gotBody))
|
|
}
|
|
if toolType != "function" && toolType != "web_search" {
|
|
t.Fatalf("tools.%d.type = %q, want function or web_search; body=%s", i, toolType, string(gotBody))
|
|
}
|
|
if got := tool.Get("name").String(); got == "apply_patch" {
|
|
t.Fatalf("tools.%d.name = apply_patch, want removed; body=%s", i, string(gotBody))
|
|
}
|
|
if toolType == "web_search" {
|
|
if tool.Get("external_web_access").Exists() {
|
|
t.Fatalf("tools.%d.external_web_access exists, want removed; body=%s", i, string(gotBody))
|
|
}
|
|
if got := tool.Get("search_content_types.1").String(); got != "image" {
|
|
t.Fatalf("tools.%d.search_content_types missing image entry; body=%s", i, string(gotBody))
|
|
}
|
|
}
|
|
}
|
|
for _, include := range gjson.GetBytes(gotBody, "include").Array() {
|
|
if include.String() == "reasoning.encrypted_content" {
|
|
t.Fatalf("xai request must not ask for encrypted reasoning content: %s", string(gotBody))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestXAIExecutorOmitsUnsupportedReasoningEffort(t *testing.T) {
|
|
var gotBody []byte
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var errRead error
|
|
gotBody, errRead = io.ReadAll(r.Body)
|
|
if errRead != nil {
|
|
t.Fatalf("read body: %v", errRead)
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
_, _ = w.Write([]byte("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"object\":\"response\",\"created_at\":0,\"status\":\"completed\",\"model\":\"grok-4\",\"output\":[{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"ok\"}]}]}}\n\n"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
exec := NewXAIExecutor(&config.Config{})
|
|
auth := &cliproxyauth.Auth{
|
|
Provider: "xai",
|
|
Attributes: map[string]string{
|
|
"base_url": server.URL,
|
|
"auth_kind": "oauth",
|
|
},
|
|
Metadata: map[string]any{"access_token": "xai-token"},
|
|
}
|
|
|
|
_, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
|
Model: "grok-4",
|
|
Payload: []byte(`{"model":"grok-4","input":"hello","reasoning":{"effort":"high"}}`),
|
|
}, cliproxyexecutor.Options{
|
|
SourceFormat: sdktranslator.FormatOpenAIResponse,
|
|
Stream: false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
|
|
if gjson.GetBytes(gotBody, "reasoning").Exists() {
|
|
t.Fatalf("unsupported xAI model must omit reasoning key: %s", string(gotBody))
|
|
}
|
|
}
|
|
|
|
func TestXAIExecutorExecuteStreamFiltersToolSearchTool(t *testing.T) {
|
|
var gotBody []byte
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var errRead error
|
|
gotBody, errRead = io.ReadAll(r.Body)
|
|
if errRead != nil {
|
|
t.Fatalf("read body: %v", errRead)
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
_, _ = w.Write([]byte("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"object\":\"response\",\"created_at\":0,\"status\":\"completed\",\"model\":\"grok-4.3\",\"output\":[{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"ok\"}]}]}}\n\n"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
exec := NewXAIExecutor(&config.Config{})
|
|
auth := &cliproxyauth.Auth{
|
|
Provider: "xai",
|
|
Attributes: map[string]string{"base_url": server.URL},
|
|
Metadata: map[string]any{"access_token": "xai-token"},
|
|
}
|
|
|
|
result, err := exec.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
|
|
Model: "grok-4.3",
|
|
Payload: []byte(`{"model":"grok-4.3","input":"hello","tools":[{"type":"tool_search"},{"type":"image_generation"},{"type":"custom","name":"apply_patch"},{"type":"custom","name":"custom_lookup"},{"type":"function","name":"lookup"},{"type":"web_search","external_web_access":true,"search_content_types":["text","image"]}]}`),
|
|
}, cliproxyexecutor.Options{
|
|
SourceFormat: sdktranslator.FormatOpenAIResponse,
|
|
Stream: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ExecuteStream() error = %v", err)
|
|
}
|
|
for chunk := range result.Chunks {
|
|
if chunk.Err != nil {
|
|
t.Fatalf("stream chunk error = %v", chunk.Err)
|
|
}
|
|
}
|
|
|
|
tools := gjson.GetBytes(gotBody, "tools").Array()
|
|
if len(tools) != 3 {
|
|
t.Fatalf("tools length = %d, want 3; body=%s", len(tools), string(gotBody))
|
|
}
|
|
for i, tool := range tools {
|
|
toolType := tool.Get("type").String()
|
|
if toolType == "image_generation" {
|
|
t.Fatalf("tools.%d.type = image_generation, want removed; body=%s", i, string(gotBody))
|
|
}
|
|
if toolType != "function" && toolType != "web_search" {
|
|
t.Fatalf("tools.%d.type = %q, want function or web_search; body=%s", i, toolType, string(gotBody))
|
|
}
|
|
if got := tool.Get("name").String(); got == "apply_patch" {
|
|
t.Fatalf("tools.%d.name = apply_patch, want removed; body=%s", i, string(gotBody))
|
|
}
|
|
if toolType == "web_search" {
|
|
if tool.Get("external_web_access").Exists() {
|
|
t.Fatalf("tools.%d.external_web_access exists, want removed; body=%s", i, string(gotBody))
|
|
}
|
|
if got := tool.Get("search_content_types.1").String(); got != "image" {
|
|
t.Fatalf("tools.%d.search_content_types missing image entry; body=%s", i, string(gotBody))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestXAIExecutorExecuteImagesUsesImagesEndpoint(t *testing.T) {
|
|
var gotPath string
|
|
var gotAuth string
|
|
var gotAccept string
|
|
var gotBody []byte
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.Path
|
|
gotAuth = r.Header.Get("Authorization")
|
|
gotAccept = r.Header.Get("Accept")
|
|
var errRead error
|
|
gotBody, errRead = io.ReadAll(r.Body)
|
|
if errRead != nil {
|
|
t.Fatalf("read body: %v", errRead)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"created":123,"data":[{"b64_json":"AA=="}]}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
exec := NewXAIExecutor(&config.Config{})
|
|
auth := &cliproxyauth.Auth{
|
|
Provider: "xai",
|
|
Attributes: map[string]string{
|
|
"base_url": server.URL,
|
|
"auth_kind": "oauth",
|
|
},
|
|
Metadata: map[string]any{"access_token": "xai-token"},
|
|
}
|
|
|
|
resp, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
|
Model: "grok-imagine-image",
|
|
Payload: []byte(`{"model":"grok-imagine-image","prompt":"draw"}`),
|
|
}, cliproxyexecutor.Options{
|
|
SourceFormat: sdktranslator.FromString("openai-image"),
|
|
Metadata: map[string]any{
|
|
cliproxyexecutor.RequestPathMetadataKey: "/v1/images/generations",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
|
|
if gotPath != "/images/generations" {
|
|
t.Fatalf("path = %q, want /images/generations", gotPath)
|
|
}
|
|
if gotAuth != "Bearer xai-token" {
|
|
t.Fatalf("Authorization = %q, want Bearer xai-token", gotAuth)
|
|
}
|
|
if gotAccept != "application/json" {
|
|
t.Fatalf("Accept = %q, want application/json", gotAccept)
|
|
}
|
|
if string(gotBody) != `{"model":"grok-imagine-image","prompt":"draw"}` {
|
|
t.Fatalf("body = %s", string(gotBody))
|
|
}
|
|
if gjson.GetBytes(resp.Payload, "data.0.b64_json").String() != "AA==" {
|
|
t.Fatalf("payload = %s", string(resp.Payload))
|
|
}
|
|
}
|
|
|
|
func TestXAIExecutorExecuteImagesUsesEditsEndpoint(t *testing.T) {
|
|
var gotPath string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.Path
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"created":123,"data":[{"url":"https://x.ai/image.png"}]}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
exec := NewXAIExecutor(&config.Config{})
|
|
auth := &cliproxyauth.Auth{
|
|
Provider: "xai",
|
|
Attributes: map[string]string{"base_url": server.URL},
|
|
Metadata: map[string]any{"access_token": "xai-token"},
|
|
}
|
|
|
|
_, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
|
Model: "grok-imagine-image",
|
|
Payload: []byte(`{"model":"grok-imagine-image","prompt":"edit","image":{"type":"image_url","url":"https://example.com/a.png"}}`),
|
|
}, cliproxyexecutor.Options{
|
|
SourceFormat: sdktranslator.FromString("openai-image"),
|
|
Metadata: map[string]any{
|
|
cliproxyexecutor.RequestPathMetadataKey: "/v1/images/edits",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
|
|
if gotPath != "/images/edits" {
|
|
t.Fatalf("path = %q, want /images/edits", gotPath)
|
|
}
|
|
}
|
|
|
|
func TestXAIExecutorExecuteVideosCreate(t *testing.T) {
|
|
var gotPath string
|
|
var gotMethod string
|
|
var gotAuth string
|
|
var gotIdempotencyKey string
|
|
var gotBody []byte
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.Path
|
|
gotMethod = r.Method
|
|
gotAuth = r.Header.Get("Authorization")
|
|
gotIdempotencyKey = r.Header.Get("x-idempotency-key")
|
|
var errRead error
|
|
gotBody, errRead = io.ReadAll(r.Body)
|
|
if errRead != nil {
|
|
t.Fatalf("read body: %v", errRead)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"request_id":"vid_123"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
exec := NewXAIExecutor(&config.Config{})
|
|
auth := &cliproxyauth.Auth{
|
|
Provider: "xai",
|
|
Attributes: map[string]string{"base_url": server.URL},
|
|
Metadata: map[string]any{"access_token": "xai-token"},
|
|
}
|
|
|
|
resp, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
|
Model: "grok-imagine-video",
|
|
Payload: []byte(`{"model":"grok-imagine-video","prompt":"animate","duration":4}`),
|
|
}, cliproxyexecutor.Options{
|
|
SourceFormat: sdktranslator.FromString("openai-video"),
|
|
Metadata: map[string]any{
|
|
"idempotency_key": "idem-123",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
|
|
if gotMethod != http.MethodPost {
|
|
t.Fatalf("method = %q, want POST", gotMethod)
|
|
}
|
|
if gotPath != "/videos/generations" {
|
|
t.Fatalf("path = %q, want /videos/generations", gotPath)
|
|
}
|
|
if gotAuth != "Bearer xai-token" {
|
|
t.Fatalf("Authorization = %q, want Bearer xai-token", gotAuth)
|
|
}
|
|
if gotIdempotencyKey != "idem-123" {
|
|
t.Fatalf("x-idempotency-key = %q, want idem-123", gotIdempotencyKey)
|
|
}
|
|
if string(gotBody) != `{"model":"grok-imagine-video","prompt":"animate","duration":4}` {
|
|
t.Fatalf("body = %s", string(gotBody))
|
|
}
|
|
if gjson.GetBytes(resp.Payload, "request_id").String() != "vid_123" {
|
|
t.Fatalf("payload = %s", string(resp.Payload))
|
|
}
|
|
}
|
|
|
|
func TestXAIExecutorExecuteVideosRetrieve(t *testing.T) {
|
|
var gotPath string
|
|
var gotMethod string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.Path
|
|
gotMethod = r.Method
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"status":"done","video":{"url":"https://vidgen.x.ai/video.mp4","duration":6},"model":"grok-imagine-video","progress":100}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
exec := NewXAIExecutor(&config.Config{})
|
|
auth := &cliproxyauth.Auth{
|
|
Provider: "xai",
|
|
Attributes: map[string]string{"base_url": server.URL},
|
|
Metadata: map[string]any{"access_token": "xai-token"},
|
|
}
|
|
|
|
resp, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
|
Model: "grok-imagine-video",
|
|
Payload: []byte(`{"request_id":"vid_123"}`),
|
|
}, cliproxyexecutor.Options{
|
|
SourceFormat: sdktranslator.FromString("openai-video"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
|
|
if gotMethod != http.MethodGet {
|
|
t.Fatalf("method = %q, want GET", gotMethod)
|
|
}
|
|
if gotPath != "/videos/vid_123" {
|
|
t.Fatalf("path = %q, want /videos/vid_123", gotPath)
|
|
}
|
|
if gjson.GetBytes(resp.Payload, "video.url").String() != "https://vidgen.x.ai/video.mp4" {
|
|
t.Fatalf("payload = %s", string(resp.Payload))
|
|
}
|
|
}
|
|
|
|
func TestXAIExecutorExecuteVideosUsesNativeEndpointFromRequestPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
requestPath string
|
|
wantPath string
|
|
}{
|
|
{
|
|
name: "generations",
|
|
requestPath: "/v1/videos/generations",
|
|
wantPath: "/videos/generations",
|
|
},
|
|
{
|
|
name: "edits",
|
|
requestPath: "/v1/videos/edits",
|
|
wantPath: "/videos/edits",
|
|
},
|
|
{
|
|
name: "extensions",
|
|
requestPath: "/v1/videos/extensions",
|
|
wantPath: "/videos/extensions",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var gotPath string
|
|
var gotMethod string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.Path
|
|
gotMethod = r.Method
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"request_id":"vid_123"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
exec := NewXAIExecutor(&config.Config{})
|
|
auth := &cliproxyauth.Auth{
|
|
Provider: "xai",
|
|
Attributes: map[string]string{"base_url": server.URL},
|
|
Metadata: map[string]any{"access_token": "xai-token"},
|
|
}
|
|
|
|
_, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
|
Model: "grok-imagine-video",
|
|
Payload: []byte(`{"model":"grok-imagine-video","prompt":"animate"}`),
|
|
}, cliproxyexecutor.Options{
|
|
SourceFormat: sdktranslator.FromString("openai-video"),
|
|
Metadata: map[string]any{
|
|
cliproxyexecutor.RequestPathMetadataKey: tt.requestPath,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
|
|
if gotMethod != http.MethodPost {
|
|
t.Fatalf("method = %q, want POST", gotMethod)
|
|
}
|
|
if gotPath != tt.wantPath {
|
|
t.Fatalf("path = %q, want %s", gotPath, tt.wantPath)
|
|
}
|
|
})
|
|
}
|
|
}
|