From f795148c58a8e69ffe752275d19009feb2937298 Mon Sep 17 00:00:00 2001 From: huanghejian Date: Wed, 10 Sep 2025 20:30:00 +0800 Subject: [PATCH 1/4] feat: support amazon nova --- relay/channel/aws/adaptor.go | 10 +++++ relay/channel/aws/constants.go | 11 +++++ relay/channel/aws/dto.go | 53 +++++++++++++++++++++++ relay/channel/aws/relay-aws.go | 78 ++++++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+) diff --git a/relay/channel/aws/adaptor.go b/relay/channel/aws/adaptor.go index 1526a7f7..9d5e5891 100644 --- a/relay/channel/aws/adaptor.go +++ b/relay/channel/aws/adaptor.go @@ -60,7 +60,16 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn if request == nil { return nil, errors.New("request is nil") } + // 检查是否为Nova模型 + if isNovaModel(request.Model) { + novaReq := convertToNovaRequest(request) + c.Set("request_model", request.Model) + c.Set("converted_request", novaReq) + c.Set("is_nova_model", true) + return novaReq, nil + } + // 原有的Claude模型处理逻辑 var claudeReq *dto.ClaudeRequest var err error claudeReq, err = claude.RequestOpenAI2ClaudeMessage(c, *request) @@ -69,6 +78,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn } c.Set("request_model", claudeReq.Model) c.Set("converted_request", claudeReq) + c.Set("is_nova_model", false) return claudeReq, err } diff --git a/relay/channel/aws/constants.go b/relay/channel/aws/constants.go index 3f8800b1..8ed8f031 100644 --- a/relay/channel/aws/constants.go +++ b/relay/channel/aws/constants.go @@ -1,5 +1,7 @@ package aws +import "strings" + var awsModelIDMap = map[string]string{ "claude-instant-1.2": "anthropic.claude-instant-v1", "claude-2.0": "anthropic.claude-v2", @@ -14,6 +16,10 @@ var awsModelIDMap = map[string]string{ "claude-sonnet-4-20250514": "anthropic.claude-sonnet-4-20250514-v1:0", "claude-opus-4-20250514": "anthropic.claude-opus-4-20250514-v1:0", "claude-opus-4-1-20250805": "anthropic.claude-opus-4-1-20250805-v1:0", + // Nova models + "amazon.nova-micro-v1:0": "us.amazon.nova-micro-v1:0", + "amazon.nova-lite-v1:0": "us.amazon.nova-lite-v1:0", + "amazon.nova-pro-v1:0": "us.amazon.nova-pro-v1:0", } var awsModelCanCrossRegionMap = map[string]map[string]bool{ @@ -67,3 +73,8 @@ var awsRegionCrossModelPrefixMap = map[string]string{ } var ChannelName = "aws" + +// 判断是否为Nova模型 +func isNovaModel(modelId string) bool { + return strings.HasPrefix(modelId, "amazon.nova-") +} diff --git a/relay/channel/aws/dto.go b/relay/channel/aws/dto.go index 0188c30a..25851ff6 100644 --- a/relay/channel/aws/dto.go +++ b/relay/channel/aws/dto.go @@ -34,3 +34,56 @@ func copyRequest(req *dto.ClaudeRequest) *AwsClaudeRequest { Thinking: req.Thinking, } } + +// Nova模型使用messages-v1格式 +type NovaMessage struct { + Role string `json:"role"` + Content []NovaContent `json:"content"` +} + +type NovaContent struct { + Text string `json:"text"` +} + +type NovaRequest struct { + SchemaVersion string `json:"schemaVersion"` + Messages []NovaMessage `json:"messages"` + InferenceConfig NovaInferenceConfig `json:"inferenceConfig,omitempty"` +} + +type NovaInferenceConfig struct { + MaxTokens int `json:"maxTokens,omitempty"` + Temperature float64 `json:"temperature,omitempty"` + TopP float64 `json:"topP,omitempty"` +} + +// 转换OpenAI请求为Nova格式 +func convertToNovaRequest(req *dto.GeneralOpenAIRequest) *NovaRequest { + novaMessages := make([]NovaMessage, len(req.Messages)) + for i, msg := range req.Messages { + novaMessages[i] = NovaMessage{ + Role: msg.Role, + Content: []NovaContent{{Text: msg.StringContent()}}, + } + } + + novaReq := &NovaRequest{ + SchemaVersion: "messages-v1", + Messages: novaMessages, + } + + // 设置推理配置 + if req.MaxTokens != 0 || (req.Temperature != nil && *req.Temperature != 0) || req.TopP != 0 { + if req.MaxTokens != 0 { + novaReq.InferenceConfig.MaxTokens = int(req.MaxTokens) + } + if req.Temperature != nil && *req.Temperature != 0 { + novaReq.InferenceConfig.Temperature = *req.Temperature + } + if req.TopP != 0 { + novaReq.InferenceConfig.TopP = req.TopP + } + } + + return novaReq +} diff --git a/relay/channel/aws/relay-aws.go b/relay/channel/aws/relay-aws.go index 26e234fa..3df6b33d 100644 --- a/relay/channel/aws/relay-aws.go +++ b/relay/channel/aws/relay-aws.go @@ -1,6 +1,7 @@ package aws import ( + "encoding/json" "fmt" "net/http" "one-api/common" @@ -93,7 +94,13 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (* } awsModelId := awsModelID(c.GetString("request_model")) + // 检查是否为Nova模型 + isNova, _ := c.Get("is_nova_model") + if isNova == true { + return handleNovaRequest(c, awsCli, info, awsModelId) + } + // 原有的Claude处理逻辑 awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region) canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix) if canCrossRegion { @@ -209,3 +216,74 @@ func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel claude.HandleStreamFinalResponse(c, info, claudeInfo, RequestModeMessage) return nil, claudeInfo.Usage } + +// Nova模型处理函数 +func handleNovaRequest(c *gin.Context, awsCli *bedrockruntime.Client, info *relaycommon.RelayInfo, awsModelId string) (*types.NewAPIError, *dto.Usage) { + novaReq_, ok := c.Get("converted_request") + if !ok { + return types.NewError(errors.New("nova request not found"), types.ErrorCodeInvalidRequest), nil + } + novaReq := novaReq_.(*NovaRequest) + + // 使用InvokeModel API,但使用Nova格式的请求体 + awsReq := &bedrockruntime.InvokeModelInput{ + ModelId: aws.String(awsModelId), + Accept: aws.String("application/json"), + ContentType: aws.String("application/json"), + } + + reqBody, err := json.Marshal(novaReq) + if err != nil { + return types.NewError(errors.Wrap(err, "marshal nova request"), types.ErrorCodeBadResponseBody), nil + } + awsReq.Body = reqBody + + awsResp, err := awsCli.InvokeModel(c.Request.Context(), awsReq) + if err != nil { + return types.NewError(errors.Wrap(err, "InvokeModel"), types.ErrorCodeChannelAwsClientError), nil + } + + // 解析Nova响应 + var novaResp struct { + Output struct { + Message struct { + Content []struct { + Text string `json:"text"` + } `json:"content"` + } `json:"message"` + } `json:"output"` + Usage struct { + InputTokens int `json:"inputTokens"` + OutputTokens int `json:"outputTokens"` + TotalTokens int `json:"totalTokens"` + } `json:"usage"` + } + + if err := json.Unmarshal(awsResp.Body, &novaResp); err != nil { + return types.NewError(errors.Wrap(err, "unmarshal nova response"), types.ErrorCodeBadResponseBody), nil + } + + // 构造OpenAI格式响应 + response := dto.OpenAITextResponse{ + Id: helper.GetResponseID(c), + Object: "chat.completion", + Created: common.GetTimestamp(), + Model: info.UpstreamModelName, + Choices: []dto.OpenAITextResponseChoice{{ + Index: 0, + Message: dto.Message{ + Role: "assistant", + Content: novaResp.Output.Message.Content[0].Text, + }, + FinishReason: "stop", + }}, + Usage: dto.Usage{ + PromptTokens: novaResp.Usage.InputTokens, + CompletionTokens: novaResp.Usage.OutputTokens, + TotalTokens: novaResp.Usage.TotalTokens, + }, + } + + c.JSON(http.StatusOK, response) + return nil, &response.Usage +} From 2d4ecaaf28a9e874529c9f4a209d2802327fc810 Mon Sep 17 00:00:00 2001 From: huanghejian Date: Thu, 11 Sep 2025 10:01:54 +0800 Subject: [PATCH 2/4] feat: amazon.nova-premier-v1:0 --- relay/channel/aws/constants.go | 7 ++++--- relay/channel/aws/dto.go | 16 +++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/relay/channel/aws/constants.go b/relay/channel/aws/constants.go index 8ed8f031..7f18d57a 100644 --- a/relay/channel/aws/constants.go +++ b/relay/channel/aws/constants.go @@ -17,9 +17,10 @@ var awsModelIDMap = map[string]string{ "claude-opus-4-20250514": "anthropic.claude-opus-4-20250514-v1:0", "claude-opus-4-1-20250805": "anthropic.claude-opus-4-1-20250805-v1:0", // Nova models - "amazon.nova-micro-v1:0": "us.amazon.nova-micro-v1:0", - "amazon.nova-lite-v1:0": "us.amazon.nova-lite-v1:0", - "amazon.nova-pro-v1:0": "us.amazon.nova-pro-v1:0", + "amazon.nova-micro-v1:0": "us.amazon.nova-micro-v1:0", + "amazon.nova-lite-v1:0": "us.amazon.nova-lite-v1:0", + "amazon.nova-pro-v1:0": "us.amazon.nova-pro-v1:0", + "amazon.nova-premier-v1:0": "us.amazon.nova-premier-v1:0", } var awsModelCanCrossRegionMap = map[string]map[string]bool{ diff --git a/relay/channel/aws/dto.go b/relay/channel/aws/dto.go index 25851ff6..cef16c11 100644 --- a/relay/channel/aws/dto.go +++ b/relay/channel/aws/dto.go @@ -35,7 +35,7 @@ func copyRequest(req *dto.ClaudeRequest) *AwsClaudeRequest { } } -// Nova模型使用messages-v1格式 +// NovaMessage Nova模型使用messages-v1格式 type NovaMessage struct { Role string `json:"role"` Content []NovaContent `json:"content"` @@ -46,15 +46,17 @@ type NovaContent struct { } type NovaRequest struct { - SchemaVersion string `json:"schemaVersion"` - Messages []NovaMessage `json:"messages"` - InferenceConfig NovaInferenceConfig `json:"inferenceConfig,omitempty"` + SchemaVersion string `json:"schemaVersion"` // 请求版本,例如 "1.0" + Messages []NovaMessage `json:"messages"` // 对话消息列表 + InferenceConfig *NovaInferenceConfig `json:"inferenceConfig,omitempty"` // 推理配置,可选 } type NovaInferenceConfig struct { - MaxTokens int `json:"maxTokens,omitempty"` - Temperature float64 `json:"temperature,omitempty"` - TopP float64 `json:"topP,omitempty"` + MaxTokens int `json:"maxTokens,omitempty"` // 最大生成的 token 数 + Temperature float64 `json:"temperature,omitempty"` // 随机性 (默认 0.7, 范围 0-1) + TopP float64 `json:"topP,omitempty"` // nucleus sampling (默认 0.9, 范围 0-1) + TopK int `json:"topK,omitempty"` // 限制候选 token 数 (默认 50, 范围 0-128) + StopSequences []string `json:"stopSequences,omitempty"` // 停止生成的序列 } // 转换OpenAI请求为Nova格式 From b10ed4b4248126caa3a9aa46dee4e0deba822a7f Mon Sep 17 00:00:00 2001 From: huanghejian Date: Thu, 11 Sep 2025 12:17:16 +0800 Subject: [PATCH 3/4] pref: support amazon nova --- relay/channel/aws/constants.go | 32 ++++++++++++++++++++++++++------ relay/channel/aws/dto.go | 1 + relay/channel/aws/relay-aws.go | 6 ++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/relay/channel/aws/constants.go b/relay/channel/aws/constants.go index 7f18d57a..72d0f989 100644 --- a/relay/channel/aws/constants.go +++ b/relay/channel/aws/constants.go @@ -17,10 +17,10 @@ var awsModelIDMap = map[string]string{ "claude-opus-4-20250514": "anthropic.claude-opus-4-20250514-v1:0", "claude-opus-4-1-20250805": "anthropic.claude-opus-4-1-20250805-v1:0", // Nova models - "amazon.nova-micro-v1:0": "us.amazon.nova-micro-v1:0", - "amazon.nova-lite-v1:0": "us.amazon.nova-lite-v1:0", - "amazon.nova-pro-v1:0": "us.amazon.nova-pro-v1:0", - "amazon.nova-premier-v1:0": "us.amazon.nova-premier-v1:0", + "nova-micro-v1:0": "amazon.nova-micro-v1:0", + "nova-lite-v1:0": "amazon.nova-lite-v1:0", + "nova-pro-v1:0": "amazon.nova-pro-v1:0", + "nova-premier-v1:0": "amazon.nova-premier-v1:0", } var awsModelCanCrossRegionMap = map[string]map[string]bool{ @@ -65,7 +65,27 @@ var awsModelCanCrossRegionMap = map[string]map[string]bool{ "anthropic.claude-opus-4-1-20250805-v1:0": { "us": true, }, -} + // Nova models - all support three major regions + "amazon.nova-micro-v1:0": { + "us": true, + "eu": true, + "apac": true, + }, + "amazon.nova-lite-v1:0": { + "us": true, + "eu": true, + "apac": true, + }, + "amazon.nova-pro-v1:0": { + "us": true, + "eu": true, + "apac": true, + }, + "amazon.nova-premier-v1:0": { + "us": true, + "eu": true, + "apac": true, + }} var awsRegionCrossModelPrefixMap = map[string]string{ "us": "us", @@ -77,5 +97,5 @@ var ChannelName = "aws" // 判断是否为Nova模型 func isNovaModel(modelId string) bool { - return strings.HasPrefix(modelId, "amazon.nova-") + return strings.HasPrefix(modelId, "nova-") } diff --git a/relay/channel/aws/dto.go b/relay/channel/aws/dto.go index cef16c11..53daef28 100644 --- a/relay/channel/aws/dto.go +++ b/relay/channel/aws/dto.go @@ -76,6 +76,7 @@ func convertToNovaRequest(req *dto.GeneralOpenAIRequest) *NovaRequest { // 设置推理配置 if req.MaxTokens != 0 || (req.Temperature != nil && *req.Temperature != 0) || req.TopP != 0 { + novaReq.InferenceConfig = &NovaInferenceConfig{} if req.MaxTokens != 0 { novaReq.InferenceConfig.MaxTokens = int(req.MaxTokens) } diff --git a/relay/channel/aws/relay-aws.go b/relay/channel/aws/relay-aws.go index 3df6b33d..eef26855 100644 --- a/relay/channel/aws/relay-aws.go +++ b/relay/channel/aws/relay-aws.go @@ -97,6 +97,12 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (* // 检查是否为Nova模型 isNova, _ := c.Get("is_nova_model") if isNova == true { + // Nova模型也支持跨区域 + awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region) + canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix) + if canCrossRegion { + awsModelId = awsModelCrossRegion(awsModelId, awsRegionPrefix) + } return handleNovaRequest(c, awsCli, info, awsModelId) } From 837f5556d9cf7b345780831c387ecd96a0d0d503 Mon Sep 17 00:00:00 2001 From: huanghejian Date: Thu, 11 Sep 2025 12:31:43 +0800 Subject: [PATCH 4/4] feat: improve nova config --- relay/channel/aws/dto.go | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/relay/channel/aws/dto.go b/relay/channel/aws/dto.go index 53daef28..9c9fe946 100644 --- a/relay/channel/aws/dto.go +++ b/relay/channel/aws/dto.go @@ -75,7 +75,7 @@ func convertToNovaRequest(req *dto.GeneralOpenAIRequest) *NovaRequest { } // 设置推理配置 - if req.MaxTokens != 0 || (req.Temperature != nil && *req.Temperature != 0) || req.TopP != 0 { + if req.MaxTokens != 0 || (req.Temperature != nil && *req.Temperature != 0) || req.TopP != 0 || req.TopK != 0 || req.Stop != nil { novaReq.InferenceConfig = &NovaInferenceConfig{} if req.MaxTokens != 0 { novaReq.InferenceConfig.MaxTokens = int(req.MaxTokens) @@ -86,7 +86,40 @@ func convertToNovaRequest(req *dto.GeneralOpenAIRequest) *NovaRequest { if req.TopP != 0 { novaReq.InferenceConfig.TopP = req.TopP } + if req.TopK != 0 { + novaReq.InferenceConfig.TopK = req.TopK + } + if req.Stop != nil { + if stopSequences := parseStopSequences(req.Stop); len(stopSequences) > 0 { + novaReq.InferenceConfig.StopSequences = stopSequences + } + } } return novaReq } + +// parseStopSequences 解析停止序列,支持字符串或字符串数组 +func parseStopSequences(stop any) []string { + if stop == nil { + return nil + } + + switch v := stop.(type) { + case string: + if v != "" { + return []string{v} + } + case []string: + return v + case []interface{}: + var sequences []string + for _, item := range v { + if str, ok := item.(string); ok && str != "" { + sequences = append(sequences, str) + } + } + return sequences + } + return nil +}