145 lines
5.1 KiB
Go
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
|
|
}
|