Files

145 lines
5.1 KiB
Go

package aws
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/relay/channel"
"github.com/QuantumNous/new-api/relay/channel/claude"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/gin-gonic/gin"
)
func mantleRequestURL(info *relaycommon.RelayInfo) (string, error) {
parts := strings.Split(info.ApiKey, "|")
if len(parts) != 2 && len(parts) != 3 {
return "", fmt.Errorf("invalid AWS credentials: use APIKey|Region or AccessKey|SecretAccessKey|Region")
}
for _, part := range parts {
if strings.TrimSpace(part) == "" {
return "", fmt.Errorf("invalid AWS credentials: empty credential or region")
}
}
region := parts[len(parts)-1]
for _, ch := range region {
if (ch < 'a' || ch > 'z') && (ch < '0' || ch > '9') && ch != '-' {
return "", fmt.Errorf("invalid AWS region")
}
}
baseURL := strings.TrimRight(info.ChannelBaseUrl, "/")
if baseURL == "" {
baseURL = "https://bedrock-mantle." + region + ".api.aws"
}
endpoint, err := url.Parse(baseURL)
if err != nil || endpoint.Scheme != "https" || endpoint.Host == "" || endpoint.User != nil || endpoint.RawQuery != "" || endpoint.Fragment != "" {
return "", fmt.Errorf("AWS Mantle base URL must be an HTTPS URL without credentials, query, or fragment")
}
switch {
case strings.HasSuffix(endpoint.Path, "/anthropic/v1/messages"):
case strings.HasSuffix(endpoint.Path, "/anthropic/v1"):
endpoint.Path += "/messages"
case strings.HasSuffix(endpoint.Path, "/anthropic"):
endpoint.Path += "/v1/messages"
default:
endpoint.Path += "/anthropic/v1/messages"
}
return endpoint.String(), nil
}
// Mantle uses native Messages JSON and SSE, not the InvokeModel envelope.
// Sign the final payload and headers, including administrator overrides.
func (a *Adaptor) newMantleRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Request, error) {
endpoint, err := mantleRequestURL(info)
if err != nil {
return nil, err
}
if a.IsNova || isNovaModel(getAwsModelID(info.UpstreamModelName)) {
return nil, fmt.Errorf("AWS Mantle Messages API requires a Claude model; use InvokeModel for Amazon Nova")
}
var payload map[string]json.RawMessage
if err := common.DecodeJson(requestBody, &payload); err != nil {
return nil, fmt.Errorf("decode AWS Mantle request: %w", err)
}
if payload == nil {
return nil, fmt.Errorf("AWS Mantle request must be a JSON object")
}
// Use the channel's resolved model even for passthrough requests. Do not
// apply InvokeModel's version suffixes or automatic cross-region prefixes.
model := info.UpstreamModelName
if model == "claude-haiku-4-5-20251001" {
model = "claude-haiku-4-5"
}
if strings.HasPrefix(model, "claude-") {
model = "anthropic." + model
}
payload["model"], err = common.Marshal(model)
if err != nil {
return nil, err
}
delete(payload, "anthropic_version")
delete(payload, "anthropic_beta")
body, err := common.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal AWS Mantle request: %w", err)
}
req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create AWS Mantle request: %w", err)
}
channel.SetupApiRequestHeader(info, c, &req.Header)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("anthropic-version", "2023-06-01")
claude.CommonClaudeHeadersOperation(c, &req.Header, info)
parts := strings.Split(info.ApiKey, "|")
if len(parts) == 2 {
req.Header.Set("x-api-key", parts[0])
}
headerOverride, err := channel.ResolveHeaderOverride(info, c)
if err != nil {
return nil, err
}
for key, value := range headerOverride {
req.Header.Set(key, value)
if strings.EqualFold(key, "Host") {
req.Host = value
}
}
// HTTP/2 and intermediaries can remove hop-by-hop headers. Strip them
// after overrides, including fields named by Connection, so SigV4 covers
// only headers that will reach Mantle. Proxy credentials must stay local.
for _, connection := range req.Header.Values("Connection") {
for name := range strings.SplitSeq(connection, ",") {
req.Header.Del(strings.TrimSpace(name))
}
}
for _, name := range []string{
"Connection", "Proxy-Connection", "Keep-Alive", "Proxy-Authenticate",
"Proxy-Authorization", "Te", "Trailer", "Transfer-Encoding", "Upgrade",
} {
req.Header.Del(name)
}
if len(parts) == 3 {
// Never send an unrelated bearer credential alongside SigV4. The AWS
// signer handles timestamps and signing the exact transmitted payload.
req.Header.Del("Authorization")
req.Header.Del("x-api-key")
credentials := aws.Credentials{AccessKeyID: parts[0], SecretAccessKey: parts[1]}
hash := sha256.Sum256(body)
if err := v4.NewSigner().SignHTTP(req.Context(), credentials, req, hex.EncodeToString(hash[:]), "bedrock-mantle", parts[2], time.Now()); err != nil {
return nil, fmt.Errorf("sign AWS Mantle request: %w", err)
}
}
return req, nil
}