mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 05:43:42 +08:00 
			
		
		
		
	Compare commits
	
		
			16 Commits
		
	
	
		
			v0.6.11-pr
			...
			v0.6.11-pr
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 517f6ad211 | ||
|  | 10aba11f18 | ||
|  | 4d011c5f98 | ||
|  | eb96aa635e | ||
|  | c715f2bc1d | ||
|  | aed090dd55 | ||
|  | 696265774e | ||
|  | 974729426d | ||
|  | 57c1367ec8 | ||
|  | 44233d5c04 | ||
|  | bf45a955c3 | ||
|  | 20435fcbfc | ||
|  | 6e7a1c2323 | ||
|  | dd65b997dd | ||
|  | 0b6d03d6c6 | ||
|  | 4375246e24 | 
							
								
								
									
										4
									
								
								.github/workflows/docker-image.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/docker-image.yml
									
									
									
									
										vendored
									
									
								
							| @@ -62,9 +62,7 @@ jobs: | ||||
|         uses: docker/build-push-action@v3 | ||||
|         with: | ||||
|           context: . | ||||
|           platforms: ${{ contains(github.ref, 'alpha') && 'linux/amd64' || 'linux/amd64,linux/arm64' }} | ||||
|           platforms: ${{ contains(github.ref, 'alpha') && 'linux/amd64' || 'linux/amd64' }} | ||||
|           push: true | ||||
|           tags: ${{ steps.meta.outputs.tags }} | ||||
|           labels: ${{ steps.meta.outputs.labels }} | ||||
|           build-args: | | ||||
|             TARGETARCH=${{ startsWith(matrix.platform, 'linux/arm64') && 'arm64' || 'amd64' }} | ||||
| @@ -24,8 +24,7 @@ RUN apk add --no-cache \ | ||||
|  | ||||
| ENV GO111MODULE=on \ | ||||
|     CGO_ENABLED=1 \ | ||||
|     GOOS=linux \ | ||||
|     GOARCH=$TARGETARCH | ||||
|     GOOS=linux | ||||
|  | ||||
| WORKDIR /build | ||||
|  | ||||
|   | ||||
							
								
								
									
										13
									
								
								common/utils/array.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								common/utils/array.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| package utils | ||||
|  | ||||
| func DeDuplication(slice []string) []string { | ||||
| 	m := make(map[string]bool) | ||||
| 	for _, v := range slice { | ||||
| 		m[v] = true | ||||
| 	} | ||||
| 	result := make([]string, 0, len(m)) | ||||
| 	for v := range m { | ||||
| 		result = append(result, v) | ||||
| 	} | ||||
| 	return result | ||||
| } | ||||
| @@ -112,6 +112,13 @@ type DeepSeekUsageResponse struct { | ||||
| 	} `json:"balance_infos"` | ||||
| } | ||||
|  | ||||
| type OpenRouterResponse struct { | ||||
| 	Data struct { | ||||
| 		TotalCredits float64 `json:"total_credits"` | ||||
| 		TotalUsage   float64 `json:"total_usage"` | ||||
| 	} `json:"data"` | ||||
| } | ||||
|  | ||||
| // GetAuthHeader get auth header | ||||
| func GetAuthHeader(token string) http.Header { | ||||
| 	h := http.Header{} | ||||
| @@ -285,6 +292,22 @@ func updateChannelDeepSeekBalance(channel *model.Channel) (float64, error) { | ||||
| 	return balance, nil | ||||
| } | ||||
|  | ||||
| func updateChannelOpenRouterBalance(channel *model.Channel) (float64, error) { | ||||
| 	url := "https://openrouter.ai/api/v1/credits" | ||||
| 	body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	response := OpenRouterResponse{} | ||||
| 	err = json.Unmarshal(body, &response) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	balance := response.Data.TotalCredits - response.Data.TotalUsage | ||||
| 	channel.UpdateBalance(balance) | ||||
| 	return balance, nil | ||||
| } | ||||
|  | ||||
| func updateChannelBalance(channel *model.Channel) (float64, error) { | ||||
| 	baseURL := channeltype.ChannelBaseURLs[channel.Type] | ||||
| 	if channel.GetBaseURL() == "" { | ||||
| @@ -313,6 +336,8 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { | ||||
| 		return updateChannelSiliconFlowBalance(channel) | ||||
| 	case channeltype.DeepSeek: | ||||
| 		return updateChannelDeepSeekBalance(channel) | ||||
| 	case channeltype.OpenRouter: | ||||
| 		return updateChannelOpenRouterBalance(channel) | ||||
| 	default: | ||||
| 		return 0, errors.New("尚未实现") | ||||
| 	} | ||||
|   | ||||
| @@ -153,6 +153,7 @@ func testChannel(ctx context.Context, channel *model.Channel, request *relaymode | ||||
| 	rawResponse := w.Body.String() | ||||
| 	_, responseMessage, err = parseTestResponse(rawResponse) | ||||
| 	if err != nil { | ||||
| 		logger.SysError(fmt.Sprintf("failed to parse error: %s, \nresponse: %s", err.Error(), rawResponse)) | ||||
| 		return "", err, nil | ||||
| 	} | ||||
| 	result := w.Result() | ||||
|   | ||||
| @@ -2,10 +2,13 @@ package model | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/songquanpeng/one-api/common" | ||||
| 	"gorm.io/gorm" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
|  | ||||
| 	"gorm.io/gorm" | ||||
|  | ||||
| 	"github.com/songquanpeng/one-api/common" | ||||
| 	"github.com/songquanpeng/one-api/common/utils" | ||||
| ) | ||||
|  | ||||
| type Ability struct { | ||||
| @@ -49,6 +52,7 @@ func GetRandomSatisfiedChannel(group string, model string, ignoreFirstPriority b | ||||
|  | ||||
| func (channel *Channel) AddAbilities() error { | ||||
| 	models_ := strings.Split(channel.Models, ",") | ||||
| 	models_ = utils.DeDuplication(models_) | ||||
| 	groups_ := strings.Split(channel.Group, ",") | ||||
| 	abilities := make([]Ability, 0, len(models_)) | ||||
| 	for _, model := range models_ { | ||||
|   | ||||
							
								
								
									
										30
									
								
								relay/adaptor/baiduv2/constants.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								relay/adaptor/baiduv2/constants.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| package baiduv2 | ||||
|  | ||||
| // https://console.bce.baidu.com/support/?_=1692863460488×tamp=1739074632076#/api?product=QIANFAN&project=%E5%8D%83%E5%B8%86ModelBuilder&parent=%E5%AF%B9%E8%AF%9DChat%20V2&api=v2%2Fchat%2Fcompletions&method=post | ||||
| // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu#%E6%94%AF%E6%8C%81%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8 | ||||
|  | ||||
| var ModelList = []string{ | ||||
| 	"ernie-4.0-8k-latest", | ||||
| 	"ernie-4.0-8k-preview", | ||||
| 	"ernie-4.0-8k", | ||||
| 	"ernie-4.0-turbo-8k-latest", | ||||
| 	"ernie-4.0-turbo-8k-preview", | ||||
| 	"ernie-4.0-turbo-8k", | ||||
| 	"ernie-4.0-turbo-128k", | ||||
| 	"ernie-3.5-8k-preview", | ||||
| 	"ernie-3.5-8k", | ||||
| 	"ernie-3.5-128k", | ||||
| 	"ernie-speed-8k", | ||||
| 	"ernie-speed-128k", | ||||
| 	"ernie-speed-pro-128k", | ||||
| 	"ernie-lite-8k", | ||||
| 	"ernie-lite-pro-128k", | ||||
| 	"ernie-tiny-8k", | ||||
| 	"ernie-char-8k", | ||||
| 	"ernie-char-fiction-8k", | ||||
| 	"ernie-novel-8k", | ||||
| 	"deepseek-v3", | ||||
| 	"deepseek-r1", | ||||
| 	"deepseek-r1-distill-qwen-32b", | ||||
| 	"deepseek-r1-distill-qwen-14b", | ||||
| } | ||||
							
								
								
									
										17
									
								
								relay/adaptor/baiduv2/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								relay/adaptor/baiduv2/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| package baiduv2 | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/songquanpeng/one-api/relay/meta" | ||||
| 	"github.com/songquanpeng/one-api/relay/relaymode" | ||||
| ) | ||||
|  | ||||
| func GetRequestURL(meta *meta.Meta) (string, error) { | ||||
| 	switch meta.Mode { | ||||
| 	case relaymode.ChatCompletions: | ||||
| 		return fmt.Sprintf("%s/v2/chat/completions", meta.BaseURL), nil | ||||
| 	default: | ||||
| 	} | ||||
| 	return "", fmt.Errorf("unsupported relay mode %d for baidu v2", meta.Mode) | ||||
| } | ||||
| @@ -8,4 +8,6 @@ var ModelList = []string{ | ||||
| 	"abab6-chat", | ||||
| 	"abab5.5-chat", | ||||
| 	"abab5.5s-chat", | ||||
| 	"MiniMax-VL-01", | ||||
| 	"MiniMax-Text-01", | ||||
| } | ||||
|   | ||||
| @@ -8,7 +8,9 @@ import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
|  | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/baiduv2" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/doubao" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/minimax" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/novita" | ||||
| @@ -52,6 +54,8 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { | ||||
| 		return doubao.GetRequestURL(meta) | ||||
| 	case channeltype.Novita: | ||||
| 		return novita.GetRequestURL(meta) | ||||
| 	case channeltype.BaiduV2: | ||||
| 		return baiduv2.GetRequestURL(meta) | ||||
| 	default: | ||||
| 		return GetFullRequestURL(meta.BaseURL, meta.RequestURLPath, meta.ChannelType), nil | ||||
| 	} | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package openai | ||||
| import ( | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/ai360" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/baichuan" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/baiduv2" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/deepseek" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/doubao" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/groq" | ||||
| @@ -11,10 +12,12 @@ import ( | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/mistral" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/moonshot" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/novita" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/openrouter" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/siliconflow" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/stepfun" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/togetherai" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/xai" | ||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/xunfeiv2" | ||||
| 	"github.com/songquanpeng/one-api/relay/channeltype" | ||||
| ) | ||||
|  | ||||
| @@ -34,6 +37,8 @@ var CompatibleChannels = []int{ | ||||
| 	channeltype.Novita, | ||||
| 	channeltype.SiliconFlow, | ||||
| 	channeltype.XAI, | ||||
| 	channeltype.BaiduV2, | ||||
| 	channeltype.XunfeiV2, | ||||
| } | ||||
|  | ||||
| func GetCompatibleChannelMeta(channelType int) (string, []string) { | ||||
| @@ -68,6 +73,12 @@ func GetCompatibleChannelMeta(channelType int) (string, []string) { | ||||
| 		return "siliconflow", siliconflow.ModelList | ||||
| 	case channeltype.XAI: | ||||
| 		return "xai", xai.ModelList | ||||
| 	case channeltype.BaiduV2: | ||||
| 		return "baiduv2", baiduv2.ModelList | ||||
| 	case channeltype.XunfeiV2: | ||||
| 		return "xunfeiv2", xunfeiv2.ModelList | ||||
| 	case channeltype.OpenRouter: | ||||
| 		return "openrouter", openrouter.ModelList | ||||
| 	default: | ||||
| 		return "openai", ModelList | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										20
									
								
								relay/adaptor/openrouter/constants.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								relay/adaptor/openrouter/constants.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| package openrouter | ||||
|  | ||||
| var ModelList = []string{ | ||||
| 	"openai/gpt-3.5-turbo", | ||||
| 	"openai/chatgpt-4o-latest", | ||||
| 	"openai/o1", | ||||
| 	"openai/o1-preview", | ||||
| 	"openai/o1-mini", | ||||
| 	"openai/o3-mini", | ||||
| 	"google/gemini-2.0-flash-001", | ||||
| 	"google/gemini-2.0-flash-thinking-exp:free", | ||||
| 	"google/gemini-2.0-flash-lite-preview-02-05:free", | ||||
| 	"google/gemini-2.0-pro-exp-02-05:free", | ||||
| 	"google/gemini-flash-1.5-8b", | ||||
| 	"anthropic/claude-3.5-sonnet", | ||||
| 	"anthropic/claude-3.5-haiku", | ||||
| 	"deepseek/deepseek-r1:free", | ||||
| 	"deepseek/deepseek-r1", | ||||
| 	"qwen/qwen-vl-plus:free", | ||||
| } | ||||
| @@ -1,5 +1,14 @@ | ||||
| package xai | ||||
|  | ||||
| //https://console.x.ai/ | ||||
|  | ||||
| var ModelList = []string{ | ||||
| 	"grok-2", | ||||
| 	"grok-vision-beta", | ||||
| 	"grok-2-vision-1212", | ||||
| 	"grok-2-vision", | ||||
| 	"grok-2-vision-latest", | ||||
| 	"grok-2-1212", | ||||
| 	"grok-2-latest", | ||||
| 	"grok-beta", | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,10 @@ | ||||
| package xunfei | ||||
|  | ||||
| var ModelList = []string{ | ||||
| 	"SparkDesk", | ||||
| 	"SparkDesk-v1.1", | ||||
| 	"SparkDesk-v2.1", | ||||
| 	"SparkDesk-v3.1", | ||||
| 	"SparkDesk-v3.1-128K", | ||||
| 	"SparkDesk-v3.5", | ||||
| 	"SparkDesk-v3.5-32K", | ||||
| 	"SparkDesk-v4.0", | ||||
| 	"Spark-Lite", | ||||
| 	"Spark-Pro", | ||||
| 	"Spark-Pro-128K", | ||||
| 	"Spark-Max", | ||||
| 	"Spark-Max-32K", | ||||
| 	"Spark-4.0-Ultra", | ||||
| } | ||||
|   | ||||
							
								
								
									
										97
									
								
								relay/adaptor/xunfei/domain.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								relay/adaptor/xunfei/domain.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| package xunfei | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E | ||||
|  | ||||
| //Spark4.0 Ultra 请求地址,对应的domain参数为4.0Ultra: | ||||
| // | ||||
| //wss://spark-api.xf-yun.com/v4.0/chat | ||||
| //Spark Max-32K请求地址,对应的domain参数为max-32k | ||||
| // | ||||
| //wss://spark-api.xf-yun.com/chat/max-32k | ||||
| //Spark Max请求地址,对应的domain参数为generalv3.5 | ||||
| // | ||||
| //wss://spark-api.xf-yun.com/v3.5/chat | ||||
| //Spark Pro-128K请求地址,对应的domain参数为pro-128k: | ||||
| // | ||||
| // wss://spark-api.xf-yun.com/chat/pro-128k | ||||
| //Spark Pro请求地址,对应的domain参数为generalv3: | ||||
| // | ||||
| //wss://spark-api.xf-yun.com/v3.1/chat | ||||
| //Spark Lite请求地址,对应的domain参数为lite: | ||||
| // | ||||
| //wss://spark-api.xf-yun.com/v1.1/chat | ||||
|  | ||||
| // Lite、Pro、Pro-128K、Max、Max-32K和4.0 Ultra | ||||
|  | ||||
| func parseAPIVersionByModelName(modelName string) string { | ||||
| 	apiVersion := modelName2APIVersion(modelName) | ||||
| 	if apiVersion != "" { | ||||
| 		return apiVersion | ||||
| 	} | ||||
|  | ||||
| 	index := strings.IndexAny(modelName, "-") | ||||
| 	if index != -1 { | ||||
| 		return modelName[index+1:] | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func modelName2APIVersion(modelName string) string { | ||||
| 	switch modelName { | ||||
| 	case "Spark-Lite": | ||||
| 		return "v1.1" | ||||
| 	case "Spark-Pro": | ||||
| 		return "v3.1" | ||||
| 	case "Spark-Pro-128K": | ||||
| 		return "v3.1-128K" | ||||
| 	case "Spark-Max": | ||||
| 		return "v3.5" | ||||
| 	case "Spark-Max-32K": | ||||
| 		return "v3.5-32K" | ||||
| 	case "Spark-4.0-Ultra": | ||||
| 		return "v4.0" | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| // https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E | ||||
| func apiVersion2domain(apiVersion string) string { | ||||
| 	switch apiVersion { | ||||
| 	case "v1.1": | ||||
| 		return "lite" | ||||
| 	case "v2.1": | ||||
| 		return "generalv2" | ||||
| 	case "v3.1": | ||||
| 		return "generalv3" | ||||
| 	case "v3.1-128K": | ||||
| 		return "pro-128k" | ||||
| 	case "v3.5": | ||||
| 		return "generalv3.5" | ||||
| 	case "v3.5-32K": | ||||
| 		return "max-32k" | ||||
| 	case "v4.0": | ||||
| 		return "4.0Ultra" | ||||
| 	} | ||||
| 	return "general" + apiVersion | ||||
| } | ||||
|  | ||||
| func getXunfeiAuthUrl(apiVersion string, apiKey string, apiSecret string) (string, string) { | ||||
| 	var authUrl string | ||||
| 	domain := apiVersion2domain(apiVersion) | ||||
| 	switch apiVersion { | ||||
| 	case "v3.1-128K": | ||||
| 		authUrl = buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/chat/pro-128k"), apiKey, apiSecret) | ||||
| 		break | ||||
| 	case "v3.5-32K": | ||||
| 		authUrl = buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/chat/max-32k"), apiKey, apiSecret) | ||||
| 		break | ||||
| 	default: | ||||
| 		authUrl = buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/%s/chat", apiVersion), apiKey, apiSecret) | ||||
| 	} | ||||
| 	return domain, authUrl | ||||
| } | ||||
| @@ -15,6 +15,7 @@ import ( | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/gorilla/websocket" | ||||
|  | ||||
| 	"github.com/songquanpeng/one-api/common" | ||||
| 	"github.com/songquanpeng/one-api/common/helper" | ||||
| 	"github.com/songquanpeng/one-api/common/logger" | ||||
| @@ -270,48 +271,3 @@ func xunfeiMakeRequest(textRequest model.GeneralOpenAIRequest, domain, authUrl, | ||||
|  | ||||
| 	return dataChan, stopChan, nil | ||||
| } | ||||
|  | ||||
| func parseAPIVersionByModelName(modelName string) string { | ||||
| 	index := strings.IndexAny(modelName, "-") | ||||
| 	if index != -1 { | ||||
| 		return modelName[index+1:] | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| // https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E | ||||
| func apiVersion2domain(apiVersion string) string { | ||||
| 	switch apiVersion { | ||||
| 	case "v1.1": | ||||
| 		return "lite" | ||||
| 	case "v2.1": | ||||
| 		return "generalv2" | ||||
| 	case "v3.1": | ||||
| 		return "generalv3" | ||||
| 	case "v3.1-128K": | ||||
| 		return "pro-128k" | ||||
| 	case "v3.5": | ||||
| 		return "generalv3.5" | ||||
| 	case "v3.5-32K": | ||||
| 		return "max-32k" | ||||
| 	case "v4.0": | ||||
| 		return "4.0Ultra" | ||||
| 	} | ||||
| 	return "general" + apiVersion | ||||
| } | ||||
|  | ||||
| func getXunfeiAuthUrl(apiVersion string, apiKey string, apiSecret string) (string, string) { | ||||
| 	var authUrl string | ||||
| 	domain := apiVersion2domain(apiVersion) | ||||
| 	switch apiVersion { | ||||
| 	case "v3.1-128K": | ||||
| 		authUrl = buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/chat/pro-128k"), apiKey, apiSecret) | ||||
| 		break | ||||
| 	case "v3.5-32K": | ||||
| 		authUrl = buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/chat/max-32k"), apiKey, apiSecret) | ||||
| 		break | ||||
| 	default: | ||||
| 		authUrl = buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/%s/chat", apiVersion), apiKey, apiSecret) | ||||
| 	} | ||||
| 	return domain, authUrl | ||||
| } | ||||
|   | ||||
							
								
								
									
										12
									
								
								relay/adaptor/xunfeiv2/constants.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								relay/adaptor/xunfeiv2/constants.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| package xunfeiv2 | ||||
|  | ||||
| // https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html#_3-%E8%AF%B7%E6%B1%82%E8%AF%B4%E6%98%8E | ||||
|  | ||||
| var ModelList = []string{ | ||||
| 	"lite", | ||||
| 	"generalv3", | ||||
| 	"pro-128k", | ||||
| 	"generalv3.5", | ||||
| 	"max-32k", | ||||
| 	"4.0Ultra", | ||||
| } | ||||
| @@ -48,5 +48,7 @@ const ( | ||||
| 	SiliconFlow | ||||
| 	XAI | ||||
| 	Replicate | ||||
| 	BaiduV2 | ||||
| 	XunfeiV2 | ||||
| 	Dummy | ||||
| ) | ||||
|   | ||||
| @@ -48,6 +48,8 @@ var ChannelBaseURLs = []string{ | ||||
| 	"https://api.siliconflow.cn",                // 44 | ||||
| 	"https://api.x.ai",                          // 45 | ||||
| 	"https://api.replicate.com/v1/models/",      // 46 | ||||
| 	"https://qianfan.baidubce.com",              // 47 | ||||
| 	"https://spark-api-open.xf-yun.com",         // 48 | ||||
| } | ||||
|  | ||||
| func init() { | ||||
|   | ||||
| @@ -38,7 +38,7 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode { | ||||
| 	textRequest.Model, _ = getMappedModelName(textRequest.Model, meta.ModelMapping) | ||||
| 	meta.ActualModelName = textRequest.Model | ||||
| 	// set system prompt if not empty | ||||
| 	systemPromptReset := setSystemPrompt(ctx, textRequest, meta.SystemPrompt) | ||||
| 	systemPromptReset := setSystemPrompt(ctx, textRequest, meta.ForcedSystemPrompt) | ||||
| 	// get model ratio & group ratio | ||||
| 	modelRatio := billingratio.GetModelRatio(textRequest.Model, meta.ChannelType) | ||||
| 	groupRatio := billingratio.GetGroupRatio(meta.Group) | ||||
| @@ -88,7 +88,11 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode { | ||||
| } | ||||
|  | ||||
| func getRequestBody(c *gin.Context, meta *meta.Meta, textRequest *model.GeneralOpenAIRequest, adaptor adaptor.Adaptor) (io.Reader, error) { | ||||
| 	if !config.EnforceIncludeUsage && meta.APIType == apitype.OpenAI && meta.OriginModelName == meta.ActualModelName && meta.ChannelType != channeltype.Baichuan { | ||||
| 	if !config.EnforceIncludeUsage && | ||||
| 		meta.APIType == apitype.OpenAI && | ||||
| 		meta.OriginModelName == meta.ActualModelName && | ||||
| 		meta.ChannelType != channeltype.Baichuan && | ||||
| 		meta.ForcedSystemPrompt == "" { | ||||
| 		// no need to convert request for openai | ||||
| 		return c.Request.Body, nil | ||||
| 	} | ||||
|   | ||||
| @@ -33,7 +33,7 @@ type Meta struct { | ||||
| 	ActualModelName    string | ||||
| 	RequestURLPath     string | ||||
| 	PromptTokens       int // only for DoResponse | ||||
| 	SystemPrompt    string | ||||
| 	ForcedSystemPrompt string | ||||
| 	StartTime          time.Time | ||||
| } | ||||
|  | ||||
| @@ -51,7 +51,7 @@ func GetByContext(c *gin.Context) *Meta { | ||||
| 		BaseURL:            c.GetString(ctxkey.BaseURL), | ||||
| 		APIKey:             strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "), | ||||
| 		RequestURLPath:     c.Request.URL.String(), | ||||
| 		SystemPrompt:    c.GetString(ctxkey.SystemPrompt), | ||||
| 		ForcedSystemPrompt: c.GetString(ctxkey.SystemPrompt), | ||||
| 		StartTime:          time.Now(), | ||||
| 	} | ||||
| 	cfg, ok := c.Get(ctxkey.Config) | ||||
|   | ||||
| @@ -26,6 +26,7 @@ type GeneralOpenAIRequest struct { | ||||
| 	Messages            []Message       `json:"messages,omitempty"` | ||||
| 	Model               string          `json:"model,omitempty"` | ||||
| 	Store               *bool           `json:"store,omitempty"` | ||||
| 	ReasoningEffort     *string         `json:"reasoning_effort,omitempty"` | ||||
| 	Metadata            any             `json:"metadata,omitempty"` | ||||
| 	FrequencyPenalty    *float64        `json:"frequency_penalty,omitempty"` | ||||
| 	LogitBias           any             `json:"logit_bias,omitempty"` | ||||
|   | ||||
| @@ -4,6 +4,14 @@ type Usage struct { | ||||
| 	PromptTokens     int `json:"prompt_tokens"` | ||||
| 	CompletionTokens int `json:"completion_tokens"` | ||||
| 	TotalTokens      int `json:"total_tokens"` | ||||
|  | ||||
| 	CompletionTokensDetails *CompletionTokensDetails `json:"completion_tokens_details,omitempty"` | ||||
| } | ||||
|  | ||||
| type CompletionTokensDetails struct { | ||||
| 	ReasoningTokens          int `json:"reasoning_tokens"` | ||||
| 	AcceptedPredictionTokens int `json:"accepted_prediction_tokens"` | ||||
| 	RejectedPredictionTokens int `json:"rejected_prediction_tokens"` | ||||
| } | ||||
|  | ||||
| type Error struct { | ||||
|   | ||||
| @@ -7,7 +7,7 @@ export const CHANNEL_OPTIONS = [ | ||||
|   { key: 24, text: 'Google Gemini', value: 24, color: 'orange' }, | ||||
|   { key: 28, text: 'Mistral AI', value: 28, color: 'orange' }, | ||||
|   { key: 41, text: 'Novita', value: 41, color: 'purple' }, | ||||
|   {key: 40, text: '火山引擎', value: 40, color: 'blue'}, | ||||
|   {key: 40, text: '字节火山引擎', value: 40, color: 'blue'}, | ||||
|   { key: 15, text: '百度文心千帆', value: 15, color: 'blue' }, | ||||
|   { key: 17, text: '阿里通义千问', value: 17, color: 'orange' }, | ||||
|   { key: 18, text: '讯飞星火认知', value: 18, color: 'blue' }, | ||||
| @@ -35,7 +35,7 @@ export const CHANNEL_OPTIONS = [ | ||||
|   { key: 8, text: '自定义渠道', value: 8, color: 'pink' }, | ||||
|   { key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' }, | ||||
|   { key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' }, | ||||
|   { key: 20, text: '代理:OpenRouter', value: 20, color: 'black' }, | ||||
|   {key: 20, text: 'OpenRouter', value: 20, color: 'black'}, | ||||
|   { key: 2, text: '代理:API2D', value: 2, color: 'blue' }, | ||||
|   { key: 5, text: '代理:OpenAI-SB', value: 5, color: 'brown' }, | ||||
|   { key: 7, text: '代理:OhMyGPT', value: 7, color: 'purple' }, | ||||
|   | ||||
| @@ -49,7 +49,7 @@ export const CHANNEL_OPTIONS = { | ||||
|   }, | ||||
|   40: { | ||||
|     key: 40, | ||||
|     text: '火山引擎', | ||||
|     text: '字节火山引擎', | ||||
|     value: 40, | ||||
|     color: 'primary' | ||||
|   }, | ||||
| @@ -217,7 +217,7 @@ export const CHANNEL_OPTIONS = { | ||||
|   }, | ||||
|   20: { | ||||
|     key: 20, | ||||
|     text: '代理:OpenRouter', | ||||
|       text: 'OpenRouter', | ||||
|     value: 20, | ||||
|     color: 'success' | ||||
|   }, | ||||
|   | ||||
| @@ -1,16 +1,6 @@ | ||||
| import React, {useEffect, useState} from 'react'; | ||||
| import {useTranslation} from 'react-i18next'; | ||||
| import { | ||||
|   Button, | ||||
|   Dropdown, | ||||
|   Form, | ||||
|   Input, | ||||
|   Label, | ||||
|   Message, | ||||
|   Pagination, | ||||
|   Popup, | ||||
|   Table, | ||||
| } from 'semantic-ui-react'; | ||||
| import {Button, Dropdown, Form, Input, Label, Message, Pagination, Popup, Table,} from 'semantic-ui-react'; | ||||
| import {Link} from 'react-router-dom'; | ||||
| import { | ||||
|   API, | ||||
| @@ -67,6 +57,8 @@ function renderBalance(type, balance, t) { | ||||
|       return <span>¥{balance.toFixed(2)}</span>; | ||||
|     case 13: // AIGC2D | ||||
|       return <span>{renderNumber(balance)}</span>; | ||||
|     case 20: // OpenRouter | ||||
|       return <span>${balance.toFixed(2)}</span>; | ||||
|     case 36: // DeepSeek | ||||
|       return <span>¥{balance.toFixed(2)}</span>; | ||||
|     case 44: // SiliconFlow | ||||
| @@ -93,11 +85,7 @@ const ChannelsTable = () => { | ||||
|   const [showPrompt, setShowPrompt] = useState(shouldShowPrompt(promptID)); | ||||
|   const [showDetail, setShowDetail] = useState(isShowDetail()); | ||||
|  | ||||
|   const loadChannels = async (startIdx) => { | ||||
|     const res = await API.get(`/api/channel/?p=${startIdx}`); | ||||
|     const { success, message, data } = res.data; | ||||
|     if (success) { | ||||
|       let localChannels = data.map((channel) => { | ||||
|   const processChannelData = (channel) => { | ||||
|     if (channel.models === '') { | ||||
|       channel.models = []; | ||||
|       channel.test_model = ''; | ||||
| @@ -116,7 +104,13 @@ const ChannelsTable = () => { | ||||
|       console.log('channel', channel); | ||||
|     } | ||||
|     return channel; | ||||
|       }); | ||||
|   }; | ||||
|  | ||||
|   const loadChannels = async (startIdx) => { | ||||
|     const res = await API.get(`/api/channel/?p=${startIdx}`); | ||||
|     const {success, message, data} = res.data; | ||||
|     if (success) { | ||||
|       let localChannels = data.map(processChannelData); | ||||
|       if (startIdx === 0) { | ||||
|         setChannels(localChannels); | ||||
|       } else { | ||||
| @@ -301,7 +295,8 @@ const ChannelsTable = () => { | ||||
|     const res = await API.get(`/api/channel/search?keyword=${searchKeyword}`); | ||||
|     const { success, message, data } = res.data; | ||||
|     if (success) { | ||||
|       setChannels(data); | ||||
|       let localChannels = data.map(processChannelData); | ||||
|       setChannels(localChannels); | ||||
|       setActivePage(1); | ||||
|     } else { | ||||
|       showError(message); | ||||
| @@ -495,7 +490,6 @@ const ChannelsTable = () => { | ||||
|               onClick={() => { | ||||
|                 sortChannel('balance'); | ||||
|               }} | ||||
|               hidden={!showDetail} | ||||
|             > | ||||
|               {t('channel.table.balance')} | ||||
|             </Table.HeaderCell> | ||||
| @@ -504,6 +498,7 @@ const ChannelsTable = () => { | ||||
|               onClick={() => { | ||||
|                 sortChannel('priority'); | ||||
|               }} | ||||
|               hidden={!showDetail} | ||||
|             > | ||||
|               {t('channel.table.priority')} | ||||
|             </Table.HeaderCell> | ||||
| @@ -543,7 +538,7 @@ const ChannelsTable = () => { | ||||
|                       basic | ||||
|                     /> | ||||
|                   </Table.Cell> | ||||
|                   <Table.Cell hidden={!showDetail}> | ||||
|                   <Table.Cell> | ||||
|                     <Popup | ||||
|                       trigger={ | ||||
|                         <span | ||||
| @@ -559,7 +554,7 @@ const ChannelsTable = () => { | ||||
|                       basic | ||||
|                     /> | ||||
|                   </Table.Cell> | ||||
|                   <Table.Cell> | ||||
|                   <Table.Cell hidden={!showDetail}> | ||||
|                     <Popup | ||||
|                       trigger={ | ||||
|                         <Input | ||||
|   | ||||
| @@ -7,10 +7,42 @@ export const CHANNEL_OPTIONS = [ | ||||
|   {key: 24, text: 'Google Gemini', value: 24, color: 'orange'}, | ||||
|   {key: 28, text: 'Mistral AI', value: 28, color: 'orange'}, | ||||
|   {key: 41, text: 'Novita', value: 41, color: 'purple'}, | ||||
|     {key: 40, text: '火山引擎', value: 40, color: 'blue'}, | ||||
|     { key: 15, text: '百度文心千帆', value: 15, color: 'blue' }, | ||||
|   { | ||||
|     key: 40, | ||||
|     text: '字节火山引擎', | ||||
|     value: 40, | ||||
|     color: 'blue', | ||||
|     description: '原字节跳动豆包', | ||||
|   }, | ||||
|   { | ||||
|     key: 15, | ||||
|     text: '百度文心千帆', | ||||
|     value: 15, | ||||
|     color: 'blue', | ||||
|     tip: '请前往<a href="https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application/v1" target="_blank">此处</a>获取 AK(API Key)以及 SK(Secret Key),注意,V2 版本接口请使用 <strong>百度文心千帆 V2 </strong>渠道类型', | ||||
|   }, | ||||
|   { | ||||
|     key: 47, | ||||
|     text: '百度文心千帆 V2', | ||||
|     value: 47, | ||||
|     color: 'blue', | ||||
|     tip: '请前往<a href="https://console.bce.baidu.com/iam/#/iam/apikey/list" target="_blank">此处</a>获取 API Key,注意本渠道仅支持<a target="_blank" href="https://cloud.baidu.com/doc/WENXINWORKSHOP/s/em4tsqo3v">推理服务 V2</a>相关模型', | ||||
|   }, | ||||
|   {key: 17, text: '阿里通义千问', value: 17, color: 'orange'}, | ||||
|     { key: 18, text: '讯飞星火认知', value: 18, color: 'blue' }, | ||||
|   { | ||||
|     key: 18, | ||||
|     text: '讯飞星火认知', | ||||
|     value: 18, | ||||
|     color: 'blue', | ||||
|     tip: '本渠道基于讯飞 WebSocket 版本 API,如需 HTTP 版本,请使用<strong>讯飞星火认知 V2</strong>渠道', | ||||
|   }, | ||||
|   { | ||||
|     key: 48, | ||||
|     text: '讯飞星火认知 V2', | ||||
|     value: 48, | ||||
|     color: 'blue', | ||||
|     tip: 'HTTP 版本的讯飞接口,前往<a href="https://console.xfyun.cn/services/cbm" target="_blank">此处</a>获取 HTTP 服务接口认证密钥', | ||||
|   }, | ||||
|   {key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet'}, | ||||
|   {key: 19, text: '360 智脑', value: 19, color: 'blue'}, | ||||
|   {key: 25, text: 'Moonshot AI', value: 25, color: 'black'}, | ||||
| @@ -35,7 +67,7 @@ export const CHANNEL_OPTIONS = [ | ||||
|   {key: 8, text: '自定义渠道', value: 8, color: 'pink'}, | ||||
|   {key: 22, text: '知识库:FastGPT', value: 22, color: 'blue'}, | ||||
|   {key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple'}, | ||||
|     { key: 20, text: '代理:OpenRouter', value: 20, color: 'black' }, | ||||
|     {key: 20, text: 'OpenRouter', value: 20, color: 'black'}, | ||||
|   {key: 2, text: '代理:API2D', value: 2, color: 'blue'}, | ||||
|   {key: 5, text: '代理:OpenAI-SB', value: 5, color: 'brown'}, | ||||
|   {key: 7, text: '代理:OhMyGPT', value: 7, color: 'purple'}, | ||||
| @@ -44,5 +76,5 @@ export const CHANNEL_OPTIONS = [ | ||||
|   {key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet'}, | ||||
|   {key: 9, text: '代理:AI.LS', value: 9, color: 'yellow'}, | ||||
|   {key: 12, text: '代理:API2GPT', value: 12, color: 'blue'}, | ||||
|     { key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' } | ||||
|   {key: 13, text: '代理:AIGC2D', value: 13, color: 'purple'}, | ||||
| ]; | ||||
|   | ||||
							
								
								
									
										13
									
								
								web/default/src/helpers/helper.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								web/default/src/helpers/helper.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import {CHANNEL_OPTIONS} from '../constants'; | ||||
|  | ||||
| let channelMap = undefined; | ||||
|  | ||||
| export function getChannelOption(channelId) { | ||||
|     if (channelMap === undefined) { | ||||
|         channelMap = {}; | ||||
|         CHANNEL_OPTIONS.forEach((option) => { | ||||
|             channelMap[option.key] = option; | ||||
|         }); | ||||
|     } | ||||
|     return channelMap[channelId]; | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Label } from 'semantic-ui-react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import {Label, Message} from 'semantic-ui-react'; | ||||
| import {getChannelOption} from './helper'; | ||||
| import React from 'react'; | ||||
|  | ||||
| export function renderText(text, limit) { | ||||
|   if (text.length > limit) { | ||||
| @@ -98,3 +99,15 @@ export function renderColorLabel(text) { | ||||
|     </Label> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function renderChannelTip(channelId) { | ||||
|   let channel = getChannelOption(channelId); | ||||
|   if (channel === undefined || channel.tip === undefined) { | ||||
|     return <></>; | ||||
|   } | ||||
|   return ( | ||||
|       <Message> | ||||
|         <div dangerouslySetInnerHTML={{__html: channel.tip}}></div> | ||||
|       </Message> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -1,25 +1,10 @@ | ||||
| import React, {useEffect, useState} from 'react'; | ||||
| import {useTranslation} from 'react-i18next'; | ||||
| import { | ||||
|   Button, | ||||
|   Form, | ||||
|   Header, | ||||
|   Input, | ||||
|   Message, | ||||
|   Segment, | ||||
|   Card, | ||||
| } from 'semantic-ui-react'; | ||||
| import {Button, Card, Form, Input, Message,} from 'semantic-ui-react'; | ||||
| import {useNavigate, useParams} from 'react-router-dom'; | ||||
| import { | ||||
|   API, | ||||
|   copy, | ||||
|   getChannelModels, | ||||
|   showError, | ||||
|   showInfo, | ||||
|   showSuccess, | ||||
|   verifyJSON, | ||||
| } from '../../helpers'; | ||||
| import {API, copy, getChannelModels, showError, showInfo, showSuccess, verifyJSON,} from '../../helpers'; | ||||
| import {CHANNEL_OPTIONS} from '../../constants'; | ||||
| import {renderChannelTip} from '../../helpers/render'; | ||||
|  | ||||
| const MODEL_MAPPING_EXAMPLE = { | ||||
|   'gpt-3.5-turbo-0301': 'gpt-3.5-turbo', | ||||
| @@ -310,6 +295,7 @@ const EditChannel = () => { | ||||
|                 options={groupOptions} | ||||
|               /> | ||||
|             </Form.Field> | ||||
|             {renderChannelTip(inputs.type)} | ||||
|  | ||||
|             {/* Azure OpenAI specific fields */} | ||||
|             {inputs.type === 3 && ( | ||||
|   | ||||
| @@ -122,11 +122,11 @@ const Dashboard = () => { | ||||
|         ? new Date(Math.min(...dates.map((d) => new Date(d)))) | ||||
|         : new Date(); | ||||
|  | ||||
|     // 确保至少显示5天的数据 | ||||
|     const fiveDaysAgo = new Date(); | ||||
|     fiveDaysAgo.setDate(fiveDaysAgo.getDate() - 4); // -4是因为包含今天 | ||||
|     if (minDate > fiveDaysAgo) { | ||||
|       minDate = fiveDaysAgo; | ||||
|     // 确保至少显示7天的数据 | ||||
|     const sevenDaysAgo = new Date(); | ||||
|     sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 6); // -6是因为包含今天 | ||||
|     if (minDate > sevenDaysAgo) { | ||||
|       minDate = sevenDaysAgo; | ||||
|     } | ||||
|  | ||||
|     // 生成所有日期 | ||||
| @@ -164,11 +164,11 @@ const Dashboard = () => { | ||||
|         ? new Date(Math.min(...dates.map((d) => new Date(d)))) | ||||
|         : new Date(); | ||||
|  | ||||
|     // 确保至少显示5天的数据 | ||||
|     const fiveDaysAgo = new Date(); | ||||
|     fiveDaysAgo.setDate(fiveDaysAgo.getDate() - 4); // -4是因为包含今天 | ||||
|     if (minDate > fiveDaysAgo) { | ||||
|       minDate = fiveDaysAgo; | ||||
|     // 确保至少显示7天的数据 | ||||
|     const sevenDaysAgo = new Date(); | ||||
|     sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 6); // -6是因为包含今天 | ||||
|     if (minDate > sevenDaysAgo) { | ||||
|       minDate = sevenDaysAgo; | ||||
|     } | ||||
|  | ||||
|     // 生成所有日期 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user