Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = constant.APITypeReplicate
case constant.ChannelTypeCodex:
apiType = constant.APITypeCodex
case constant.ChannelTypeClaudeOnAws:
apiType = constant.APITypeClaudeOnAws
}
if apiType == -1 {
return constant.APITypeOpenAI, false
Expand Down
2 changes: 2 additions & 0 deletions common/endpoint_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant
// endpointTypes = []constant.EndpointType{constant.EndpointTypeJimeng}
case constant.ChannelTypeAws:
fallthrough
case constant.ChannelTypeClaudeOnAws:
fallthrough
case constant.ChannelTypeAnthropic:
endpointTypes = []constant.EndpointType{constant.EndpointTypeAnthropic, constant.EndpointTypeOpenAI}
case constant.ChannelTypeVertexAi:
Expand Down
1 change: 1 addition & 0 deletions constant/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ const (
APITypeMiniMax
APITypeReplicate
APITypeCodex
APITypeClaudeOnAws
APITypeDummy // this one is only for count, do not add any channel after this
)
3 changes: 3 additions & 0 deletions constant/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const (
ChannelTypeSora = 55
ChannelTypeReplicate = 56
ChannelTypeCodex = 57
ChannelTypeClaudeOnAws = 58
ChannelTypeDummy // this one is only for count, do not add any channel after this

)
Expand Down Expand Up @@ -118,6 +119,7 @@ var ChannelBaseURLs = []string{
"https://api.openai.com", //55
"https://api.replicate.com", //56
"https://chatgpt.com", //57
"", //58
}

var ChannelTypeNames = map[int]string{
Expand Down Expand Up @@ -175,6 +177,7 @@ var ChannelTypeNames = map[int]string{
ChannelTypeSora: "Sora",
ChannelTypeReplicate: "Replicate",
ChannelTypeCodex: "Codex",
ChannelTypeClaudeOnAws: "ClaudePlatformOnAWS",
}

func GetChannelTypeName(channelType int) string {
Expand Down
46 changes: 29 additions & 17 deletions dto/channel_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,36 @@ const (
AwsKeyTypeApiKey AwsKeyType = "api_key"
)

// ClaudeOnAwsAuthType selects the auth strategy for the
// "Claude Platform on AWS" channel.
type ClaudeOnAwsAuthType string

const (
ClaudeOnAwsAuthSigV4 ClaudeOnAwsAuthType = "sigv4" // Default: AWS SigV4 (IAM) signing.
ClaudeOnAwsAuthApiKey ClaudeOnAwsAuthType = "api_key" // Bearer key issued in the AWS Console.
)

type ChannelOtherSettings struct {
AzureResponsesVersion string `json:"azure_responses_version,omitempty"`
VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key"
OpenRouterEnterprise *bool `json:"openrouter_enterprise,omitempty"`
ClaudeBetaQuery bool `json:"claude_beta_query,omitempty"` // Claude 渠道是否强制追加 ?beta=true
AllowServiceTier bool `json:"allow_service_tier,omitempty"` // 是否允许 service_tier 透传(默认过滤以避免额外计费)
AllowInferenceGeo bool `json:"allow_inference_geo,omitempty"` // 是否允许 inference_geo 透传(仅 Claude,默认过滤以满足数据驻留合规
AllowSpeed bool `json:"allow_speed,omitempty"` // 是否允许 speed 透传(仅 Claude,默认过滤以避免意外切换推理速度模式)
AllowSafetyIdentifier bool `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私)
DisableStore bool `json:"disable_store,omitempty"` // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用)
AllowIncludeObfuscation bool `json:"allow_include_obfuscation,omitempty"` // 是否允许 stream_options.include_obfuscation 透传(默认过滤以避免关闭流混淆保护)
AwsKeyType AwsKeyType `json:"aws_key_type,omitempty"`
UpstreamModelUpdateCheckEnabled bool `json:"upstream_model_update_check_enabled,omitempty"` // 是否检测上游模型更新
UpstreamModelUpdateAutoSyncEnabled bool `json:"upstream_model_update_auto_sync_enabled,omitempty"` // 是否自动同步上游模型更新
UpstreamModelUpdateLastCheckTime int64 `json:"upstream_model_update_last_check_time,omitempty"` // 上次检测时间
UpstreamModelUpdateLastDetectedModels []string `json:"upstream_model_update_last_detected_models,omitempty"` // 上次检测到的可加入模型
UpstreamModelUpdateLastRemovedModels []string `json:"upstream_model_update_last_removed_models,omitempty"` // 上次检测到的可删除模型
UpstreamModelUpdateIgnoredModels []string `json:"upstream_model_update_ignored_models,omitempty"` // 手动忽略的模型
AzureResponsesVersion string `json:"azure_responses_version,omitempty"`
VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key"
OpenRouterEnterprise *bool `json:"openrouter_enterprise,omitempty"`
ClaudeBetaQuery bool `json:"claude_beta_query,omitempty"` // Claude 渠道是否强制追加 ?beta=true
AllowServiceTier bool `json:"allow_service_tier,omitempty"` // 是否允许 service_tier 透传(默认过滤以避免额外计费)
AllowInferenceGeo bool `json:"allow_inference_geo,omitempty"` // 是否允许 inference_geo 透传(仅 Claude,默认过滤以满足数据驻留合规
AllowSpeed bool `json:"allow_speed,omitempty"` // 是否允许 speed 透传(仅 Claude,默认过滤以避免意外切换推理速度模式)
AllowSafetyIdentifier bool `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私)
DisableStore bool `json:"disable_store,omitempty"` // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用)
AllowIncludeObfuscation bool `json:"allow_include_obfuscation,omitempty"` // 是否允许 stream_options.include_obfuscation 透传(默认过滤以避免关闭流混淆保护)
AwsKeyType AwsKeyType `json:"aws_key_type,omitempty"`
ClaudeOnAwsAuthType ClaudeOnAwsAuthType `json:"claude_on_aws_auth_type,omitempty"` // Claude Platform on AWS auth type: "sigv4" (default) or "api_key"
ClaudeOnAwsRegion string `json:"claude_on_aws_region,omitempty"` // Claude Platform on AWS region, e.g. us-east-1, us-west-2
ClaudeOnAwsWorkspaceID string `json:"claude_on_aws_workspace_id,omitempty"` // Required anthropic-workspace-id header value, format wrkspc_xxx
UpstreamModelUpdateCheckEnabled bool `json:"upstream_model_update_check_enabled,omitempty"` // 是否检测上游模型更新
UpstreamModelUpdateAutoSyncEnabled bool `json:"upstream_model_update_auto_sync_enabled,omitempty"` // 是否自动同步上游模型更新
UpstreamModelUpdateLastCheckTime int64 `json:"upstream_model_update_last_check_time,omitempty"` // 上次检测时间
UpstreamModelUpdateLastDetectedModels []string `json:"upstream_model_update_last_detected_models,omitempty"` // 上次检测到的可加入模型
UpstreamModelUpdateLastRemovedModels []string `json:"upstream_model_update_last_removed_models,omitempty"` // 上次检测到的可删除模型
UpstreamModelUpdateIgnoredModels []string `json:"upstream_model_update_ignored_models,omitempty"` // 手动忽略的模型
}

func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool {
Expand Down
192 changes: 192 additions & 0 deletions relay/channel/claude_platform_on_aws/adaptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package claude_platform_on_aws

import (
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"

"github.com/QuantumNous/new-api/dto"
"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/QuantumNous/new-api/types"

"github.com/gin-gonic/gin"
)

// Adaptor implements the channel adapter for "Claude Platform on AWS".
//
// The wire protocol is identical to the first-party Anthropic Messages API
// (POST /v1/messages). The only differences from the standard Anthropic
// channel are:
//
// - Base URL takes the form https://aws-external-anthropic.{region}.api.aws
// - Authentication uses either AWS SigV4 (IAM) or a Bearer API key issued
// in the AWS Console
// - Each request must carry an additional anthropic-workspace-id header
//
// See https://docs.aws.amazon.com/claude-platform/latest/userguide/welcome.html
//
// Implementation strategy: embed claude.Adaptor and only override
// GetRequestURL / SetupRequestHeader / DoRequest / GetChannelName /
// GetModelList. Everything else (request/response parsing including
// streaming, tool use, thinking, etc.) is reused as-is from the Claude
// channel.
type Adaptor struct {
claude.Adaptor
}

// resolveRegion prefers ChannelOtherSettings.ClaudeOnAwsRegion, falling back
// to the generic ApiVersion field if the region was put there by mistake.
func resolveRegion(info *relaycommon.RelayInfo) string {
if info == nil {
return ""
}
if r := strings.TrimSpace(info.ChannelOtherSettings.ClaudeOnAwsRegion); r != "" {
return r
}
return strings.TrimSpace(info.ApiVersion)
}

// resolveWorkspaceID prefers the workspace configured on the channel,
// then falls back to the anthropic-workspace-id header on the incoming
// request (so multiple workspaces can share a single channel if desired).
func resolveWorkspaceID(c *gin.Context, info *relaycommon.RelayInfo) string {
if info != nil {
if w := strings.TrimSpace(info.ChannelOtherSettings.ClaudeOnAwsWorkspaceID); w != "" {
return w
}
}
if c != nil && c.Request != nil {
if w := strings.TrimSpace(c.Request.Header.Get("anthropic-workspace-id")); w != "" {
return w
}
}
return ""
}

// GetChannelName returns the human-readable channel identifier used in
// logs and admin dashboards.
func (a *Adaptor) GetChannelName() string {
return ChannelName
}

// GetModelList returns the list of model IDs supported by Claude Platform
// on AWS. The list is reused from the first-party Claude channel because
// AWS publishes the same model IDs.
func (a *Adaptor) GetModelList() []string {
return ModelList
}

// Init is intentionally empty, matching claude.Adaptor.Init.
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {}

// GetRequestURL returns the regional /v1/messages endpoint for Claude
// Platform on AWS.
//
// Behaviour:
// - If the channel's base URL is empty, the URL is auto-built from region.
// - If the channel sets a custom base URL (e.g. a corporate proxy),
// /v1/messages is appended to it.
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
region := resolveRegion(info)
base := strings.TrimRight(info.ChannelBaseUrl, "/")
if base == "" {
if region == "" {
return "", errors.New("claude platform on aws: region is required (set it in channel other_settings.claude_on_aws_region)")
}
base = fmt.Sprintf(EndpointTemplate, region)
}
return base + "/v1/messages", nil
}

// SetupRequestHeader sets the headers required by Claude Platform on AWS.
// In SigV4 mode the actual Authorization / X-Amz-* headers are set later
// in DoRequest, where the request body is available for signing.
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
channel.SetupApiRequestHeader(info, c, req)

// anthropic-version
anthropicVersion := strings.TrimSpace(c.Request.Header.Get("anthropic-version"))
if anthropicVersion == "" {
anthropicVersion = DefaultAnthropicVersion
}
req.Set("anthropic-version", anthropicVersion)

// anthropic-workspace-id is required.
wsID := resolveWorkspaceID(c, info)
if wsID == "" {
return errors.New("claude platform on aws: anthropic-workspace-id is required (set it in channel other_settings.claude_on_aws_workspace_id or send via header)")
}
req.Set("anthropic-workspace-id", wsID)

// Pass through anthropic-beta and Claude common headers (custom headers etc.).
claude.CommonClaudeHeadersOperation(c, req, info)

// API Key mode: set Bearer immediately. SigV4 mode signs in DoRequest.
if info.ChannelOtherSettings.ClaudeOnAwsAuthType == dto.ClaudeOnAwsAuthApiKey {
req.Set("Authorization", "Bearer "+info.ApiKey)
}
return nil
}

// DoRequest takes over the full request flow when SigV4 is selected: the
// body must be read in full to compute the payload hash, signed onto the
// *http.Request, and only then dispatched. API key mode goes through the
// shared channel.DoApiRequest helper.
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
authType := info.ChannelOtherSettings.ClaudeOnAwsAuthType
if authType == "" {
authType = dto.ClaudeOnAwsAuthSigV4 // SigV4 is the default.
}

if authType == dto.ClaudeOnAwsAuthApiKey {
return channel.DoApiRequest(a, c, info, requestBody)
}

// === SigV4 path ===
region := resolveRegion(info)
if region == "" {
return nil, errors.New("claude platform on aws: region is required for sigv4 auth")
}
creds, err := parseSigV4ApiKey(info.ApiKey)
if err != nil {
return nil, fmt.Errorf("claude platform on aws: %w", err)
}

fullURL, err := a.GetRequestURL(info)
if err != nil {
return nil, fmt.Errorf("get request url failed: %w", err)
}

httpReq, err := http.NewRequestWithContext(c.Request.Context(), c.Request.Method, fullURL, requestBody)
if err != nil {
return nil, fmt.Errorf("new request failed: %w", err)
}

headers := httpReq.Header
if err := a.SetupRequestHeader(c, &headers, info); err != nil {
return nil, fmt.Errorf("setup request header failed: %w", err)
}

// Read the body for signing and put the same bytes back so client.Do can read them.
bodyBytes, err := readAllAndReset(httpReq)
if err != nil {
return nil, fmt.Errorf("read request body failed: %w", err)
}

if err := signRequestSigV4(httpReq, bodyBytes, creds, region, SigV4ServiceName, time.Now()); err != nil {
return nil, fmt.Errorf("sigv4 sign failed: %w", err)
}

return channel.DoRequest(c, httpReq, info)
}

// DoResponse delegates to claude.Adaptor; that implementation already sets
// info.FinalRequestRelayFormat = types.RelayFormatClaude as needed.
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (any, *types.NewAPIError) {
return a.Adaptor.DoResponse(c, resp, info)
}
23 changes: 23 additions & 0 deletions relay/channel/claude_platform_on_aws/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package claude_platform_on_aws

import (
"github.com/QuantumNous/new-api/relay/channel/claude"
)

// ChannelName is used as the channel's log / identifier name.
const ChannelName = "claude-platform-on-aws"

// SigV4ServiceName is the AWS SigV4 service name for this endpoint.
// See: https://docs.aws.amazon.com/claude-platform/latest/userguide/making-requests.html
const SigV4ServiceName = "aws-external-anthropic"

// DefaultAnthropicVersion matches the value used by the first-party Anthropic API.
const DefaultAnthropicVersion = "2023-06-01"

// EndpointTemplate is the default region-rendered base URL.
// Used as a fallback when the channel's base URL is left empty.
const EndpointTemplate = "https://aws-external-anthropic.%s.api.aws"

// ModelList reuses the Claude channel's model list — Claude Platform on AWS
// publishes the exact same model IDs as the first-party Claude API.
var ModelList = claude.ModelList
Loading