6 Commits
Author SHA1 Message Date
Sakurasan 1f5e1c221c fix: classify chatgpt-4o-latest 2024-12-20 03:49:30 +08:00
Sakurasan 24bac8e38d update: openai struct 2024-12-20 03:25:56 +08:00
Sakurasan fb5b1a55ae update openai struct 2024-12-20 02:42:49 +08:00
Sakurasan 07bcabc5d4 fix: model prefix 2024-12-18 15:58:43 +08:00
Sakurasan 8f6f3313f5 fix: custom_endpoint 2024-12-18 01:08:23 +08:00
Sakurasan f8b364a052 fix: custom_endpoint 2024-12-18 01:00:45 +08:00
8 changed files with 129 additions and 129 deletions
+3 -5
View File
@@ -77,9 +77,6 @@ wget https://github.com/mirrors2/opencatd-open/raw/main/docker/docker-compose.ym
- [Fly.io](https://fly.io/) - [Fly.io](https://fly.io/)
- 或者其他 - 或者其他
修改openai的endpoint地址?使用任意上游地址(套娃代理)
- 设置环境变量 openai_endpoint
使用Nginx + Docker部署 使用Nginx + Docker部署
- [使用Nginx + Docker部署](./doc/deploy.md) - [使用Nginx + Docker部署](./doc/deploy.md)
@@ -87,12 +84,13 @@ pandora for team
- [pandora for team](./doc/pandora.md) - [pandora for team](./doc/pandora.md)
如何自定义HOST地址? (仅OpenAI) 如何自定义HOST地址? (仅OpenAI)
- 需修改环境变量,优先级递增 - 需修改环境变量,优先级递增(全局配置谨慎修改)
- Cloudflare AI Gateway地址 `AIGateWay_Endpoint=https://gateway.ai.cloudflare.com/v1/123456789/xxxx/openai/chat/completions` - Cloudflare AI Gateway地址 `AIGateWay_Endpoint=https://gateway.ai.cloudflare.com/v1/123456789/xxxx/openai/chat/completions`
- 自定义的endpoint `$CUSTOM_ENDPOINT=true && $OpenAI_Endpoint=https://your.domain/v1/chat/completions` - 自定义的endpoint `OpenAI_Endpoint=https://your.domain/v1/chat/completions`
设置主页跳转地址? 设置主页跳转地址?
- 修改环境变量 `CUSTOM_REDIRECT=https://your.domain` - 修改环境变量 `CUSTOM_REDIRECT=https://your.domain`
## 获取更多信息 ## 获取更多信息
[![TG](https://telegram.org/img/favicon.ico)](https://t.me/OpenTeamLLM) [![TG](https://telegram.org/img/favicon.ico)](https://t.me/OpenTeamLLM)
+43 -37
View File
@@ -143,51 +143,57 @@ func ChatMessages(c *gin.Context, chatReq *openai.ChatCompletionRequest) {
claudReq.Model = "" claudReq.Model = ""
} }
var prompt string
var claudecontent []VisionContent var claudecontent []VisionContent
var prompt string
for _, msg := range chatReq.Messages { for _, msg := range chatReq.Messages {
if msg.Role == "system" { switch ct := msg.Content.(type) {
claudReq.System = string(msg.Content) case string:
continue prompt += "<" + msg.Role + ">: " + msg.Content.(string) + "\n"
} if msg.Role == "system" {
claudReq.System = msg.Content.(string)
var oaivisioncontent []openai.VisionContent continue
if err := json.Unmarshal(msg.Content, &oaivisioncontent); err != nil { }
prompt += "<" + msg.Role + ">: " + string(msg.Content) + "\n" claudecontent = append(claudecontent, VisionContent{Type: "text", Text: msg.Role + ":" + msg.Content.(string)})
case []any:
claudecontent = append(claudecontent, VisionContent{Type: "text", Text: msg.Role + ":" + string(msg.Content)}) for _, item := range ct {
} else { if m, ok := item.(map[string]interface{}); ok {
if len(oaivisioncontent) > 0 { if m["type"] == "text" {
for _, content := range oaivisioncontent { prompt += "<" + msg.Role + ">: " + m["text"].(string) + "\n"
if content.Type == "text" { claudecontent = append(claudecontent, VisionContent{Type: "text", Text: msg.Role + ":" + m["text"].(string)})
prompt += "<" + msg.Role + ">: " + content.Text + "\n" } else if m["type"] == "image_url" {
claudecontent = append(claudecontent, VisionContent{Type: "text", Text: msg.Role + ":" + content.Text}) if url, ok := m["image_url"].(map[string]interface{}); ok {
} else if content.Type == "image_url" { fmt.Printf(" URL: %v\n", url["url"])
if strings.HasPrefix(content.ImageURL.URL, "http") { if strings.HasPrefix(url["url"].(string), "http") {
fmt.Println("链接:", content.ImageURL.URL) fmt.Println("网络图片:", url["url"].(string))
} else if strings.HasPrefix(content.ImageURL.URL, "data:image") { } else if strings.HasPrefix(url["url"].(string), "data:image") {
fmt.Println("base64:", content.ImageURL.URL[:20]) fmt.Println("base64:", url["url"].(string)[:20])
var mediaType string
if strings.HasPrefix(url["url"].(string), "data:image/jpeg") {
mediaType = "image/jpeg"
}
if strings.HasPrefix(url["url"].(string), "data:image/png") {
mediaType = "image/png"
}
claudecontent = append(claudecontent, VisionContent{Type: "image", Source: &VisionSource{Type: "base64", MediaType: mediaType, Data: strings.Split(url["url"].(string), ",")[1]}})
}
} }
// todo image tokens
var mediaType string
if strings.HasPrefix(content.ImageURL.URL, "data:image/jpeg") {
mediaType = "image/jpeg"
}
if strings.HasPrefix(content.ImageURL.URL, "data:image/png") {
mediaType = "image/png"
}
claudecontent = append(claudecontent, VisionContent{Type: "image", Source: &VisionSource{Type: "base64", MediaType: mediaType, Data: strings.Split(content.ImageURL.URL, ",")[1]}})
} }
} }
} }
default:
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": "Invalid content type",
},
})
return
}
if len(chatReq.Tools) > 0 {
tooljson, _ := json.Marshal(chatReq.Tools)
prompt += "<tools>: " + string(tooljson) + "\n"
} }
// if len(chatReq.Tools) > 0 {
// tooljson, _ := json.Marshal(chatReq.Tools)
// prompt += "<tools>: " + string(tooljson) + "\n"
// }
} }
claudReq.Messages = []VisionMessages{{Role: "user", Content: claudecontent}} claudReq.Messages = []VisionMessages{{Role: "user", Content: claudecontent}}
usagelog.PromptCount = tokenizer.NumTokensFromStr(prompt, chatReq.Model) usagelog.PromptCount = tokenizer.NumTokensFromStr(prompt, chatReq.Model)
+41 -44
View File
@@ -97,59 +97,55 @@ func ChatProxy(c *gin.Context, chatReq *openai.ChatCompletionRequest) {
var prompts []genai.Part var prompts []genai.Part
var prompt string var prompt string
for _, msg := range chatReq.Messages { for _, msg := range chatReq.Messages {
var visioncontent []openai.VisionContent switch ct := msg.Content.(type) {
if err := json.Unmarshal(msg.Content, &visioncontent); err != nil { case string:
prompt += "<" + msg.Role + ">: " + string(msg.Content) + "\n" prompt += "<" + msg.Role + ">: " + msg.Content.(string) + "\n"
prompts = append(prompts, genai.Text("<"+msg.Role+">: "+string(msg.Content))) prompts = append(prompts, genai.Text("<"+msg.Role+">: "+msg.Content.(string)))
} else { case []any:
if len(visioncontent) > 0 { for _, item := range ct {
for _, content := range visioncontent { if m, ok := item.(map[string]interface{}); ok {
if content.Type == "text" { if m["type"] == "text" {
prompt += "<" + msg.Role + ">: " + content.Text + "\n" prompt += "<" + msg.Role + ">: " + m["text"].(string) + "\n"
prompts = append(prompts, genai.Text("<"+msg.Role+">: "+content.Text)) prompts = append(prompts, genai.Text("<"+msg.Role+">: "+m["text"].(string)))
} else if content.Type == "image_url" { } else if m["type"] == "image_url" {
if strings.HasPrefix(content.ImageURL.URL, "http") { if url, ok := m["image_url"].(map[string]interface{}); ok {
fmt.Println("链接:", content.ImageURL.URL) if strings.HasPrefix(url["url"].(string), "http") {
} else if strings.HasPrefix(content.ImageURL.URL, "data:image") { fmt.Println("网络图片:", url["url"].(string))
fmt.Println("base64:", content.ImageURL.URL[:20]) } else if strings.HasPrefix(url["url"].(string), "data:image") {
if chatReq.Model != "gemini-pro-vision" { fmt.Println("base64:", url["url"].(string)[:20])
chatReq.Model = "gemini-pro-vision" var mime string
// openai 会以 data:image 开头,则去掉 data:image/png;base64, 和 data:image/jpeg;base64,
if strings.HasPrefix(url["url"].(string), "data:image/png") {
mime = "image/png"
} else if strings.HasPrefix(url["url"].(string), "data:image/jpeg") {
mime = "image/jpeg"
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Unsupported image format"})
return
}
imageString := strings.Split(url["url"].(string), ",")[1]
imageBytes, err := base64.StdEncoding.DecodeString(imageString)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
prompts = append(prompts, genai.Blob{MIMEType: mime, Data: imageBytes})
} }
var mime string
// openai 会以 data:image 开头,则去掉 data:image/png;base64, 和 data:image/jpeg;base64,
if strings.HasPrefix(content.ImageURL.URL, "data:image/png") {
mime = "image/png"
} else if strings.HasPrefix(content.ImageURL.URL, "data:image/jpeg") {
mime = "image/jpeg"
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Unsupported image format"})
return
}
imageString := strings.Split(content.ImageURL.URL, ",")[1]
imageBytes, err := base64.StdEncoding.DecodeString(imageString)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
prompts = append(prompts, genai.Blob{MIMEType: mime, Data: imageBytes})
} }
// todo image tokens
} }
} }
} }
default:
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": "Invalid content type",
},
})
return
} }
if len(chatReq.Tools) > 0 { if len(chatReq.Tools) > 0 {
tooljson, _ := json.Marshal(chatReq.Tools) tooljson, _ := json.Marshal(chatReq.Tools)
prompt += "<tools>: " + string(tooljson) + "\n" prompt += "<tools>: " + string(tooljson) + "\n"
// for _, tool := range chatReq.Tools {
// }
} }
} }
@@ -171,6 +167,7 @@ func ChatProxy(c *gin.Context, chatReq *openai.ChatCompletionRequest) {
defer client.Close() defer client.Close()
model := client.GenerativeModel(chatReq.Model) model := client.GenerativeModel(chatReq.Model)
model.Tools = []*genai.Tool{}
iter := model.GenerateContentStream(ctx, prompts...) iter := model.GenerateContentStream(ctx, prompts...)
datachan := make(chan string) datachan := make(chan string)
+34 -30
View File
@@ -25,13 +25,13 @@ const (
) )
var ( var (
BaseURL string // "https://api.openai.com" Custom_Endpoint string
AIGateWay_Endpoint = "https://gateway.ai.cloudflare.com/v1/431ba10f11200d544922fbca177aaa7f/openai/openai/chat/completions" AIGateWay_Endpoint string // "https://gateway.ai.cloudflare.com/v1/431ba10f11200d544922fbca177aaa7f/openai/openai/chat/completions"
) )
func init() { func init() {
if os.Getenv("OpenAI_Endpoint") != "" { if os.Getenv("OpenAI_Endpoint") != "" {
BaseURL = os.Getenv("OpenAI_Endpoint") Custom_Endpoint = os.Getenv("OpenAI_Endpoint")
} }
if os.Getenv("AIGateWay_Endpoint") != "" { if os.Getenv("AIGateWay_Endpoint") != "" {
AIGateWay_Endpoint = os.Getenv("AIGateWay_Endpoint") AIGateWay_Endpoint = os.Getenv("AIGateWay_Endpoint")
@@ -50,9 +50,9 @@ type VisionImageURL struct {
} }
type ChatCompletionMessage struct { type ChatCompletionMessage struct {
Role string `json:"role"` Role string `json:"role"`
Content json.RawMessage `json:"content"` Content any `json:"content"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
// MultiContent []VisionContent // MultiContent []VisionContent
} }
@@ -185,27 +185,31 @@ func ChatProxy(c *gin.Context, chatReq *ChatCompletionRequest) {
var prompt string var prompt string
for _, msg := range chatReq.Messages { for _, msg := range chatReq.Messages {
// prompt += "<" + msg.Role + ">: " + msg.Content + "\n" switch ct := msg.Content.(type) {
var visioncontent []VisionContent case string:
if err := json.Unmarshal(msg.Content, &visioncontent); err != nil { prompt += "<" + msg.Role + ">: " + msg.Content.(string) + "\n"
prompt += "<" + msg.Role + ">: " + string(msg.Content) + "\n" case []any:
} else { for _, item := range ct {
if len(visioncontent) > 0 { if m, ok := item.(map[string]interface{}); ok {
for _, content := range visioncontent { if m["type"] == "text" {
if content.Type == "text" { prompt += "<" + msg.Role + ">: " + m["text"].(string) + "\n"
prompt += "<" + msg.Role + ">: " + content.Text + "\n" } else if m["type"] == "image_url" {
} else if content.Type == "image_url" { if url, ok := m["image_url"].(map[string]interface{}); ok {
if strings.HasPrefix(content.ImageURL.URL, "http") { fmt.Printf(" URL: %v\n", url["url"])
fmt.Println("链接:", content.ImageURL.URL) if strings.HasPrefix(url["url"].(string), "http") {
} else if strings.HasPrefix(content.ImageURL.URL, "data:image") { fmt.Println("网络图片:", url["url"].(string))
fmt.Println("base64:", content.ImageURL.URL[:20]) }
} }
// todo image tokens
} }
} }
} }
default:
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": "Invalid content type",
},
})
return
} }
if len(chatReq.Tools) > 0 { if len(chatReq.Tools) > 0 {
tooljson, _ := json.Marshal(chatReq.Tools) tooljson, _ := json.Marshal(chatReq.Tools)
@@ -247,16 +251,16 @@ func ChatProxy(c *gin.Context, chatReq *ChatCompletionRequest) {
req.Header = c.Request.Header req.Header = c.Request.Header
req.Header.Set("api-key", onekey.Key) req.Header.Set("api-key", onekey.Key)
default: default:
req, err = http.NewRequest(c.Request.Method, OpenAI_Endpoint, bytes.NewReader(chatReq.ToByteJson())) req, err = http.NewRequest(c.Request.Method, OpenAI_Endpoint, bytes.NewReader(chatReq.ToByteJson())) // default endpoint
if onekey.EndPoint != "" { // 优先key的endpoint
req, err = http.NewRequest(c.Request.Method, onekey.EndPoint+c.Request.RequestURI, bytes.NewReader(chatReq.ToByteJson()))
}
if AIGateWay_Endpoint != "" { // cloudflare gateway的endpoint if AIGateWay_Endpoint != "" { // cloudflare gateway的endpoint
req, err = http.NewRequest(c.Request.Method, AIGateWay_Endpoint, bytes.NewReader(chatReq.ToByteJson())) req, err = http.NewRequest(c.Request.Method, AIGateWay_Endpoint, bytes.NewReader(chatReq.ToByteJson()))
} }
customEndpoint := os.Getenv("CUSTOM_ENDPOINT") // 最后是用户自定义的endpoint CUSTOM_ENDPOINT=true OpenAI_Endpoint if Custom_Endpoint != "" { // 自定义endpoint
if customEndpoint == "true" && OpenAI_Endpoint != "" { req, err = http.NewRequest(c.Request.Method, Custom_Endpoint, bytes.NewReader(chatReq.ToByteJson()))
req, err = http.NewRequest(c.Request.Method, BaseURL, bytes.NewReader(chatReq.ToByteJson())) }
if onekey.EndPoint != "" { // 优先key的endpoint
req, err = http.NewRequest(c.Request.Method, onekey.EndPoint+c.Request.RequestURI, bytes.NewReader(chatReq.ToByteJson()))
} }
req.Header = c.Request.Header req.Header = c.Request.Header
+2
View File
@@ -183,6 +183,8 @@ func Cost(model string, promptCount, completionCount int) float64 {
cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion) cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion)
case "gemini-2.0-flash-exp": case "gemini-2.0-flash-exp":
cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion) cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion)
case "gemini-2.0-flash-thinking-exp-1219":
cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion)
case "learnlm-1.5-pro-experimental", " gemini-exp-1114", "gemini-exp-1121", "gemini-exp-1206": case "learnlm-1.5-pro-experimental", " gemini-exp-1114", "gemini-exp-1121", "gemini-exp-1206":
cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion) cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion)
+1 -1
View File
@@ -18,7 +18,7 @@ func ChatHandler(c *gin.Context) {
return return
} }
if strings.HasPrefix(chatreq.Model, "gpt") || strings.HasPrefix(chatreq.Model, "o1-") { if strings.Contains(chatreq.Model, "gpt") || strings.HasPrefix(chatreq.Model, "o1-") {
openai.ChatProxy(c, &chatreq) openai.ChatProxy(c, &chatreq)
return return
} }
-11
View File
@@ -2,22 +2,18 @@ package router
import ( import (
"crypto/tls" "crypto/tls"
"fmt"
"log"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"opencatd-open/pkg/claude" "opencatd-open/pkg/claude"
oai "opencatd-open/pkg/openai" oai "opencatd-open/pkg/openai"
"opencatd-open/store" "opencatd-open/store"
"os"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var ( var (
baseUrl = "https://api.openai.com"
GPT3Dot5Turbo = "gpt-3.5-turbo" GPT3Dot5Turbo = "gpt-3.5-turbo"
GPT4 = "gpt-4" GPT4 = "gpt-4"
) )
@@ -62,13 +58,6 @@ var (
// } `json:"usage"` // } `json:"usage"`
// } // }
func init() {
if openai_endpoint := os.Getenv("openai_endpoint"); openai_endpoint != "" {
log.Println(fmt.Sprintf("replace %s to %s", baseUrl, openai_endpoint))
baseUrl = openai_endpoint
}
}
func HandleProxy(c *gin.Context) { func HandleProxy(c *gin.Context) {
var ( var (
localuser bool localuser bool
+5 -1
View File
@@ -4,6 +4,7 @@ import (
"errors" "errors"
"log" "log"
"math/rand" "math/rand"
"os"
"strings" "strings"
"time" "time"
@@ -76,6 +77,9 @@ func SelectKeyCache(apitype string) (Key, error) {
func SelectKeyCacheByModel(model string) (Key, error) { func SelectKeyCacheByModel(model string) (Key, error) {
var keys []Key var keys []Key
if os.Getenv("OPENAI_API_KEY") != "" {
keys = append(keys, Key{ApiType: "openai", Key: os.Getenv("OPENAI_API_KEY")})
}
items := KeysCache.Items() items := KeysCache.Items()
for _, item := range items { for _, item := range items {
if strings.Contains(model, "realtime") { if strings.Contains(model, "realtime") {
@@ -97,7 +101,7 @@ func SelectKeyCacheByModel(model string) (Key, error) {
keys = append(keys, item.Object.(Key)) keys = append(keys, item.Object.(Key))
} }
} }
if strings.HasPrefix(model, "o1-") || model == "chatgpt-4o-latest" { if strings.HasPrefix(model, "o1-") || strings.HasPrefix(model, "chatgpt-") {
if item.Object.(Key).ApiType == "openai" { if item.Object.(Key).ApiType == "openai" {
keys = append(keys, item.Object.(Key)) keys = append(keys, item.Object.(Key))
} }