diff --git a/common/api_type.go b/common/api_type.go index 39c1fe9a5406..1c9588159f24 100644 --- a/common/api_type.go +++ b/common/api_type.go @@ -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 diff --git a/common/endpoint_type.go b/common/endpoint_type.go index a5e2ff8412e8..59a3c0ed6f3e 100644 --- a/common/endpoint_type.go +++ b/common/endpoint_type.go @@ -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: diff --git a/constant/api_type.go b/constant/api_type.go index 536ebd2c7198..5c19b1b3b5c5 100644 --- a/constant/api_type.go +++ b/constant/api_type.go @@ -36,5 +36,6 @@ const ( APITypeMiniMax APITypeReplicate APITypeCodex + APITypeClaudeOnAws APITypeDummy // this one is only for count, do not add any channel after this ) diff --git a/constant/channel.go b/constant/channel.go index 48502bedc52c..e45beae32692 100644 --- a/constant/channel.go +++ b/constant/channel.go @@ -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 ) @@ -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{ @@ -175,6 +177,7 @@ var ChannelTypeNames = map[int]string{ ChannelTypeSora: "Sora", ChannelTypeReplicate: "Replicate", ChannelTypeCodex: "Codex", + ChannelTypeClaudeOnAws: "ClaudePlatformOnAWS", } func GetChannelTypeName(channelType int) string { diff --git a/dto/channel_settings.go b/dto/channel_settings.go index b6a1ab9f7138..620ea19fa702 100644 --- a/dto/channel_settings.go +++ b/dto/channel_settings.go @@ -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 { diff --git a/relay/channel/claude_platform_on_aws/adaptor.go b/relay/channel/claude_platform_on_aws/adaptor.go new file mode 100644 index 000000000000..bb7b74d6756d --- /dev/null +++ b/relay/channel/claude_platform_on_aws/adaptor.go @@ -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) +} diff --git a/relay/channel/claude_platform_on_aws/constants.go b/relay/channel/claude_platform_on_aws/constants.go new file mode 100644 index 000000000000..b952e8c8fb1f --- /dev/null +++ b/relay/channel/claude_platform_on_aws/constants.go @@ -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 diff --git a/relay/channel/claude_platform_on_aws/sigv4.go b/relay/channel/claude_platform_on_aws/sigv4.go new file mode 100644 index 000000000000..b2668aa511b6 --- /dev/null +++ b/relay/channel/claude_platform_on_aws/sigv4.go @@ -0,0 +1,333 @@ +package claude_platform_on_aws + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "sort" + "strings" + "time" +) + +// AwsCredentials holds the credential triplet required for SigV4 signing. +// SessionToken is only present when temporary credentials (STS / SSO / IRSA) +// are used. +type AwsCredentials struct { + AccessKeyID string + SecretAccessKey string + SessionToken string +} + +// parseSigV4ApiKey parses the credentials stored in the channel's ApiKey field. +// +// Accepted formats: +// - "|" long-term IAM user credentials +// - "||" temporary (STS / SSO / IRSA) credentials +// +// This mirrors the AWS Bedrock channel's ApiKey parsing convention +// (which uses "|"). +func parseSigV4ApiKey(apiKey string) (AwsCredentials, error) { + parts := strings.Split(apiKey, "|") + switch len(parts) { + case 2: + ak := strings.TrimSpace(parts[0]) + sk := strings.TrimSpace(parts[1]) + if ak == "" || sk == "" { + return AwsCredentials{}, errors.New("invalid sigv4 api key: access key id and secret access key are required") + } + return AwsCredentials{AccessKeyID: ak, SecretAccessKey: sk}, nil + case 3: + ak := strings.TrimSpace(parts[0]) + sk := strings.TrimSpace(parts[1]) + token := strings.TrimSpace(parts[2]) + if ak == "" || sk == "" || token == "" { + return AwsCredentials{}, errors.New("invalid sigv4 api key: access key id, secret access key and session token are required") + } + return AwsCredentials{AccessKeyID: ak, SecretAccessKey: sk, SessionToken: token}, nil + default: + return AwsCredentials{}, errors.New("invalid sigv4 api key, expected '|' or '||'") + } +} + +// signRequestSigV4 signs an *http.Request with AWS Signature Version 4 and +// mutates req.Header in place. +// +// Implementation follows the AWS official spec at +// https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html +// +// Note: the body must be available for the entire signing pass. Callers +// must arrange this (typically via readAllAndReset which both returns the +// payload bytes and re-installs req.Body / req.GetBody). +func signRequestSigV4(req *http.Request, body []byte, creds AwsCredentials, region, service string, signTime time.Time) error { + if req == nil { + return errors.New("nil request") + } + if creds.AccessKeyID == "" || creds.SecretAccessKey == "" { + return errors.New("missing aws credentials") + } + if region == "" { + return errors.New("missing aws region") + } + if service == "" { + return errors.New("missing aws service") + } + + signTime = signTime.UTC() + amzDate := signTime.Format("20060102T150405Z") + dateStamp := signTime.Format("20060102") + + // Canonical host + host := req.Host + if host == "" { + host = req.URL.Host + } + + // Required headers for SigV4 — set before computing the canonical request. + req.Header.Set("Host", host) + req.Header.Set("X-Amz-Date", amzDate) + if creds.SessionToken != "" { + req.Header.Set("X-Amz-Security-Token", creds.SessionToken) + } + + // Hashed payload — the SigV4 spec uses lower-case hex sha256 of the body. + // Note: we do NOT set X-Amz-Content-Sha256 as a header. That header is + // only signed for S3-style requests; for normal services like + // aws-external-anthropic the AWS reference signer leaves it out. The + // hash still feeds into the canonical request via the trailing field. + payloadHash := hashSHA256Hex(body) + + canonicalURIv := canonicalURI(req.URL.Path) + canonicalQueryv := canonicalQuery(req.URL.RawQuery) + canonicalHeadersv, signedHeaders := canonicalHeaders(req.Header, host) + + canonicalRequest := strings.Join([]string{ + req.Method, + canonicalURIv, + canonicalQueryv, + canonicalHeadersv, + signedHeaders, + payloadHash, + }, "\n") + + credentialScope := fmt.Sprintf("%s/%s/%s/aws4_request", dateStamp, region, service) + stringToSign := strings.Join([]string{ + "AWS4-HMAC-SHA256", + amzDate, + credentialScope, + hashSHA256Hex([]byte(canonicalRequest)), + }, "\n") + + signingKey := deriveSigningKey(creds.SecretAccessKey, dateStamp, region, service) + signature := hex.EncodeToString(hmacSHA256(signingKey, []byte(stringToSign))) + + authorization := fmt.Sprintf( + "AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s", + creds.AccessKeyID, credentialScope, signedHeaders, signature, + ) + req.Header.Set("Authorization", authorization) + return nil +} + +// canonicalURI percent-encodes the URL path per the SigV4 spec, but does +// NOT encode '/'. Our target endpoint path is just "/v1/messages", which +// makes a simple segment-by-segment encoding sufficient. +func canonicalURI(path string) string { + if path == "" { + return "/" + } + segments := strings.Split(path, "/") + for i, seg := range segments { + segments[i] = awsURIEscape(seg, false) + } + return strings.Join(segments, "/") +} + +// canonicalQuery sorts the query string parameters and percent-encodes +// each key and value separately, then joins them with '&'. +func canonicalQuery(rawQuery string) string { + if rawQuery == "" { + return "" + } + pairs := strings.Split(rawQuery, "&") + type kv struct{ k, v string } + parsed := make([]kv, 0, len(pairs)) + for _, p := range pairs { + if p == "" { + continue + } + eq := strings.IndexByte(p, '=') + if eq < 0 { + parsed = append(parsed, kv{k: awsURIEscape(p, true), v: ""}) + } else { + parsed = append(parsed, kv{ + k: awsURIEscape(p[:eq], true), + v: awsURIEscape(p[eq+1:], true), + }) + } + } + sort.Slice(parsed, func(i, j int) bool { + if parsed[i].k == parsed[j].k { + return parsed[i].v < parsed[j].v + } + return parsed[i].k < parsed[j].k + }) + out := make([]string, len(parsed)) + for i, p := range parsed { + out[i] = p.k + "=" + p.v + } + return strings.Join(out, "&") +} + +// canonicalHeaders returns (canonicalHeaders, signedHeaders) per the SigV4 +// rules: lower-case header names, sorted by name, internal whitespace +// collapsed, formatted as "header:value\n" lines and joined. +func canonicalHeaders(h http.Header, host string) (string, string) { + type kv struct { + key string + value string + } + pairs := make([]kv, 0, len(h)+1) + + // host is handled explicitly so that it is always signed. + pairs = append(pairs, kv{key: "host", value: trimAllWS(host)}) + + for name, values := range h { + lower := strings.ToLower(name) + if lower == "host" { + continue // already added + } + // SigV4 minimally requires host + x-amz-* + content-type. We adopt a + // safe default: sign every header except the ones below. + if lower == "authorization" { + continue + } + // Combine multiple values with ", ", then collapse internal whitespace. + val := strings.Join(values, ",") + pairs = append(pairs, kv{key: lower, value: trimAllWS(val)}) + } + + // Sort by key, merge values for duplicate keys. + sort.Slice(pairs, func(i, j int) bool { return pairs[i].key < pairs[j].key }) + merged := pairs[:0] + for _, p := range pairs { + if len(merged) > 0 && merged[len(merged)-1].key == p.key { + merged[len(merged)-1].value += "," + p.value + continue + } + merged = append(merged, p) + } + + var canon bytes.Buffer + signedKeys := make([]string, len(merged)) + for i, p := range merged { + canon.WriteString(p.key) + canon.WriteByte(':') + canon.WriteString(p.value) + canon.WriteByte('\n') + signedKeys[i] = p.key + } + return canon.String(), strings.Join(signedKeys, ";") +} + +// awsURIEscape implements RFC 3986 percent-encoding per the AWS SigV4 +// spec. Unencoded characters are A-Z a-z 0-9 - _ . ~ . If isQuery is false, +// '/' is also left unencoded (used for path segments). +func awsURIEscape(s string, isQuery bool) string { + var b strings.Builder + b.Grow(len(s)) + for i := 0; i < len(s); i++ { + c := s[i] + switch { + case c >= 'A' && c <= 'Z', + c >= 'a' && c <= 'z', + c >= '0' && c <= '9', + c == '-', c == '_', c == '.', c == '~': + b.WriteByte(c) + case c == '/' && !isQuery: + b.WriteByte(c) + default: + b.WriteString(fmt.Sprintf("%%%02X", c)) + } + } + return b.String() +} + +// trimAllWS collapses runs of whitespace to a single space and trims the +// ends. SigV4 requires that header values have internal whitespace +// collapsed (outside of quoted regions) so that signing is deterministic. +func trimAllWS(s string) string { + s = strings.TrimSpace(s) + if !strings.ContainsAny(s, " \t") { + return s + } + var b strings.Builder + prevSpace := false + for _, r := range s { + if r == ' ' || r == '\t' { + if !prevSpace { + b.WriteByte(' ') + prevSpace = true + } + continue + } + prevSpace = false + b.WriteRune(r) + } + return b.String() +} + +// hashSHA256Hex returns the lower-case hex SHA-256 digest of data. +// SigV4 uses this for both the canonical request hash and the payload +// hash. +func hashSHA256Hex(data []byte) string { + sum := sha256.Sum256(data) + return hex.EncodeToString(sum[:]) +} + +// hmacSHA256 returns HMAC-SHA256(key, data). Used as the building block +// for the SigV4 signing key derivation chain. +func hmacSHA256(key, data []byte) []byte { + h := hmac.New(sha256.New, key) + h.Write(data) + return h.Sum(nil) +} + +// deriveSigningKey computes the SigV4 signing key by chaining HMAC-SHA256 +// over date, region, service and the literal "aws4_request", as defined +// by the AWS Signature Version 4 specification: +// +// kDate = HMAC("AWS4" + secret, dateStamp) +// kRegion = HMAC(kDate, region) +// kService = HMAC(kRegion, service) +// kSigning = HMAC(kService, "aws4_request") +func deriveSigningKey(secretKey, dateStamp, region, service string) []byte { + kDate := hmacSHA256([]byte("AWS4"+secretKey), []byte(dateStamp)) + kRegion := hmacSHA256(kDate, []byte(region)) + kService := hmacSHA256(kRegion, []byte(service)) + return hmacSHA256(kService, []byte("aws4_request")) +} + +// readAllAndReset reads the request body once (for SigV4 payload hashing) +// and re-installs it on the request so that subsequent client.Do can read +// it again. Returns nil bytes for bodyless requests (e.g. GET). +func readAllAndReset(req *http.Request) ([]byte, error) { + if req.Body == nil { + return nil, nil + } + data, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + _ = req.Body.Close() + req.Body = io.NopCloser(bytes.NewReader(data)) + req.GetBody = func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(data)), nil + } + req.ContentLength = int64(len(data)) + return data, nil +} diff --git a/relay/channel/claude_platform_on_aws/sigv4_test.go b/relay/channel/claude_platform_on_aws/sigv4_test.go new file mode 100644 index 000000000000..ac49f4cc01fd --- /dev/null +++ b/relay/channel/claude_platform_on_aws/sigv4_test.go @@ -0,0 +1,165 @@ +package claude_platform_on_aws + +import ( + "net/http" + "strings" + "testing" + "time" +) + +// Test vector taken from the AWS SigV4 official documentation: +// https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html +// (GET ListUsers example for service "iam"). We reuse it because the Claude +// Platform on AWS endpoint is behind the same SigV4 algorithm — passing this +// vector guarantees the algorithm itself is correct. +// +// Request: +// +// GET https://iam.amazonaws.com/?Action=ListUsers&Version=2010-05-08 +// Headers: +// Content-Type: application/x-www-form-urlencoded; charset=utf-8 +// Host: iam.amazonaws.com +// X-Amz-Date: 20150830T123600Z +// Body: empty +// +// Credentials: +// +// AKIDEXAMPLE / wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY +// +// Service/region: iam / us-east-1 +// +// Expected Authorization (per AWS docs): +// +// AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, +// SignedHeaders=content-type;host;x-amz-date, +// Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7 +func TestSigV4_AWSReferenceVector(t *testing.T) { + signTime, _ := time.Parse("20060102T150405Z", "20150830T123600Z") + + req, err := http.NewRequest( + http.MethodGet, + "https://iam.amazonaws.com/?Action=ListUsers&Version=2010-05-08", + nil, + ) + if err != nil { + t.Fatalf("new request: %v", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8") + + creds := AwsCredentials{ + AccessKeyID: "AKIDEXAMPLE", + SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + } + + if err := signRequestSigV4(req, nil, creds, "us-east-1", "iam", signTime); err != nil { + t.Fatalf("sign: %v", err) + } + + got := req.Header.Get("Authorization") + want := "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7" + if got != want { + t.Fatalf("unexpected authorization\n got: %s\n want: %s", got, want) + } + + if h := req.Header.Get("X-Amz-Date"); h != "20150830T123600Z" { + t.Fatalf("X-Amz-Date = %q, want 20150830T123600Z", h) + } +} + +// Sanity test for our claude-platform-on-aws specific path. +func TestSigV4_ClaudePlatformOnAws_AddsRequiredHeaders(t *testing.T) { + signTime := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC) + body := []byte(`{"model":"claude-sonnet-4-6","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}`) + + req, err := http.NewRequest(http.MethodPost, "https://aws-external-anthropic.us-west-2.api.aws/v1/messages", strings.NewReader(string(body))) + if err != nil { + t.Fatalf("new request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("anthropic-version", "2023-06-01") + req.Header.Set("anthropic-workspace-id", "wrkspc_demo") + + creds := AwsCredentials{ + AccessKeyID: "AKIDEXAMPLE", + SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + SessionToken: "FQoGZX-token", + } + if err := signRequestSigV4(req, body, creds, "us-west-2", SigV4ServiceName, signTime); err != nil { + t.Fatalf("sign: %v", err) + } + + if got := req.Header.Get("X-Amz-Security-Token"); got != "FQoGZX-token" { + t.Fatalf("X-Amz-Security-Token = %q, want FQoGZX-token", got) + } + if got := req.Header.Get("X-Amz-Date"); got != "20260102T030405Z" { + t.Fatalf("X-Amz-Date = %q, want 20260102T030405Z", got) + } + auth := req.Header.Get("Authorization") + if !strings.HasPrefix(auth, "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20260102/us-west-2/"+SigV4ServiceName+"/aws4_request, ") { + t.Fatalf("authorization prefix wrong: %q", auth) + } + // Signed headers must include host, the security token, content-type, and the anthropic-* headers. + for _, want := range []string{"host", "x-amz-date", "x-amz-security-token", "content-type", "anthropic-version", "anthropic-workspace-id"} { + if !strings.Contains(auth, want) { + t.Fatalf("authorization missing signed header %q: %s", want, auth) + } + } +} + +// TestParseSigV4ApiKey covers the accepted API key formats for SigV4 +// authentication: two-part long-term credentials, three-part temporary +// credentials, and the malformed inputs that must be rejected. +func TestParseSigV4ApiKey(t *testing.T) { + cases := []struct { + name string + raw string + wantErr bool + ak, sk string + token string + }{ + {name: "two parts", raw: "AKID|SECRET", ak: "AKID", sk: "SECRET"}, + {name: "three parts", raw: "AKID|SECRET|TOKEN", ak: "AKID", sk: "SECRET", token: "TOKEN"}, + {name: "single part invalid", raw: "AKID", wantErr: true}, + {name: "four parts invalid", raw: "a|b|c|d", wantErr: true}, + {name: "empty fields invalid", raw: "AKID|", wantErr: true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := parseSigV4ApiKey(tc.raw) + if tc.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.AccessKeyID != tc.ak || got.SecretAccessKey != tc.sk || got.SessionToken != tc.token { + t.Fatalf("got %+v, want ak=%q sk=%q token=%q", got, tc.ak, tc.sk, tc.token) + } + }) + } +} + +// TestCanonicalQuery exercises canonicalQuery against the SigV4 spec rules: +// empty query, already-sorted pairs, out-of-order pairs that must be +// re-sorted, repeated keys preserved in original value order, and +// percent-encoding of reserved characters such as space. +func TestCanonicalQuery(t *testing.T) { + cases := []struct { + in, out string + }{ + {"", ""}, + {"a=1&b=2", "a=1&b=2"}, + {"b=2&a=1", "a=1&b=2"}, + {"a=1&a=2", "a=1&a=2"}, + {"key with space=value", "key%20with%20space=value"}, + } + for _, tc := range cases { + got := canonicalQuery(tc.in) + if got != tc.out { + t.Fatalf("canonicalQuery(%q) = %q, want %q", tc.in, got, tc.out) + } + } +} diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 64d4d4eedfaa..138a82cced86 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -312,6 +312,7 @@ func (info *RelayInfo) ToString() string { var streamSupportedChannels = map[int]bool{ constant.ChannelTypeOpenAI: true, constant.ChannelTypeAnthropic: true, + constant.ChannelTypeClaudeOnAws: true, constant.ChannelTypeAws: true, constant.ChannelTypeGemini: true, constant.ChannelCloudflare: true, diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index 3139c9a2dd4a..f6c89d5276d4 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -10,6 +10,7 @@ import ( "github.com/QuantumNous/new-api/relay/channel/baidu" "github.com/QuantumNous/new-api/relay/channel/baidu_v2" "github.com/QuantumNous/new-api/relay/channel/claude" + claude_platform_on_aws "github.com/QuantumNous/new-api/relay/channel/claude_platform_on_aws" "github.com/QuantumNous/new-api/relay/channel/cloudflare" "github.com/QuantumNous/new-api/relay/channel/codex" "github.com/QuantumNous/new-api/relay/channel/cohere" @@ -120,6 +121,8 @@ func GetAdaptor(apiType int) channel.Adaptor { return &replicate.Adaptor{} case constant.APITypeCodex: return &codex.Adaptor{} + case constant.APITypeClaudeOnAws: + return &claude_platform_on_aws.Adaptor{} } return nil } diff --git a/web/classic/src/components/table/channels/modals/EditChannelModal.jsx b/web/classic/src/components/table/channels/modals/EditChannelModal.jsx index fad105b1c223..ee114683edb5 100644 --- a/web/classic/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/classic/src/components/table/channels/modals/EditChannelModal.jsx @@ -200,6 +200,10 @@ const EditChannelModal = (props) => { vertex_key_type: 'json', // 仅 AWS: 密钥格式和区域(存入 settings.aws_key_type 和 settings.aws_region) aws_key_type: 'ak_sk', + // Claude Platform on AWS (type 58) + claude_on_aws_auth_type: 'sigv4', + claude_on_aws_region: '', + claude_on_aws_workspace_id: '', // 企业账户设置 is_enterprise_account: false, // 字段透传控制默认值 @@ -897,6 +901,13 @@ const EditChannelModal = (props) => { data.vertex_key_type = parsedSettings.vertex_key_type || 'json'; // 读取 AWS 密钥格式和区域 data.aws_key_type = parsedSettings.aws_key_type || 'ak_sk'; + // Read Claude Platform on AWS settings + data.claude_on_aws_auth_type = + parsedSettings.claude_on_aws_auth_type || 'sigv4'; + data.claude_on_aws_region = + parsedSettings.claude_on_aws_region || ''; + data.claude_on_aws_workspace_id = + parsedSettings.claude_on_aws_workspace_id || ''; // 读取企业账户设置 data.is_enterprise_account = parsedSettings.openrouter_enterprise === true; @@ -933,6 +944,9 @@ const EditChannelModal = (props) => { data.region = ''; data.vertex_key_type = 'json'; data.aws_key_type = 'ak_sk'; + data.claude_on_aws_auth_type = 'sigv4'; + data.claude_on_aws_region = ''; + data.claude_on_aws_workspace_id = ''; data.is_enterprise_account = false; data.allow_service_tier = false; data.disable_store = false; @@ -951,6 +965,9 @@ const EditChannelModal = (props) => { // 兼容历史数据:老渠道没有 settings 时,默认按 json 展示 data.vertex_key_type = 'json'; data.aws_key_type = 'ak_sk'; + data.claude_on_aws_auth_type = 'sigv4'; + data.claude_on_aws_region = ''; + data.claude_on_aws_workspace_id = ''; data.is_enterprise_account = false; data.allow_service_tier = false; data.disable_store = false; @@ -1778,6 +1795,19 @@ const EditChannelModal = (props) => { settings.aws_key_type = localInputs.aws_key_type || 'ak_sk'; } + // type === 58 (Claude Platform on AWS): persist auth type / region / workspace_id + if (localInputs.type === 58) { + settings.claude_on_aws_auth_type = + localInputs.claude_on_aws_auth_type || 'sigv4'; + settings.claude_on_aws_region = localInputs.claude_on_aws_region || ''; + settings.claude_on_aws_workspace_id = + localInputs.claude_on_aws_workspace_id || ''; + } else { + delete settings.claude_on_aws_auth_type; + delete settings.claude_on_aws_region; + delete settings.claude_on_aws_workspace_id; + } + // type === 41 (Vertex): 始终保存 vertex_key_type 到 settings,避免编辑时被重置 if (localInputs.type === 41) { settings.vertex_key_type = localInputs.vertex_key_type || 'json'; @@ -1840,6 +1870,10 @@ const EditChannelModal = (props) => { delete localInputs.vertex_key_type; // 顶层的 aws_key_type 不应发送给后端 delete localInputs.aws_key_type; + // Strip Claude Platform on AWS transient fields from the payload + delete localInputs.claude_on_aws_auth_type; + delete localInputs.claude_on_aws_region; + delete localInputs.claude_on_aws_workspace_id; // 清理字段透传控制的临时字段 delete localInputs.allow_service_tier; delete localInputs.disable_store; @@ -2694,6 +2728,70 @@ const EditChannelModal = (props) => { )} + {inputs.type === 58 && ( + <> + { + handleChannelOtherSettingsChange( + 'claude_on_aws_auth_type', + value, + ); + }} + extraText={t( + 'API Key mode uses the Bearer key issued in the AWS Console; SigV4 mode uses AK|SK or AK|SK|SessionToken', + )} + /> + { + handleChannelOtherSettingsChange( + 'claude_on_aws_region', + value, + ); + }} + extraText={t( + 'Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws', + )} + /> + { + handleChannelOtherSettingsChange( + 'claude_on_aws_workspace_id', + value, + ); + }} + extraText={t( + 'Required anthropic-workspace-id header. Find it in AWS Console → Claude Platform on AWS → Workspaces.', + )} + /> + + )} + {inputs.type === 41 && ( { : t( '按照如下格式输入:AccessKey|SecretAccessKey|Region', ) - : t(type2secretPrompt(inputs.type)) + : inputs.type === 58 + ? inputs.claude_on_aws_auth_type === 'sigv4' + ? t( + '按照如下格式输入:AccessKeyID|SecretAccessKey 或 AK|SK|SessionToken', + ) + : t(type2secretPrompt(inputs.type)) + : t(type2secretPrompt(inputs.type)) } rules={ isEdit diff --git a/web/classic/src/constants/channel.constants.js b/web/classic/src/constants/channel.constants.js index 9fa78779de8f..2e1d8a95cfe5 100644 --- a/web/classic/src/constants/channel.constants.js +++ b/web/classic/src/constants/channel.constants.js @@ -45,6 +45,11 @@ export const CHANNEL_OPTIONS = [ color: 'indigo', label: 'AWS Claude', }, + { + value: 58, + color: 'indigo', + label: 'Claude Platform on AWS', + }, { value: 41, color: 'blue', label: 'Vertex AI' }, { value: 3, diff --git a/web/classic/src/helpers/render.jsx b/web/classic/src/helpers/render.jsx index 46c95b236831..da235bc093d1 100644 --- a/web/classic/src/helpers/render.jsx +++ b/web/classic/src/helpers/render.jsx @@ -345,6 +345,7 @@ export function getChannelIcon(channelType) { return ; case 14: // Anthropic Claude case 33: // AWS Claude + case 58: // Claude Platform on AWS return ; case 41: // Vertex AI return ; diff --git a/web/classic/src/i18n/locales/en.json b/web/classic/src/i18n/locales/en.json index ea6bca1b4c7e..d966a9367518 100644 --- a/web/classic/src/i18n/locales/en.json +++ b/web/classic/src/i18n/locales/en.json @@ -1643,6 +1643,7 @@ "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "Per request: {{symbol}}{{price}} * {{ratioType}}: {{ratio}} = {{symbol}}{{total}}", "按次计费": "Pay per request", "按照如下格式输入:AccessKey|SecretAccessKey|Region": "Enter in the format: AccessKey|SecretAccessKey|Region", + "按照如下格式输入:AccessKeyID|SecretAccessKey 或 AK|SK|SessionToken": "Enter in the format: AccessKeyID|SecretAccessKey or AK|SK|SessionToken", "按量计费": "Pay as you go", "按量计费下需要先填写输入价格,才能保存其它价格项。": "For per-token billing, fill in the input price before saving other price fields.", "按顺序替换content中的变量占位符": "Replace variable placeholders in content in order", diff --git a/web/classic/src/i18n/locales/fr.json b/web/classic/src/i18n/locales/fr.json index a24d32bad00c..e2279a5bcc45 100644 --- a/web/classic/src/i18n/locales/fr.json +++ b/web/classic/src/i18n/locales/fr.json @@ -1647,6 +1647,7 @@ "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "Par requête : {{symbol}}{{price}} * {{ratioType}} : {{ratio}} = {{symbol}}{{total}}", "按次计费": "Paiement par requête", "按照如下格式输入:AccessKey|SecretAccessKey|Region": "Entrez au format : AccessKey|SecretAccessKey|Region", + "按照如下格式输入:AccessKeyID|SecretAccessKey 或 AK|SK|SessionToken": "Entrez au format : AccessKeyID|SecretAccessKey ou AK|SK|SessionToken", "按量计费": "Paiement à l'utilisation", "按量计费下需要先填写输入价格,才能保存其它价格项。": "En facturation au volume, il faut d'abord renseigner le prix d'entrée avant d'enregistrer les autres prix.", "按顺序替换content中的变量占位符": "Remplacer les espaces réservés de variable dans le contenu dans l'ordre", diff --git a/web/classic/src/i18n/locales/ja.json b/web/classic/src/i18n/locales/ja.json index dde2a1a578e2..630296809627 100644 --- a/web/classic/src/i18n/locales/ja.json +++ b/web/classic/src/i18n/locales/ja.json @@ -1618,6 +1618,7 @@ "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "リクエストごと:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}", "按次计费": "リクエストごとの課金", "按照如下格式输入:AccessKey|SecretAccessKey|Region": "Enter in the format: AccessKey|SecretAccessKey|Region", + "按照如下格式输入:AccessKeyID|SecretAccessKey 或 AK|SK|SessionToken": "次の形式で入力してください: AccessKeyID|SecretAccessKey または AK|SK|SessionToken", "按量计费": "従量課金", "按量计费下需要先填写输入价格,才能保存其它价格项。": "従量課金では、他の価格項目を保存する前に入力価格を設定する必要があります。", "按顺序替换content中的变量占位符": "content内の変数プレースホルダーを順番に置換します", diff --git a/web/classic/src/i18n/locales/ru.json b/web/classic/src/i18n/locales/ru.json index b934dfe1bc5c..060657d27307 100644 --- a/web/classic/src/i18n/locales/ru.json +++ b/web/classic/src/i18n/locales/ru.json @@ -1665,6 +1665,7 @@ "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "За запрос: {{symbol}}{{price}} * {{ratioType}}: {{ratio}} = {{symbol}}{{total}}", "按次计费": "Оплата за запрос", "按照如下格式输入:AccessKey|SecretAccessKey|Region": "Введите в формате: AccessKey|SecretAccessKey|Region", + "按照如下格式输入:AccessKeyID|SecretAccessKey 或 AK|SK|SessionToken": "Введите в формате: AccessKeyID|SecretAccessKey или AK|SK|SessionToken", "按量计费": "Оплата по объему", "按量计费下需要先填写输入价格,才能保存其它价格项。": "При тарификации по объему сначала нужно указать входную цену, чтобы сохранить остальные ценовые поля.", "按顺序替换content中的变量占位符": "Последовательно заменять переменные-заполнители в content", diff --git a/web/classic/src/i18n/locales/vi.json b/web/classic/src/i18n/locales/vi.json index 771a25fcf201..de9e26dca179 100644 --- a/web/classic/src/i18n/locales/vi.json +++ b/web/classic/src/i18n/locales/vi.json @@ -1619,6 +1619,7 @@ "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "Theo lượt gọi: {{symbol}}{{price}} * {{ratioType}}: {{ratio}} = {{symbol}}{{total}}", "按次计费": "Tính phí theo lượt gọi", "按照如下格式输入:AccessKey|SecretAccessKey|Region": "Enter in the format: AccessKey|SecretAccessKey|Region", + "按照如下格式输入:AccessKeyID|SecretAccessKey 或 AK|SK|SessionToken": "Nhập theo định dạng: AccessKeyID|SecretAccessKey hoặc AK|SK|SessionToken", "按量计费": "Trả tiền theo mức sử dụng", "按量计费下需要先填写输入价格,才能保存其它价格项。": "Ở chế độ tính phí theo lượng, cần điền giá đầu vào trước thì mới lưu được các mục giá khác.", "按顺序替换content中的变量占位符": "Thay thế các trình giữ chỗ biến trong nội dung theo thứ tự", diff --git a/web/classic/src/i18n/locales/zh-CN.json b/web/classic/src/i18n/locales/zh-CN.json index e1141b0414f7..f5023400056e 100644 --- a/web/classic/src/i18n/locales/zh-CN.json +++ b/web/classic/src/i18n/locales/zh-CN.json @@ -1605,6 +1605,7 @@ "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}", "按次计费": "按次计费", "按照如下格式输入:AccessKey|SecretAccessKey|Region": "按照如下格式输入:AccessKey|SecretAccessKey|Region", + "按照如下格式输入:AccessKeyID|SecretAccessKey 或 AK|SK|SessionToken": "按照如下格式输入:AccessKeyID|SecretAccessKey 或 AK|SK|SessionToken", "按量计费": "按量计费", "按量计费下需要先填写输入价格,才能保存其它价格项。": "按量计费下需要先填写输入价格,才能保存其它价格项。", "按顺序替换content中的变量占位符": "按顺序替换content中的变量占位符", diff --git a/web/classic/src/i18n/locales/zh-TW.json b/web/classic/src/i18n/locales/zh-TW.json index 3be48fb4dce5..2d61467f30a5 100644 --- a/web/classic/src/i18n/locales/zh-TW.json +++ b/web/classic/src/i18n/locales/zh-TW.json @@ -1615,6 +1615,7 @@ "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "按次:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}", "按次计费": "按次計費", "按照如下格式输入:AccessKey|SecretAccessKey|Region": "按照如下格式輸入:AccessKey|SecretAccessKey|Region", + "按照如下格式输入:AccessKeyID|SecretAccessKey 或 AK|SK|SessionToken": "按照如下格式輸入:AccessKeyID|SecretAccessKey 或 AK|SK|SessionToken", "按量计费": "按量計費", "按量计费下需要先填写输入价格,才能保存其它价格项。": "按量計費下需要先填寫輸入價格,才能儲存其它價格項。", "按顺序替换content中的变量占位符": "按順序替換content中的變數佔位符", diff --git a/web/classic/src/i18n/locales/zh.json b/web/classic/src/i18n/locales/zh.json index 88ac70c139f9..b623a75d2c0b 100644 --- a/web/classic/src/i18n/locales/zh.json +++ b/web/classic/src/i18n/locales/zh.json @@ -1101,6 +1101,7 @@ "按倍率设置": "按倍率设置", "按次计费": "按次计费", "按照如下格式输入:AccessKey|SecretAccessKey|Region": "按照如下格式输入:AccessKey|SecretAccessKey|Region", + "按照如下格式输入:AccessKeyID|SecretAccessKey 或 AK|SK|SessionToken": "按照如下格式输入:AccessKeyID|SecretAccessKey 或 AK|SK|SessionToken", "按量计费": "按量计费", "按顺序替换content中的变量占位符": "按顺序替换content中的变量占位符", "换脸": "换脸", diff --git a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx index 39a6e1527b55..3a830f3f8e7c 100644 --- a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -1421,6 +1421,106 @@ export function ChannelMutateDrawer({ /> )} + {/* Claude Platform on AWS (type 58) */} + {currentType === 58 && ( + <> + ( + + {t('Authentication Method')} + + + {field.value === 'sigv4' + ? t( + 'SigV4 mode: paste credentials in format AK|SK or AK|SK|SessionToken' + ) + : t( + 'API Key mode: paste the key generated in AWS Console → Claude Platform on AWS → API keys' + )} + + + + )} + /> + + ( + + {t('AWS Region *')} + + + + + {t( + 'Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws' + )} + + + + )} + /> + + ( + + {t('Workspace ID *')} + + + + + {t( + 'Required anthropic-workspace-id header. Find it in AWS Console → Claude Platform on AWS → Workspaces.' + )} + + + + )} + /> + + )} + {/* AI Proxy Library (type 21) */} {currentType === 21 && ( diff --git a/web/default/src/features/channels/constants.ts b/web/default/src/features/channels/constants.ts index ca4009ce0be7..c4e34d0d952c 100644 --- a/web/default/src/features/channels/constants.ts +++ b/web/default/src/features/channels/constants.ts @@ -76,12 +76,13 @@ export const CHANNEL_TYPES = { 55: 'Sora', 56: 'Replicate', 57: 'Codex', + 58: 'Claude Platform on AWS', } as const const CHANNEL_TYPE_DISPLAY_ORDER: number[] = [ - 1, 14, 33, 24, 43, 3, 41, 48, 42, 34, 20, 4, 40, 27, 25, 17, 26, 15, 46, 23, - 18, 45, 31, 35, 49, 19, 47, 37, 38, 39, 11, 8, 57, 22, 21, 44, 2, 5, 36, 50, - 51, 52, 53, 54, 55, 56, + 1, 14, 58, 33, 24, 43, 3, 41, 48, 42, 34, 20, 4, 40, 27, 25, 17, 26, 15, 46, + 23, 18, 45, 31, 35, 49, 19, 47, 37, 38, 39, 11, 8, 57, 22, 21, 44, 2, 5, 36, + 50, 51, 52, 53, 54, 55, 56, ] export const CHANNEL_TYPE_OPTIONS: { value: number; label: string }[] = (() => { @@ -392,6 +393,7 @@ export const TYPE_TO_KEY_PROMPT: Record = { 50: 'Format: AccessKey|SecretKey (or just ApiKey if upstream is New API)', 51: 'Format: Access Key ID|Secret Access Key', 57: 'Paste Codex OAuth JSON credential (access_token / refresh_token / account_id)', + 58: 'API Key mode: paste the key from AWS Console. SigV4 mode: format AK|SK or AK|SK|SessionToken', } export const CHANNEL_TYPE_WARNINGS: Record = { diff --git a/web/default/src/features/channels/lib/channel-form.ts b/web/default/src/features/channels/lib/channel-form.ts index 03db2f2355f3..1615a6cc2a69 100644 --- a/web/default/src/features/channels/lib/channel-form.ts +++ b/web/default/src/features/channels/lib/channel-form.ts @@ -65,6 +65,10 @@ export const channelFormSchema = z.object({ is_enterprise_account: z.boolean().optional(), // OpenRouter specific vertex_key_type: z.enum(['json', 'api_key']).optional(), // Vertex AI specific aws_key_type: z.enum(['ak_sk', 'api_key']).optional(), // AWS specific + // Claude Platform on AWS specific (type 58) + claude_on_aws_auth_type: z.enum(['sigv4', 'api_key']).optional(), + claude_on_aws_region: z.string().optional(), + claude_on_aws_workspace_id: z.string().optional(), azure_responses_version: z.string().optional(), // Azure specific // Field passthrough controls (stored in settings JSON) allow_service_tier: z.boolean().optional(), // OpenAI/Anthropic @@ -78,6 +82,31 @@ export const channelFormSchema = z.object({ upstream_model_update_check_enabled: z.boolean().optional(), upstream_model_update_auto_sync_enabled: z.boolean().optional(), upstream_model_update_ignored_models: z.string().optional(), +}).superRefine((data, ctx) => { + // Claude Platform on AWS (type 58): require region and workspace ID, + // and constrain the auth type so the form cannot submit invalid configs. + if (data.type !== 58) return + if (!data.claude_on_aws_auth_type) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['claude_on_aws_auth_type'], + message: 'Authentication method is required', + }) + } + if (!data.claude_on_aws_region?.trim()) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['claude_on_aws_region'], + message: 'AWS region is required', + }) + } + if (!data.claude_on_aws_workspace_id?.trim()) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['claude_on_aws_workspace_id'], + message: 'Workspace ID is required', + }) + } }) export type ChannelFormValues = z.infer @@ -123,6 +152,10 @@ export const CHANNEL_FORM_DEFAULT_VALUES: ChannelFormValues = { is_enterprise_account: false, vertex_key_type: 'json', aws_key_type: 'ak_sk', + // Claude Platform on AWS specific + claude_on_aws_auth_type: 'sigv4', + claude_on_aws_region: '', + claude_on_aws_workspace_id: '', azure_responses_version: '', // Field passthrough controls allow_service_tier: false, @@ -179,6 +212,9 @@ export function transformChannelToFormDefaults( let azureResponsesVersion = '' let isEnterpriseAccount = false let awsKeyType: 'ak_sk' | 'api_key' = 'ak_sk' + let claudeOnAwsAuthType: 'sigv4' | 'api_key' = 'sigv4' + let claudeOnAwsRegion = '' + let claudeOnAwsWorkspaceId = '' let allowServiceTier = false let disableStore = false let allowSafetyIdentifier = false @@ -197,6 +233,9 @@ export function transformChannelToFormDefaults( azureResponsesVersion = parsed.azure_responses_version || '' isEnterpriseAccount = parsed.openrouter_enterprise === true awsKeyType = parsed.aws_key_type || 'ak_sk' + claudeOnAwsAuthType = parsed.claude_on_aws_auth_type || 'sigv4' + claudeOnAwsRegion = parsed.claude_on_aws_region || '' + claudeOnAwsWorkspaceId = parsed.claude_on_aws_workspace_id || '' allowServiceTier = parsed.allow_service_tier === true disableStore = parsed.disable_store === true allowSafetyIdentifier = parsed.allow_safety_identifier === true @@ -252,6 +291,9 @@ export function transformChannelToFormDefaults( vertex_key_type: vertexKeyType, azure_responses_version: azureResponsesVersion, aws_key_type: awsKeyType, + claude_on_aws_auth_type: claudeOnAwsAuthType, + claude_on_aws_region: claudeOnAwsRegion, + claude_on_aws_workspace_id: claudeOnAwsWorkspaceId, allow_service_tier: allowServiceTier, disable_store: disableStore, allow_include_obfuscation: allowIncludeObfuscation, @@ -324,6 +366,22 @@ function buildSettingsJSON(formData: ChannelFormValues): string { delete settingsObj.aws_key_type } + // Claude Platform on AWS (type 58): auth type, region, workspace id + if (formData.type === 58) { + settingsObj.claude_on_aws_auth_type = + formData.claude_on_aws_auth_type || 'sigv4' + settingsObj.claude_on_aws_region = formData.claude_on_aws_region || '' + settingsObj.claude_on_aws_workspace_id = + formData.claude_on_aws_workspace_id || '' + } else { + if ('claude_on_aws_auth_type' in settingsObj) + delete settingsObj.claude_on_aws_auth_type + if ('claude_on_aws_region' in settingsObj) + delete settingsObj.claude_on_aws_region + if ('claude_on_aws_workspace_id' in settingsObj) + delete settingsObj.claude_on_aws_workspace_id + } + // Field passthrough controls: // - OpenAI (type 1) and Anthropic (type 14): allow_service_tier // - OpenAI only: disable_store, allow_safety_identifier diff --git a/web/default/src/features/channels/lib/channel-type-config.ts b/web/default/src/features/channels/lib/channel-type-config.ts index 097f942f81ab..4d3fcd63ec0a 100644 --- a/web/default/src/features/channels/lib/channel-type-config.ts +++ b/web/default/src/features/channels/lib/channel-type-config.ts @@ -83,6 +83,16 @@ export const CHANNEL_TYPE_CONFIGS: Record = { models: 'claude-3-opus,claude-3-sonnet,claude-3-haiku', }, }, + 58: { + id: 58, + name: CHANNEL_TYPES[58], + icon: 'anthropic', + requiresRegion: true, + hints: { + key: 'API Key from AWS Console, or AK|SK / AK|SK|SessionToken for SigV4', + models: 'claude-sonnet-4-6,claude-opus-4-7,claude-haiku-4-5-20251001', + }, + }, 24: { id: 24, name: CHANNEL_TYPES[24], diff --git a/web/default/src/features/channels/lib/channel-utils.ts b/web/default/src/features/channels/lib/channel-utils.ts index 3b55f15eb63c..6ad17f45c7bb 100644 --- a/web/default/src/features/channels/lib/channel-utils.ts +++ b/web/default/src/features/channels/lib/channel-utils.ts @@ -55,6 +55,7 @@ export function getChannelTypeIcon(type: number): string { // Anthropic 14: 'Claude', // Anthropic + 58: 'Claude', // Claude Platform on AWS // Google family 24: 'Gemini', // Gemini diff --git a/web/default/src/features/channels/types.ts b/web/default/src/features/channels/types.ts index a282053a3a95..75fb2a26ef73 100644 --- a/web/default/src/features/channels/types.ts +++ b/web/default/src/features/channels/types.ts @@ -93,6 +93,10 @@ export interface ChannelOtherSettings { vertex_key_type?: 'json' | 'api_key' openrouter_enterprise?: boolean aws_key_type?: 'ak_sk' | 'api_key' + // Claude Platform on AWS specific + claude_on_aws_auth_type?: 'sigv4' | 'api_key' + claude_on_aws_region?: string + claude_on_aws_workspace_id?: string allow_service_tier?: boolean disable_store?: boolean allow_safety_identifier?: boolean diff --git a/web/default/src/i18n/locales/_reports/_sync-report.json b/web/default/src/i18n/locales/_reports/_sync-report.json index 771b6f6550aa..35d3c250f4ee 100644 --- a/web/default/src/i18n/locales/_reports/_sync-report.json +++ b/web/default/src/i18n/locales/_reports/_sync-report.json @@ -9,33 +9,33 @@ }, "fr": { "file": "fr.json", - "missingCount": 0, + "missingCount": 12, "extrasCount": 0, "untranslatedCount": 21 }, "ja": { "file": "ja.json", - "missingCount": 0, + "missingCount": 12, "extrasCount": 0, - "untranslatedCount": 120 + "untranslatedCount": 132 }, "ru": { "file": "ru.json", - "missingCount": 0, + "missingCount": 12, "extrasCount": 0, - "untranslatedCount": 135 + "untranslatedCount": 147 }, "vi": { "file": "vi.json", - "missingCount": 0, + "missingCount": 12, "extrasCount": 0, - "untranslatedCount": 23 + "untranslatedCount": 26 }, "zh": { "file": "zh.json", "missingCount": 0, "extrasCount": 0, - "untranslatedCount": 99 + "untranslatedCount": 101 } } } diff --git a/web/default/src/i18n/locales/_reports/ja.untranslated.json b/web/default/src/i18n/locales/_reports/ja.untranslated.json index b6d34b23ff93..eb3b72cd777c 100644 --- a/web/default/src/i18n/locales/_reports/ja.untranslated.json +++ b/web/default/src/i18n/locales/_reports/ja.untranslated.json @@ -9,10 +9,14 @@ "Anthropic": "Anthropic", "API URL": "API URL", "API2GPT": "API2GPT", + "Authentication Method": "Authentication Method", + "AWS Region *": "AWS Region *", + "AWS SigV4 (IAM)": "AWS SigV4 (IAM)", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "checkout.session.completed": "checkout.session.completed", "checkout.session.expired": "checkout.session.expired", "Claude": "Claude", + "Claude Platform on AWS": "Claude Platform on AWS", "Cloudflare": "Cloudflare", "Cohere": "Cohere", "Compliance confirmation required": "Compliance confirmation required", @@ -118,5 +122,13 @@ "You commit to using upstream APIs, accounts, keys, quotas, and service capabilities only within the scope of lawful authorization obtained from upstream service providers, model service providers, or relevant rights holders, and will not conduct unauthorized resale, trafficking, distribution, or other non-compliant commercialization.": "You commit to using upstream APIs, accounts, keys, quotas, and service capabilities only within the scope of lawful authorization obtained from upstream service providers, model service providers, or relevant rights holders, and will not conduct unauthorized resale, trafficking, distribution, or other non-compliant commercialization.", "You have legally obtained authorization for the connected model APIs, accounts, keys, and quotas.": "You have legally obtained authorization for the connected model APIs, accounts, keys, and quotas.", "You understand and independently bear legal responsibility arising from deployment, operation, and charging behavior.": "You understand and independently bear legal responsibility arising from deployment, operation, and charging behavior.", - "You understand this compliance reminder is only for risk notice and does not constitute legal advice, a compliance review conclusion, or a guarantee of the legality of your use of this system; you should consult professional legal or compliance advisors based on your actual business scenario.": "You understand this compliance reminder is only for risk notice and does not constitute legal advice, a compliance review conclusion, or a guarantee of the legality of your use of this system; you should consult professional legal or compliance advisors based on your actual business scenario." + "You understand this compliance reminder is only for risk notice and does not constitute legal advice, a compliance review conclusion, or a guarantee of the legality of your use of this system; you should consult professional legal or compliance advisors based on your actual business scenario.": "You understand this compliance reminder is only for risk notice and does not constitute legal advice, a compliance review conclusion, or a guarantee of the legality of your use of this system; you should consult professional legal or compliance advisors based on your actual business scenario.", + "Workspace ID *": "Workspace ID *", + "Select auth method": "Select auth method", + "API Key mode: paste the key generated in AWS Console → Claude Platform on AWS → API keys": "API Key mode: paste the key generated in AWS Console → Claude Platform on AWS → API keys", + "SigV4 mode: paste credentials in format AK|SK or AK|SK|SessionToken": "SigV4 mode: paste credentials in format AK|SK or AK|SK|SessionToken", + "e.g., us-east-1, us-west-2, eu-west-1": "e.g., us-east-1, us-west-2, eu-west-1", + "Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws": "Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws", + "e.g., wrkspc_01abcdefghij": "e.g., wrkspc_01abcdefghij", + "Required anthropic-workspace-id header. Find it in AWS Console → Claude Platform on AWS → Workspaces.": "Required anthropic-workspace-id header. Find it in AWS Console → Claude Platform on AWS → Workspaces." } diff --git a/web/default/src/i18n/locales/_reports/ru.untranslated.json b/web/default/src/i18n/locales/_reports/ru.untranslated.json index fa683ab75872..6c3dc5912d74 100644 --- a/web/default/src/i18n/locales/_reports/ru.untranslated.json +++ b/web/default/src/i18n/locales/_reports/ru.untranslated.json @@ -13,12 +13,16 @@ "All conditions must match before this tier is used.": "All conditions must match before this tier is used.", "Anthropic": "Anthropic", "API2GPT": "API2GPT", + "Authentication Method": "Authentication Method", + "AWS Region *": "AWS Region *", + "AWS SigV4 (IAM)": "AWS SigV4 (IAM)", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "Baidu V2": "Baidu V2", "Base input and output token prices for this tier.": "Base input and output token prices for this tier.", "Cache pricing": "Cache pricing", "checkout.session.completed": "checkout.session.completed", "checkout.session.expired": "checkout.session.expired", + "Claude Platform on AWS": "Claude Platform on AWS", "Cloudflare": "Cloudflare", "Cohere": "Cohere", "Compliance confirmation required": "Compliance confirmation required", @@ -133,5 +137,13 @@ "You have legally obtained authorization for the connected model APIs, accounts, keys, and quotas.": "You have legally obtained authorization for the connected model APIs, accounts, keys, and quotas.", "You understand and independently bear legal responsibility arising from deployment, operation, and charging behavior.": "You understand and independently bear legal responsibility arising from deployment, operation, and charging behavior.", "You understand this compliance reminder is only for risk notice and does not constitute legal advice, a compliance review conclusion, or a guarantee of the legality of your use of this system; you should consult professional legal or compliance advisors based on your actual business scenario.": "You understand this compliance reminder is only for risk notice and does not constitute legal advice, a compliance review conclusion, or a guarantee of the legality of your use of this system; you should consult professional legal or compliance advisors based on your actual business scenario.", + "Workspace ID *": "Workspace ID *", + "Select auth method": "Select auth method", + "API Key mode: paste the key generated in AWS Console → Claude Platform on AWS → API keys": "API Key mode: paste the key generated in AWS Console → Claude Platform on AWS → API keys", + "SigV4 mode: paste credentials in format AK|SK or AK|SK|SessionToken": "SigV4 mode: paste credentials in format AK|SK or AK|SK|SessionToken", + "e.g., us-east-1, us-west-2, eu-west-1": "e.g., us-east-1, us-west-2, eu-west-1", + "Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws": "Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws", + "e.g., wrkspc_01abcdefghij": "e.g., wrkspc_01abcdefghij", + "Required anthropic-workspace-id header. Find it in AWS Console → Claude Platform on AWS → Workspaces.": "Required anthropic-workspace-id header. Find it in AWS Console → Claude Platform on AWS → Workspaces.", "Zhipu V4": "Zhipu V4" } diff --git a/web/default/src/i18n/locales/_reports/vi.untranslated.json b/web/default/src/i18n/locales/_reports/vi.untranslated.json index 48dce390e608..ff56fdfbeabf 100644 --- a/web/default/src/i18n/locales/_reports/vi.untranslated.json +++ b/web/default/src/i18n/locales/_reports/vi.untranslated.json @@ -21,5 +21,8 @@ "You commit to using upstream APIs, accounts, keys, quotas, and service capabilities only within the scope of lawful authorization obtained from upstream service providers, model service providers, or relevant rights holders, and will not conduct unauthorized resale, trafficking, distribution, or other non-compliant commercialization.": "You commit to using upstream APIs, accounts, keys, quotas, and service capabilities only within the scope of lawful authorization obtained from upstream service providers, model service providers, or relevant rights holders, and will not conduct unauthorized resale, trafficking, distribution, or other non-compliant commercialization.", "You have legally obtained authorization for the connected model APIs, accounts, keys, and quotas.": "You have legally obtained authorization for the connected model APIs, accounts, keys, and quotas.", "You understand and independently bear legal responsibility arising from deployment, operation, and charging behavior.": "You understand and independently bear legal responsibility arising from deployment, operation, and charging behavior.", - "You understand this compliance reminder is only for risk notice and does not constitute legal advice, a compliance review conclusion, or a guarantee of the legality of your use of this system; you should consult professional legal or compliance advisors based on your actual business scenario.": "You understand this compliance reminder is only for risk notice and does not constitute legal advice, a compliance review conclusion, or a guarantee of the legality of your use of this system; you should consult professional legal or compliance advisors based on your actual business scenario." + "You understand this compliance reminder is only for risk notice and does not constitute legal advice, a compliance review conclusion, or a guarantee of the legality of your use of this system; you should consult professional legal or compliance advisors based on your actual business scenario.": "You understand this compliance reminder is only for risk notice and does not constitute legal advice, a compliance review conclusion, or a guarantee of the legality of your use of this system; you should consult professional legal or compliance advisors based on your actual business scenario.", + "API Key mode: paste the key generated in AWS Console → Claude Platform on AWS → API keys": "API Key mode: paste the key generated in AWS Console → Claude Platform on AWS → API keys", + "SigV4 mode: paste credentials in format AK|SK or AK|SK|SessionToken": "SigV4 mode: paste credentials in format AK|SK or AK|SK|SessionToken", + "Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws": "Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws" } diff --git a/web/default/src/i18n/locales/_reports/zh.untranslated.json b/web/default/src/i18n/locales/_reports/zh.untranslated.json index 12e5e08677f1..66ff9597554d 100644 --- a/web/default/src/i18n/locales/_reports/zh.untranslated.json +++ b/web/default/src/i18n/locales/_reports/zh.untranslated.json @@ -14,6 +14,7 @@ "checkout.session.completed": "checkout.session.completed", "checkout.session.expired": "checkout.session.expired", "Claude": "Claude", + "Claude Platform on AWS": "Claude Platform on AWS", "Client ID": "Client ID", "Client Secret": "Client Secret", "Cloudflare": "Cloudflare", @@ -97,5 +98,6 @@ "Well-Known URL": "Well-Known URL", "whsec_xxx": "whsec_xxx", "Worker URL": "Worker URL", - "Xinference": "Xinference" + "Xinference": "Xinference", + "Workspace ID *": "Workspace ID *" } diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 4f8ae557694c..843d20db3594 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -423,6 +423,7 @@ "Auth configured": "Auth configured", "Auth Style": "Auth Style", "Authentication": "Authentication", + "Authentication Method": "Authentication Method", "Authenticator code": "Authenticator code", "Authorization Endpoint": "Authorization Endpoint", "Authorization Endpoint (Optional)": "Authorization Endpoint (Optional)", @@ -466,6 +467,8 @@ "AWS": "AWS", "AWS Bedrock Claude Compat": "AWS Bedrock Claude Compat", "AWS Key Format": "AWS Key Format", + "AWS Region *": "AWS Region *", + "AWS SigV4 (IAM)": "AWS SigV4 (IAM)", "Azure": "Azure", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "Back": "Back", @@ -699,6 +702,7 @@ "Classic (Legacy Frontend)": "Classic (Legacy Frontend)", "Claude": "Claude", "Claude CLI Header Passthrough": "Claude CLI Header Passthrough", + "Claude Platform on AWS": "Claude Platform on AWS", "Clean": "Clean", "Clean history logs": "Clean history logs", "Clean logs": "Clean logs", @@ -4473,6 +4477,14 @@ "Your transaction history will appear here": "Your transaction history will appear here", "Your Turnstile secret key": "Your Turnstile secret key", "Your Turnstile site key": "Your Turnstile site key", + "Workspace ID *": "Workspace ID *", + "Select auth method": "Select auth method", + "API Key mode: paste the key generated in AWS Console → Claude Platform on AWS → API keys": "API Key mode: paste the key generated in AWS Console → Claude Platform on AWS → API keys", + "SigV4 mode: paste credentials in format AK|SK or AK|SK|SessionToken": "SigV4 mode: paste credentials in format AK|SK or AK|SK|SessionToken", + "e.g., us-east-1, us-west-2, eu-west-1": "e.g., us-east-1, us-west-2, eu-west-1", + "Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws": "Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws", + "e.g., wrkspc_01abcdefghij": "e.g., wrkspc_01abcdefghij", + "Required anthropic-workspace-id header. Find it in AWS Console → Claude Platform on AWS → Workspaces.": "Required anthropic-workspace-id header. Find it in AWS Console → Claude Platform on AWS → Workspaces.", "Zero retention": "Zero retention", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V4", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index 850f83621e43..b4a2d28dec25 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -423,6 +423,7 @@ "Auth configured": "Authentification configurée", "Auth Style": "Style d'authentification", "Authentication": "Authentification", + "Authentication Method": "Méthode d'authentification", "Authenticator code": "Code d'authentification", "Authorization Endpoint": "Point de terminaison d'autorisation", "Authorization Endpoint (Optional)": "Point de terminaison d'autorisation (Facultatif)", @@ -466,6 +467,8 @@ "AWS": "AWS", "AWS Bedrock Claude Compat": "AWS Bedrock Claude Compat", "AWS Key Format": "Format de clé AWS", + "AWS Region *": "Région AWS *", + "AWS SigV4 (IAM)": "AWS SigV4 (IAM)", "Azure": "Azure", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "Back": "Retour", @@ -699,6 +702,7 @@ "Classic (Legacy Frontend)": "Classique (Ancien frontend)", "Claude": "Claude", "Claude CLI Header Passthrough": "Passthrough en-tête Claude CLI", + "Claude Platform on AWS": "Claude Platform on AWS", "Clean": "Sans conflit", "Clean history logs": "Nettoyer les journaux d'historique", "Clean logs": "Nettoyer les logs", @@ -4473,6 +4477,14 @@ "Your transaction history will appear here": "Votre historique de transactions apparaîtra ici", "Your Turnstile secret key": "Votre clé secrète Turnstile", "Your Turnstile site key": "Votre clé de site Turnstile", + "Workspace ID *": "ID de l'espace de travail *", + "Select auth method": "Sélectionner la méthode d'authentification", + "API Key mode: paste the key generated in AWS Console → Claude Platform on AWS → API keys": "Mode clé API : collez la clé générée dans la console AWS → Claude Platform on AWS → Clés API", + "SigV4 mode: paste credentials in format AK|SK or AK|SK|SessionToken": "Mode SigV4 : collez les identifiants au format AK|SK ou AK|SK|SessionToken", + "e.g., us-east-1, us-west-2, eu-west-1": "par ex., us-east-1, us-west-2, eu-west-1", + "Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws": "Région du point de terminaison Claude Platform on AWS, utilisée dans l'URL aws-external-anthropic.{region}.api.aws", + "e.g., wrkspc_01abcdefghij": "par ex., wrkspc_01abcdefghij", + "Required anthropic-workspace-id header. Find it in AWS Console → Claude Platform on AWS → Workspaces.": "En-tête anthropic-workspace-id requis. Trouvez-le dans la console AWS → Claude Platform on AWS → Espaces de travail.", "Zero retention": "Aucune rétention", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V4", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index cb5743b0fab2..d1fa2acb8ca9 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -423,6 +423,7 @@ "Auth configured": "認証設定済み", "Auth Style": "認証スタイル", "Authentication": "認証", + "Authentication Method": "認証方式", "Authenticator code": "認証コード", "Authorization Endpoint": "認可エンドポイント", "Authorization Endpoint (Optional)": "認証エンドポイント (オプション)", @@ -466,6 +467,8 @@ "AWS": "AWS", "AWS Bedrock Claude Compat": "AWS Bedrock Claude 互換テンプレート", "AWS Key Format": "AWSキーフォーマット", + "AWS Region *": "AWS リージョン *", + "AWS SigV4 (IAM)": "AWS SigV4 (IAM)", "Azure": "Azure", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "Back": "戻る", @@ -699,6 +702,7 @@ "Classic (Legacy Frontend)": "クラシック(旧フロントエンド)", "Claude": "Claude", "Claude CLI Header Passthrough": "Claude CLI ヘッダーパススルー", + "Claude Platform on AWS": "Claude Platform on AWS", "Clean": "問題なし", "Clean history logs": "履歴ログをクリーンアップ", "Clean logs": "ログをクリア", @@ -4473,6 +4477,14 @@ "Your transaction history will appear here": "取引履歴はここに表示されます", "Your Turnstile secret key": "あなたのTurnstileシークレットキー", "Your Turnstile site key": "あなたのTurnstileサイトキー", + "Workspace ID *": "ワークスペース ID *", + "Select auth method": "認証方式を選択してください", + "API Key mode: paste the key generated in AWS Console → Claude Platform on AWS → API keys": "API Key モード: AWS Console → Claude Platform on AWS → API keys で生成されたキーを貼り付けてください", + "SigV4 mode: paste credentials in format AK|SK or AK|SK|SessionToken": "SigV4 モード: 認証情報を AK|SK または AK|SK|SessionToken の形式で貼り付けてください", + "e.g., us-east-1, us-west-2, eu-west-1": "例: us-east-1, us-west-2, eu-west-1", + "Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws": "Claude Platform on AWS エンドポイントのリージョン。URL aws-external-anthropic.{region}.api.aws に使用されます", + "e.g., wrkspc_01abcdefghij": "例: wrkspc_01abcdefghij", + "Required anthropic-workspace-id header. Find it in AWS Console → Claude Platform on AWS → Workspaces.": "必須の anthropic-workspace-id ヘッダー。AWS Console → Claude Platform on AWS → Workspaces で確認できます。", "Zero retention": "データ保持なし", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V 4", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index 91b6d6da780c..bd77ec0156e7 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -423,6 +423,7 @@ "Auth configured": "Аутентификация настроена", "Auth Style": "Стиль аутентификации", "Authentication": "Аутентификация", + "Authentication Method": "Метод аутентификации", "Authenticator code": "Код аутентификатора", "Authorization Endpoint": "Конечная точка авторизации", "Authorization Endpoint (Optional)": "Конечная точка авторизации (необязательно)", @@ -466,6 +467,8 @@ "AWS": "AWS", "AWS Bedrock Claude Compat": "AWS Bedrock Claude совместимость", "AWS Key Format": "Формат ключа AWS", + "AWS Region *": "Регион AWS *", + "AWS SigV4 (IAM)": "AWS SigV4 (IAM)", "Azure": "Azure", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "Back": "Назад", @@ -699,6 +702,7 @@ "Classic (Legacy Frontend)": "Классический (Старый интерфейс)", "Claude": "Клод", "Claude CLI Header Passthrough": "Проброс заголовков Claude CLI", + "Claude Platform on AWS": "Claude Platform on AWS", "Clean": "Без конфликта", "Clean history logs": "Очистить журналы истории", "Clean logs": "Очистить логи", @@ -4473,6 +4477,14 @@ "Your transaction history will appear here": "Ваша история транзакций появится здесь", "Your Turnstile secret key": "Секретный ключ Turnstile", "Your Turnstile site key": "Ключ сайта Turnstile", + "Workspace ID *": "ID рабочего пространства *", + "Select auth method": "Выберите метод аутентификации", + "API Key mode: paste the key generated in AWS Console → Claude Platform on AWS → API keys": "Режим API Key: вставьте ключ, созданный в AWS Console → Claude Platform on AWS → API keys", + "SigV4 mode: paste credentials in format AK|SK or AK|SK|SessionToken": "Режим SigV4: вставьте учётные данные в формате AK|SK или AK|SK|SessionToken", + "e.g., us-east-1, us-west-2, eu-west-1": "например: us-east-1, us-west-2, eu-west-1", + "Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws": "Регион конечной точки Claude Platform on AWS, используется в URL aws-external-anthropic.{region}.api.aws", + "e.g., wrkspc_01abcdefghij": "например: wrkspc_01abcdefghij", + "Required anthropic-workspace-id header. Find it in AWS Console → Claude Platform on AWS → Workspaces.": "Обязательный заголовок anthropic-workspace-id. Найдите его в AWS Console → Claude Platform on AWS → Workspaces.", "Zero retention": "Без хранения данных", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V4", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index ed2764d292c6..9b57962342c0 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -423,6 +423,7 @@ "Auth configured": "Đã cấu hình xác thực", "Auth Style": "Kiểu xác thực", "Authentication": "Xác thực", + "Authentication Method": "Phương thức xác thực", "Authenticator code": "Mã xác thực", "Authorization Endpoint": "Điểm cuối ủy quyền", "Authorization Endpoint (Optional)": "Điểm cuối ủy quyền (Tùy chọn)", @@ -466,6 +467,8 @@ "AWS": "AWS", "AWS Bedrock Claude Compat": "AWS Bedrock Claude tương thích", "AWS Key Format": "Định dạng khóa AWS", + "AWS Region *": "Khu vực AWS *", + "AWS SigV4 (IAM)": "AWS SigV4 (IAM)", "Azure": "Azure", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "Back": "Quay lại", @@ -699,6 +702,7 @@ "Classic (Legacy Frontend)": "Cổ điển (Frontend cũ)", "Claude": "Claude", "Claude CLI Header Passthrough": "Chuyển tiếp header Claude CLI", + "Claude Platform on AWS": "Claude Platform on AWS", "Clean": "Không xung đột", "Clean history logs": "Xóa nhật ký lịch sử", "Clean logs": "Dọn dẹp nhật ký", @@ -4473,6 +4477,14 @@ "Your transaction history will appear here": "Lịch sử giao dịch của bạn sẽ xuất hiện ở đây", "Your Turnstile secret key": "Khóa bí mật Turnstile của bạn", "Your Turnstile site key": "Khóa site Turnstile của bạn", + "Workspace ID *": "ID không gian làm việc *", + "Select auth method": "Chọn phương thức xác thực", + "API Key mode: paste the key generated in AWS Console → Claude Platform on AWS → API keys": "Chế độ API Key: dán khóa được tạo trong AWS Console → Claude Platform on AWS → API keys", + "SigV4 mode: paste credentials in format AK|SK or AK|SK|SessionToken": "Chế độ SigV4: dán thông tin xác thực theo định dạng AK|SK hoặc AK|SK|SessionToken", + "e.g., us-east-1, us-west-2, eu-west-1": "ví dụ: us-east-1, us-west-2, eu-west-1", + "Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws": "Khu vực của endpoint Claude Platform on AWS, được sử dụng trong URL aws-external-anthropic.{region}.api.aws", + "e.g., wrkspc_01abcdefghij": "ví dụ: wrkspc_01abcdefghij", + "Required anthropic-workspace-id header. Find it in AWS Console → Claude Platform on AWS → Workspaces.": "Header anthropic-workspace-id bắt buộc. Tìm thấy trong AWS Console → Claude Platform on AWS → Workspaces.", "Zero retention": "Không lưu dữ liệu", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V4", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index fbfd6d733c83..e059251e84c9 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -423,6 +423,7 @@ "Auth configured": "认证已配置", "Auth Style": "认证方式", "Authentication": "身份验证", + "Authentication Method": "鉴权方式", "Authenticator code": "身份验证器代码", "Authorization Endpoint": "授权端点", "Authorization Endpoint (Optional)": "授权端点(可选)", @@ -466,6 +467,8 @@ "AWS": "AWS", "AWS Bedrock Claude Compat": "AWS Bedrock Claude 兼容模板", "AWS Key Format": "AWS 密钥格式", + "AWS Region *": "AWS 区域 *", + "AWS SigV4 (IAM)": "AWS SigV4(IAM)", "Azure": "Azure", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "Back": "返回", @@ -699,6 +702,7 @@ "Classic (Legacy Frontend)": "经典前端", "Claude": "Claude", "Claude CLI Header Passthrough": "Claude CLI 请求头透传", + "Claude Platform on AWS": "Claude Platform on AWS", "Clean": "无冲突", "Clean history logs": "清理历史日志", "Clean logs": "清理日志", @@ -4473,6 +4477,14 @@ "Your transaction history will appear here": "您的交易历史会显示在这里", "Your Turnstile secret key": "您的 Turnstile 密钥", "Your Turnstile site key": "您的 Turnstile 站点密钥", + "Workspace ID *": "工作区 ID *", + "Select auth method": "请选择鉴权方式", + "API Key mode: paste the key generated in AWS Console → Claude Platform on AWS → API keys": "API Key 模式:粘贴在 AWS Console → Claude Platform on AWS → API keys 生成的 key", + "SigV4 mode: paste credentials in format AK|SK or AK|SK|SessionToken": "SigV4 模式:使用 AK|SK 或 AK|SK|SessionToken 三段格式", + "e.g., us-east-1, us-west-2, eu-west-1": "如 us-east-1, us-west-2, eu-west-1", + "Region of the Claude Platform on AWS endpoint, used in URL aws-external-anthropic.{region}.api.aws": "Claude Platform on AWS 端点的区域,用于拼接 URL aws-external-anthropic.{region}.api.aws", + "e.g., wrkspc_01abcdefghij": "如 wrkspc_01abcdefghij", + "Required anthropic-workspace-id header. Find it in AWS Console → Claude Platform on AWS → Workspaces.": "必填的 anthropic-workspace-id 头,在 AWS Console → Claude Platform on AWS → Workspaces 查看。", "Zero retention": "零数据保留", "Zhipu": "智谱", "Zhipu V4": "智谱 V4",