chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator"
|
||||
|
||||
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestOpenAIToCodex_PreservesBuiltinTools(t *testing.T) {
|
||||
in := []byte(`{
|
||||
"model":"gpt-5",
|
||||
"messages":[{"role":"user","content":"hi"}],
|
||||
"tools":[{"type":"web_search","search_context_size":"high"}],
|
||||
"tool_choice":{"type":"web_search"}
|
||||
}`)
|
||||
|
||||
out := sdktranslator.TranslateRequest(sdktranslator.FormatOpenAI, sdktranslator.FormatCodex, "gpt-5", in, false)
|
||||
|
||||
if got := gjson.GetBytes(out, "tools.#").Int(); got != 1 {
|
||||
t.Fatalf("expected 1 tool, got %d: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tools.0.type").String(); got != "web_search" {
|
||||
t.Fatalf("expected tools[0].type=web_search, got %q: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tools.0.search_context_size").String(); got != "high" {
|
||||
t.Fatalf("expected tools[0].search_context_size=high, got %q: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tool_choice.type").String(); got != "web_search" {
|
||||
t.Fatalf("expected tool_choice.type=web_search, got %q: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIResponsesToOpenAI_IgnoresBuiltinTools(t *testing.T) {
|
||||
in := []byte(`{
|
||||
"model":"gpt-5",
|
||||
"input":[{"role":"user","content":[{"type":"input_text","text":"hi"}]}],
|
||||
"tools":[{"type":"web_search","search_context_size":"low"}]
|
||||
}`)
|
||||
|
||||
out := sdktranslator.TranslateRequest(sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAI, "gpt-5", in, false)
|
||||
|
||||
if got := gjson.GetBytes(out, "tools.#").Int(); got != 0 {
|
||||
t.Fatalf("expected 0 tools (builtin tools not supported in Chat Completions), got %d: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type jsonObject = map[string]any
|
||||
|
||||
func loadClaudeCodeSentinelFixture(t *testing.T, name string) jsonObject {
|
||||
t.Helper()
|
||||
path := filepath.Join("testdata", "claude_code_sentinels", name)
|
||||
data := mustReadFile(t, path)
|
||||
var payload jsonObject
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
t.Fatalf("unmarshal %s: %v", name, err)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func mustReadFile(t *testing.T, path string) []byte {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func requireStringField(t *testing.T, obj jsonObject, key string) string {
|
||||
t.Helper()
|
||||
value, ok := obj[key].(string)
|
||||
if !ok || value == "" {
|
||||
t.Fatalf("field %q missing or empty: %#v", key, obj[key])
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func TestClaudeCodeSentinel_ToolProgressShape(t *testing.T) {
|
||||
payload := loadClaudeCodeSentinelFixture(t, "tool_progress.json")
|
||||
if got := requireStringField(t, payload, "type"); got != "tool_progress" {
|
||||
t.Fatalf("type = %q, want tool_progress", got)
|
||||
}
|
||||
requireStringField(t, payload, "tool_use_id")
|
||||
requireStringField(t, payload, "tool_name")
|
||||
requireStringField(t, payload, "session_id")
|
||||
if _, ok := payload["elapsed_time_seconds"].(float64); !ok {
|
||||
t.Fatalf("elapsed_time_seconds missing or non-number: %#v", payload["elapsed_time_seconds"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeCodeSentinel_SessionStateShape(t *testing.T) {
|
||||
payload := loadClaudeCodeSentinelFixture(t, "session_state_changed.json")
|
||||
if got := requireStringField(t, payload, "type"); got != "system" {
|
||||
t.Fatalf("type = %q, want system", got)
|
||||
}
|
||||
if got := requireStringField(t, payload, "subtype"); got != "session_state_changed" {
|
||||
t.Fatalf("subtype = %q, want session_state_changed", got)
|
||||
}
|
||||
state := requireStringField(t, payload, "state")
|
||||
switch state {
|
||||
case "idle", "running", "requires_action":
|
||||
default:
|
||||
t.Fatalf("unexpected session state %q", state)
|
||||
}
|
||||
requireStringField(t, payload, "session_id")
|
||||
}
|
||||
|
||||
func TestClaudeCodeSentinel_ToolUseSummaryShape(t *testing.T) {
|
||||
payload := loadClaudeCodeSentinelFixture(t, "tool_use_summary.json")
|
||||
if got := requireStringField(t, payload, "type"); got != "tool_use_summary" {
|
||||
t.Fatalf("type = %q, want tool_use_summary", got)
|
||||
}
|
||||
requireStringField(t, payload, "summary")
|
||||
rawIDs, ok := payload["preceding_tool_use_ids"].([]any)
|
||||
if !ok || len(rawIDs) == 0 {
|
||||
t.Fatalf("preceding_tool_use_ids missing or empty: %#v", payload["preceding_tool_use_ids"])
|
||||
}
|
||||
for i, raw := range rawIDs {
|
||||
if id, ok := raw.(string); !ok || id == "" {
|
||||
t.Fatalf("preceding_tool_use_ids[%d] invalid: %#v", i, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeCodeSentinel_ControlRequestCanUseToolShape(t *testing.T) {
|
||||
payload := loadClaudeCodeSentinelFixture(t, "control_request_can_use_tool.json")
|
||||
if got := requireStringField(t, payload, "type"); got != "control_request" {
|
||||
t.Fatalf("type = %q, want control_request", got)
|
||||
}
|
||||
requireStringField(t, payload, "request_id")
|
||||
request, ok := payload["request"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("request missing or invalid: %#v", payload["request"])
|
||||
}
|
||||
if got := requireStringField(t, request, "subtype"); got != "can_use_tool" {
|
||||
t.Fatalf("request.subtype = %q, want can_use_tool", got)
|
||||
}
|
||||
requireStringField(t, request, "tool_name")
|
||||
requireStringField(t, request, "tool_use_id")
|
||||
if input, ok := request["input"].(map[string]any); !ok || len(input) == 0 {
|
||||
t.Fatalf("request.input missing or empty: %#v", request["input"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "control_request",
|
||||
"request_id": "req_123",
|
||||
"request": {
|
||||
"subtype": "can_use_tool",
|
||||
"tool_name": "Bash",
|
||||
"input": {"command": "npm test"},
|
||||
"tool_use_id": "toolu_123",
|
||||
"description": "Running npm test"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "system",
|
||||
"subtype": "session_state_changed",
|
||||
"state": "requires_action",
|
||||
"uuid": "22222222-2222-4222-8222-222222222222",
|
||||
"session_id": "sess_123"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"type": "tool_progress",
|
||||
"tool_use_id": "toolu_123",
|
||||
"tool_name": "Bash",
|
||||
"parent_tool_use_id": null,
|
||||
"elapsed_time_seconds": 2.5,
|
||||
"task_id": "task_123",
|
||||
"uuid": "11111111-1111-4111-8111-111111111111",
|
||||
"session_id": "sess_123"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "tool_use_summary",
|
||||
"summary": "Searched in auth/",
|
||||
"preceding_tool_use_ids": ["toolu_1", "toolu_2"],
|
||||
"uuid": "33333333-3333-4333-8333-333333333333",
|
||||
"session_id": "sess_123"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,122 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
|
||||
runtimeexecutor "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor"
|
||||
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"
|
||||
)
|
||||
|
||||
func TestGeminiExecutorRecordsSuccessfulZeroUsageInQueue(t *testing.T) {
|
||||
model := fmt.Sprintf("gemini-2.5-flash-zero-usage-%d", time.Now().UnixNano())
|
||||
source := fmt.Sprintf("zero-usage-%d@example.com", time.Now().UnixNano())
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
wantPath := "/v1beta/models/" + model + ":generateContent"
|
||||
if r.URL.Path != wantPath {
|
||||
t.Fatalf("path = %q, want %q", r.URL.Path, wantPath)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":0,"candidatesTokenCount":0,"totalTokenCount":0}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
executor := runtimeexecutor.NewGeminiExecutor(&config.Config{})
|
||||
auth := &cliproxyauth.Auth{
|
||||
Provider: "gemini",
|
||||
Attributes: map[string]string{
|
||||
"api_key": "test-upstream-key",
|
||||
"base_url": server.URL,
|
||||
},
|
||||
Metadata: map[string]any{
|
||||
"email": source,
|
||||
},
|
||||
}
|
||||
|
||||
prevQueueEnabled := redisqueue.Enabled()
|
||||
prevUsageEnabled := redisqueue.UsageStatisticsEnabled()
|
||||
redisqueue.SetEnabled(false)
|
||||
redisqueue.SetEnabled(true)
|
||||
redisqueue.SetUsageStatisticsEnabled(true)
|
||||
t.Cleanup(func() {
|
||||
redisqueue.SetEnabled(false)
|
||||
redisqueue.SetEnabled(prevQueueEnabled)
|
||||
redisqueue.SetUsageStatisticsEnabled(prevUsageEnabled)
|
||||
})
|
||||
|
||||
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
||||
Model: model,
|
||||
Payload: []byte(`{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}`),
|
||||
}, cliproxyexecutor.Options{
|
||||
SourceFormat: sdktranslator.FormatGemini,
|
||||
OriginalRequest: []byte(`{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}`),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Execute error: %v", err)
|
||||
}
|
||||
|
||||
waitForQueuedUsageModelTotalTokens(t, "gemini", model, 0)
|
||||
}
|
||||
|
||||
func waitForQueuedUsageModelTotalTokens(t *testing.T, wantProvider, wantModel string, wantTokens int64) {
|
||||
t.Helper()
|
||||
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
items := redisqueue.PopOldest(10)
|
||||
for _, item := range items {
|
||||
got, ok := parseQueuedUsagePayload(t, item)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if got.Provider != wantProvider || got.Model != wantModel {
|
||||
continue
|
||||
}
|
||||
if got.Failed {
|
||||
t.Fatalf("payload failed = true, want false")
|
||||
}
|
||||
if got.Tokens.TotalTokens != wantTokens {
|
||||
t.Fatalf("payload total tokens = %d, want %d", got.Tokens.TotalTokens, wantTokens)
|
||||
}
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
t.Fatalf("timed out waiting for queued usage payload for provider=%q model=%q", wantProvider, wantModel)
|
||||
}
|
||||
|
||||
type queuedUsagePayload struct {
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model"`
|
||||
Failed bool `json:"failed"`
|
||||
Tokens struct {
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
} `json:"tokens"`
|
||||
}
|
||||
|
||||
func parseQueuedUsagePayload(t *testing.T, payload []byte) (queuedUsagePayload, bool) {
|
||||
t.Helper()
|
||||
|
||||
var parsed queuedUsagePayload
|
||||
if len(payload) == 0 {
|
||||
return parsed, false
|
||||
}
|
||||
if err := json.Unmarshal(payload, &parsed); err != nil {
|
||||
return parsed, false
|
||||
}
|
||||
if parsed.Provider == "" || parsed.Model == "" {
|
||||
return parsed, false
|
||||
}
|
||||
return parsed, true
|
||||
}
|
||||
Reference in New Issue
Block a user