mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-06-09 01:14:56 +08:00
fix(copilot): route claude models through native messages
This commit is contained in:
@@ -2,12 +2,17 @@ package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
copilotauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/copilot"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
||||
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"
|
||||
@@ -618,6 +623,144 @@ func TestCountTokens_ClaudeSourceFormatTranslates(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubCopilotExecute_ClaudeModelUsesNativeGateway(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var gotPath string
|
||||
var gotQuery string
|
||||
var gotAuth string
|
||||
var gotAPIVersion string
|
||||
var gotEditorVersion string
|
||||
var gotIntent string
|
||||
var gotInitiator string
|
||||
var gotBody []byte
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
gotQuery = r.URL.RawQuery
|
||||
gotAuth = r.Header.Get("Authorization")
|
||||
gotAPIVersion = r.Header.Get("X-Github-Api-Version")
|
||||
gotEditorVersion = r.Header.Get("Editor-Version")
|
||||
gotIntent = r.Header.Get("Openai-Intent")
|
||||
gotInitiator = r.Header.Get("X-Initiator")
|
||||
gotBody, _ = io.ReadAll(r.Body)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-sonnet-4.6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
e := NewGitHubCopilotExecutor(&config.Config{})
|
||||
e.cache["gh-access-token"] = &cachedAPIToken{
|
||||
token: "copilot-api-token",
|
||||
apiEndpoint: server.URL,
|
||||
expiresAt: time.Now().Add(time.Hour),
|
||||
}
|
||||
auth := &cliproxyauth.Auth{Metadata: map[string]any{"access_token": "gh-access-token"}}
|
||||
payload := []byte(`{"model":"claude-sonnet-4.6","max_tokens":256,"messages":[{"role":"user","content":"hello"}]}`)
|
||||
|
||||
resp, err := e.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
||||
Model: "claude-sonnet-4.6",
|
||||
Payload: payload,
|
||||
}, cliproxyexecutor.Options{
|
||||
SourceFormat: sdktranslator.FromString("claude"),
|
||||
OriginalRequest: payload,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Execute() error: %v", err)
|
||||
}
|
||||
|
||||
if gotPath != "/v1/messages" {
|
||||
t.Fatalf("path = %q, want %q", gotPath, "/v1/messages")
|
||||
}
|
||||
if gotQuery != "beta=true" {
|
||||
t.Fatalf("query = %q, want %q", gotQuery, "beta=true")
|
||||
}
|
||||
if gotAuth != "Bearer copilot-api-token" {
|
||||
t.Fatalf("Authorization = %q, want %q", gotAuth, "Bearer copilot-api-token")
|
||||
}
|
||||
if gotAPIVersion != copilotGitHubAPIVer {
|
||||
t.Fatalf("X-Github-Api-Version = %q, want %q", gotAPIVersion, copilotGitHubAPIVer)
|
||||
}
|
||||
if gotEditorVersion != copilotEditorVersion {
|
||||
t.Fatalf("Editor-Version = %q, want %q", gotEditorVersion, copilotEditorVersion)
|
||||
}
|
||||
if gotIntent != copilotOpenAIIntent {
|
||||
t.Fatalf("Openai-Intent = %q, want %q", gotIntent, copilotOpenAIIntent)
|
||||
}
|
||||
if gotInitiator != "user" {
|
||||
t.Fatalf("X-Initiator = %q, want %q", gotInitiator, "user")
|
||||
}
|
||||
if gjson.GetBytes(gotBody, "model").String() != "claude-sonnet-4.6" {
|
||||
t.Fatalf("upstream model = %q, want %q", gjson.GetBytes(gotBody, "model").String(), "claude-sonnet-4.6")
|
||||
}
|
||||
if gjson.GetBytes(resp.Payload, "content.0.text").String() != "ok" {
|
||||
t.Fatalf("response text = %q, want %q", gjson.GetBytes(resp.Payload, "content.0.text").String(), "ok")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubCopilotExecuteStream_ClaudeModelUsesNativeGateway(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var gotPath string
|
||||
var gotInitiator string
|
||||
var gotAPIVersion string
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
gotInitiator = r.Header.Get("X-Initiator")
|
||||
gotAPIVersion = r.Header.Get("X-Github-Api-Version")
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
_, _ = w.Write([]byte("event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4.6\",\"content\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":0}}}\n\n"))
|
||||
_, _ = w.Write([]byte("event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n"))
|
||||
_, _ = w.Write([]byte("event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"ok\"}}\n\n"))
|
||||
_, _ = w.Write([]byte("event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\n"))
|
||||
_, _ = w.Write([]byte("event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":1}}\n\n"))
|
||||
_, _ = w.Write([]byte("event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
e := NewGitHubCopilotExecutor(&config.Config{})
|
||||
e.cache["gh-access-token"] = &cachedAPIToken{
|
||||
token: "copilot-api-token",
|
||||
apiEndpoint: server.URL,
|
||||
expiresAt: time.Now().Add(time.Hour),
|
||||
}
|
||||
auth := &cliproxyauth.Auth{Metadata: map[string]any{"access_token": "gh-access-token"}}
|
||||
payload := []byte(`{"model":"claude-sonnet-4.6","stream":true,"max_tokens":256,"messages":[{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"Read","input":{"path":"notes.txt"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"file contents"}]}]}`)
|
||||
|
||||
result, err := e.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
|
||||
Model: "claude-sonnet-4.6",
|
||||
Payload: payload,
|
||||
}, cliproxyexecutor.Options{
|
||||
SourceFormat: sdktranslator.FromString("claude"),
|
||||
OriginalRequest: payload,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteStream() error: %v", err)
|
||||
}
|
||||
|
||||
var joined strings.Builder
|
||||
for chunk := range result.Chunks {
|
||||
if chunk.Err != nil {
|
||||
t.Fatalf("stream chunk error: %v", chunk.Err)
|
||||
}
|
||||
joined.Write(chunk.Payload)
|
||||
}
|
||||
|
||||
if gotPath != "/v1/messages" {
|
||||
t.Fatalf("path = %q, want %q", gotPath, "/v1/messages")
|
||||
}
|
||||
if gotInitiator != "agent" {
|
||||
t.Fatalf("X-Initiator = %q, want %q", gotInitiator, "agent")
|
||||
}
|
||||
if gotAPIVersion != copilotGitHubAPIVer {
|
||||
t.Fatalf("X-Github-Api-Version = %q, want %q", gotAPIVersion, copilotGitHubAPIVer)
|
||||
}
|
||||
if !strings.Contains(joined.String(), "message_start") || !strings.Contains(joined.String(), "text_delta") {
|
||||
t.Fatalf("stream = %q, want Claude SSE payload", joined.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountTokens_EmptyPayload(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := &GitHubCopilotExecutor{}
|
||||
|
||||
Reference in New Issue
Block a user