mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-03-26 05:46:19 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cef2aeeb08 | ||
|
|
bcd1e8cc34 | ||
|
|
198b3f4a40 | ||
|
|
9fee7f488e | ||
|
|
1b46d39b8b | ||
|
|
b5701f416b | ||
|
|
4b1a404fcb |
6
.github/workflows/docker-image.yml
vendored
6
.github/workflows/docker-image.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Generate Build Metadata
|
||||
run: |
|
||||
echo VERSION=`git describe --tags --always --dirty` >> $GITHUB_ENV
|
||||
echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_ENV
|
||||
echo COMMIT=`git rev-parse --short HEAD` >> $GITHUB_ENV
|
||||
echo BUILD_DATE=`date -u +%Y-%m-%dT%H:%M:%SZ` >> $GITHUB_ENV
|
||||
- name: Build and push (amd64)
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Generate Build Metadata
|
||||
run: |
|
||||
echo VERSION=`git describe --tags --always --dirty` >> $GITHUB_ENV
|
||||
echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_ENV
|
||||
echo COMMIT=`git rev-parse --short HEAD` >> $GITHUB_ENV
|
||||
echo BUILD_DATE=`date -u +%Y-%m-%dT%H:%M:%SZ` >> $GITHUB_ENV
|
||||
- name: Build and push (arm64)
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Generate Build Metadata
|
||||
run: |
|
||||
echo VERSION=`git describe --tags --always --dirty` >> $GITHUB_ENV
|
||||
echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_ENV
|
||||
echo COMMIT=`git rev-parse --short HEAD` >> $GITHUB_ENV
|
||||
echo BUILD_DATE=`date -u +%Y-%m-%dT%H:%M:%SZ` >> $GITHUB_ENV
|
||||
- name: Create and push multi-arch manifests
|
||||
|
||||
5
.github/workflows/release.yaml
vendored
5
.github/workflows/release.yaml
vendored
@@ -27,15 +27,14 @@ jobs:
|
||||
cache: true
|
||||
- name: Generate Build Metadata
|
||||
run: |
|
||||
VERSION=$(git describe --tags --always --dirty)
|
||||
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_ENV
|
||||
echo COMMIT=`git rev-parse --short HEAD` >> $GITHUB_ENV
|
||||
echo BUILD_DATE=`date -u +%Y-%m-%dT%H:%M:%SZ` >> $GITHUB_ENV
|
||||
- uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
args: release --clean --skip=validate
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION: ${{ env.VERSION }}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
version: 2
|
||||
|
||||
builds:
|
||||
- id: "cli-proxy-api-plus"
|
||||
env:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package management
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
@@ -56,3 +57,57 @@ func TestAPICallTransportInvalidAuthFallsBackToGlobalProxy(t *testing.T) {
|
||||
t.Fatalf("proxy URL = %v, want http://global-proxy.example.com:8080", proxyURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthByIndexDistinguishesSharedAPIKeysAcrossProviders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
manager := coreauth.NewManager(nil, nil, nil)
|
||||
geminiAuth := &coreauth.Auth{
|
||||
ID: "gemini:apikey:123",
|
||||
Provider: "gemini",
|
||||
Attributes: map[string]string{
|
||||
"api_key": "shared-key",
|
||||
},
|
||||
}
|
||||
compatAuth := &coreauth.Auth{
|
||||
ID: "openai-compatibility:bohe:456",
|
||||
Provider: "bohe",
|
||||
Label: "bohe",
|
||||
Attributes: map[string]string{
|
||||
"api_key": "shared-key",
|
||||
"compat_name": "bohe",
|
||||
"provider_key": "bohe",
|
||||
},
|
||||
}
|
||||
|
||||
if _, errRegister := manager.Register(context.Background(), geminiAuth); errRegister != nil {
|
||||
t.Fatalf("register gemini auth: %v", errRegister)
|
||||
}
|
||||
if _, errRegister := manager.Register(context.Background(), compatAuth); errRegister != nil {
|
||||
t.Fatalf("register compat auth: %v", errRegister)
|
||||
}
|
||||
|
||||
geminiIndex := geminiAuth.EnsureIndex()
|
||||
compatIndex := compatAuth.EnsureIndex()
|
||||
if geminiIndex == compatIndex {
|
||||
t.Fatalf("shared api key produced duplicate auth_index %q", geminiIndex)
|
||||
}
|
||||
|
||||
h := &Handler{authManager: manager}
|
||||
|
||||
gotGemini := h.authByIndex(geminiIndex)
|
||||
if gotGemini == nil {
|
||||
t.Fatal("expected gemini auth by index")
|
||||
}
|
||||
if gotGemini.ID != geminiAuth.ID {
|
||||
t.Fatalf("authByIndex(gemini) returned %q, want %q", gotGemini.ID, geminiAuth.ID)
|
||||
}
|
||||
|
||||
gotCompat := h.authByIndex(compatIndex)
|
||||
if gotCompat == nil {
|
||||
t.Fatal("expected compat auth by index")
|
||||
}
|
||||
if gotCompat.ID != compatAuth.ID {
|
||||
t.Fatalf("authByIndex(compat) returned %q, want %q", gotCompat.ID, compatAuth.ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,21 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
||||
part, _ = sjson.Set(part, "functionResponse.name", funcName)
|
||||
part, _ = sjson.Set(part, "functionResponse.response.result", responseData)
|
||||
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
||||
|
||||
case "image":
|
||||
source := contentResult.Get("source")
|
||||
if source.Get("type").String() != "base64" {
|
||||
return true
|
||||
}
|
||||
mimeType := source.Get("media_type").String()
|
||||
data := source.Get("data").String()
|
||||
if mimeType == "" || data == "" {
|
||||
return true
|
||||
}
|
||||
part := `{"inline_data":{"mime_type":"","data":""}}`
|
||||
part, _ = sjson.Set(part, "inline_data.mime_type", mimeType)
|
||||
part, _ = sjson.Set(part, "inline_data.data", data)
|
||||
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -40,3 +40,41 @@ func TestConvertClaudeRequestToGemini_ToolChoice_SpecificTool(t *testing.T) {
|
||||
t.Fatalf("Expected allowedFunctionNames ['json'], got %s", gjson.GetBytes(output, "toolConfig.functionCallingConfig.allowedFunctionNames").Raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToGemini_ImageContent(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"model": "gemini-3-flash-preview",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "describe this image"},
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/png",
|
||||
"data": "aGVsbG8="
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
output := ConvertClaudeRequestToGemini("gemini-3-flash-preview", inputJSON, false)
|
||||
|
||||
parts := gjson.GetBytes(output, "contents.0.parts").Array()
|
||||
if len(parts) != 2 {
|
||||
t.Fatalf("Expected 2 parts, got %d", len(parts))
|
||||
}
|
||||
if got := parts[0].Get("text").String(); got != "describe this image" {
|
||||
t.Fatalf("Expected first part text 'describe this image', got '%s'", got)
|
||||
}
|
||||
if got := parts[1].Get("inline_data.mime_type").String(); got != "image/png" {
|
||||
t.Fatalf("Expected image mime type 'image/png', got '%s'", got)
|
||||
}
|
||||
if got := parts[1].Get("inline_data.data").String(); got != "aGVsbG8=" {
|
||||
t.Fatalf("Expected image data 'aGVsbG8=', got '%s'", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +162,60 @@ func stableAuthIndex(seed string) string {
|
||||
return hex.EncodeToString(sum[:8])
|
||||
}
|
||||
|
||||
// EnsureIndex returns a stable index derived from the auth file name or API key.
|
||||
func (a *Auth) indexSeed() string {
|
||||
if a == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if fileName := strings.TrimSpace(a.FileName); fileName != "" {
|
||||
return "file:" + fileName
|
||||
}
|
||||
|
||||
providerKey := strings.ToLower(strings.TrimSpace(a.Provider))
|
||||
compatName := ""
|
||||
baseURL := ""
|
||||
apiKey := ""
|
||||
source := ""
|
||||
if a.Attributes != nil {
|
||||
if value := strings.TrimSpace(a.Attributes["provider_key"]); value != "" {
|
||||
providerKey = strings.ToLower(value)
|
||||
}
|
||||
compatName = strings.ToLower(strings.TrimSpace(a.Attributes["compat_name"]))
|
||||
baseURL = strings.TrimSpace(a.Attributes["base_url"])
|
||||
apiKey = strings.TrimSpace(a.Attributes["api_key"])
|
||||
source = strings.TrimSpace(a.Attributes["source"])
|
||||
}
|
||||
|
||||
proxyURL := strings.TrimSpace(a.ProxyURL)
|
||||
hasCredentialIdentity := compatName != "" || baseURL != "" || proxyURL != "" || apiKey != "" || source != ""
|
||||
if providerKey != "" && hasCredentialIdentity {
|
||||
parts := []string{"provider=" + providerKey}
|
||||
if compatName != "" {
|
||||
parts = append(parts, "compat="+compatName)
|
||||
}
|
||||
if baseURL != "" {
|
||||
parts = append(parts, "base="+baseURL)
|
||||
}
|
||||
if proxyURL != "" {
|
||||
parts = append(parts, "proxy="+proxyURL)
|
||||
}
|
||||
if apiKey != "" {
|
||||
parts = append(parts, "api_key="+apiKey)
|
||||
}
|
||||
if source != "" {
|
||||
parts = append(parts, "source="+source)
|
||||
}
|
||||
return "config:" + strings.Join(parts, "\x00")
|
||||
}
|
||||
|
||||
if id := strings.TrimSpace(a.ID); id != "" {
|
||||
return "id:" + id
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// EnsureIndex returns a stable index derived from the auth file name or credential identity.
|
||||
func (a *Auth) EnsureIndex() string {
|
||||
if a == nil {
|
||||
return ""
|
||||
@@ -171,20 +224,9 @@ func (a *Auth) EnsureIndex() string {
|
||||
return a.Index
|
||||
}
|
||||
|
||||
seed := strings.TrimSpace(a.FileName)
|
||||
if seed != "" {
|
||||
seed = "file:" + seed
|
||||
} else if a.Attributes != nil {
|
||||
if apiKey := strings.TrimSpace(a.Attributes["api_key"]); apiKey != "" {
|
||||
seed = "api_key:" + apiKey
|
||||
}
|
||||
}
|
||||
seed := a.indexSeed()
|
||||
if seed == "" {
|
||||
if id := strings.TrimSpace(a.ID); id != "" {
|
||||
seed = "id:" + id
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
idx := stableAuthIndex(seed)
|
||||
|
||||
@@ -33,3 +33,66 @@ func TestToolPrefixDisabled(t *testing.T) {
|
||||
t.Error("should return false when set to false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureIndexUsesCredentialIdentity(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
geminiAuth := &Auth{
|
||||
Provider: "gemini",
|
||||
Attributes: map[string]string{
|
||||
"api_key": "shared-key",
|
||||
"source": "config:gemini[abc123]",
|
||||
},
|
||||
}
|
||||
compatAuth := &Auth{
|
||||
Provider: "bohe",
|
||||
Attributes: map[string]string{
|
||||
"api_key": "shared-key",
|
||||
"compat_name": "bohe",
|
||||
"provider_key": "bohe",
|
||||
"source": "config:bohe[def456]",
|
||||
},
|
||||
}
|
||||
geminiAltBase := &Auth{
|
||||
Provider: "gemini",
|
||||
Attributes: map[string]string{
|
||||
"api_key": "shared-key",
|
||||
"base_url": "https://alt.example.com",
|
||||
"source": "config:gemini[ghi789]",
|
||||
},
|
||||
}
|
||||
geminiDuplicate := &Auth{
|
||||
Provider: "gemini",
|
||||
Attributes: map[string]string{
|
||||
"api_key": "shared-key",
|
||||
"source": "config:gemini[abc123-1]",
|
||||
},
|
||||
}
|
||||
|
||||
geminiIndex := geminiAuth.EnsureIndex()
|
||||
compatIndex := compatAuth.EnsureIndex()
|
||||
altBaseIndex := geminiAltBase.EnsureIndex()
|
||||
duplicateIndex := geminiDuplicate.EnsureIndex()
|
||||
|
||||
if geminiIndex == "" {
|
||||
t.Fatal("gemini index should not be empty")
|
||||
}
|
||||
if compatIndex == "" {
|
||||
t.Fatal("compat index should not be empty")
|
||||
}
|
||||
if altBaseIndex == "" {
|
||||
t.Fatal("alt base index should not be empty")
|
||||
}
|
||||
if duplicateIndex == "" {
|
||||
t.Fatal("duplicate index should not be empty")
|
||||
}
|
||||
if geminiIndex == compatIndex {
|
||||
t.Fatalf("shared api key produced duplicate auth_index %q", geminiIndex)
|
||||
}
|
||||
if geminiIndex == altBaseIndex {
|
||||
t.Fatalf("same provider/key with different base_url produced duplicate auth_index %q", geminiIndex)
|
||||
}
|
||||
if geminiIndex == duplicateIndex {
|
||||
t.Fatalf("duplicate config entries should be separated by source-derived seed, got %q", geminiIndex)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user