fix(copilot): route claude models through native messages

This commit is contained in:
JokerRun
2026-04-12 04:11:03 +08:00
parent 1d8e68ad15
commit 1c6c3675d1
4 changed files with 276 additions and 9 deletions

View File

@@ -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{}