From a6fb4a0ed1dd8e7f03a0bec432e25e321d6d2289 Mon Sep 17 00:00:00 2001 From: hjxwz123 Date: Mon, 10 Aug 2026 16:25:35 +0800 Subject: [PATCH] fix(claude): forward Anthropic workspace ID --- relay/channel/aws/adaptor.go | 12 +- relay/channel/aws/relay-aws.go | 20 ++- relay/channel/aws/relay_aws_test.go | 135 ++++++++++++++++++ relay/channel/claude/adaptor.go | 20 +++ relay/channel/claude/adaptor_test.go | 84 +++++++++++ relay/channel/claude/constants.go | 3 + relaykit/dto/channel_settings.go | 1 + .../drawers/channel-mutate-drawer.tsx | 30 ++++ .../__tests__/anthropic-workspace-id.test.ts | 94 ++++++++++++ web/src/features/channels/lib/channel-form.ts | 15 ++ web/src/features/channels/types.ts | 1 + web/src/i18n/locales/en.json | 3 + web/src/i18n/locales/fr.json | 3 + web/src/i18n/locales/ja.json | 3 + web/src/i18n/locales/ru.json | 3 + web/src/i18n/locales/vi.json | 3 + web/src/i18n/locales/zh-TW.json | 3 + web/src/i18n/locales/zh.json | 3 + 18 files changed, 428 insertions(+), 8 deletions(-) create mode 100644 relay/channel/claude/adaptor_test.go create mode 100644 web/src/features/channels/lib/__tests__/anthropic-workspace-id.test.ts diff --git a/relay/channel/aws/adaptor.go b/relay/channel/aws/adaptor.go index 480aea3993f1..b2951a2e98a0 100644 --- a/relay/channel/aws/adaptor.go +++ b/relay/channel/aws/adaptor.go @@ -26,11 +26,12 @@ const ( ) type Adaptor struct { - ClientMode ClientMode - AwsClient *bedrockruntime.Client - AwsModelId string - AwsReq any - IsNova bool + ClientMode ClientMode + AwsClient *bedrockruntime.Client + AwsModelId string + AwsReq any + anthropicWorkspaceID string + IsNova bool } func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) { @@ -105,6 +106,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { claude.CommonClaudeHeadersOperation(c, req, info) + claude.ForwardAnthropicWorkspaceIDHeader(c, req, info) if a.ClientMode == ClientModeApiKey { req.Set("Authorization", "Bearer "+info.ApiKey) } diff --git a/relay/channel/aws/relay-aws.go b/relay/channel/aws/relay-aws.go index c4751b5af855..4ec44b0df0f6 100644 --- a/relay/channel/aws/relay-aws.go +++ b/relay/channel/aws/relay-aws.go @@ -27,6 +27,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" bedrockruntimeTypes "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types" "github.com/aws/smithy-go/auth/bearer" + smithyhttp "github.com/aws/smithy-go/transport/http" ) // getAwsErrorStatusCode extracts HTTP status code from AWS SDK error @@ -119,6 +120,7 @@ func doAwsClientRequest(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor, for key, value := range headerOverride { requestHeader.Set(key, value) } + a.anthropicWorkspaceID = "" if isNovaModel(awsModelId) { var novaReq *NovaRequest @@ -142,6 +144,7 @@ func doAwsClientRequest(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor, a.AwsReq = awsReq return nil, nil } else { + a.anthropicWorkspaceID = requestHeader.Get(claude.AnthropicWorkspaceIDHeader) awsClaudeReq, err := formatRequest(requestBody, requestHeader) if err != nil { return nil, types.NewError(errors.Wrap(err, "format aws request fail"), types.ErrorCodeBadRequestBody) @@ -175,6 +178,17 @@ func doAwsClientRequest(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor, } } +func (a *Adaptor) invokeOptions() []func(*bedrockruntime.Options) { + if a.anthropicWorkspaceID == "" { + return nil + } + return []func(*bedrockruntime.Options){ + bedrockruntime.WithAPIOptions( + smithyhttp.SetHeaderValue(claude.AnthropicWorkspaceIDHeader, a.anthropicWorkspaceID), + ), + } +} + // buildAwsRequestBody prepares the payload for AWS requests, applying passthrough rules when enabled. func buildAwsRequestBody(c *gin.Context, info *relaycommon.RelayInfo, awsClaudeReq any) ([]byte, error) { if model_setting.GetGlobalSettings().PassThroughRequestEnabled || info.ChannelSetting.PassThroughBodyEnabled { @@ -232,7 +246,7 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor) (*types ctx, cancel := newAwsInvokeContext(requestContext) defer cancel() - awsResp, err := a.AwsClient.InvokeModel(ctx, a.AwsReq.(*bedrockruntime.InvokeModelInput)) + awsResp, err := a.AwsClient.InvokeModel(ctx, a.AwsReq.(*bedrockruntime.InvokeModelInput), a.invokeOptions()...) if err != nil { return newAwsInvokeError(requestContext, err, "InvokeModel"), nil } @@ -262,7 +276,7 @@ func awsStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor) ( ctx, cancel := newAwsInvokeContext(requestContext) defer cancel() - awsResp, err := a.AwsClient.InvokeModelWithResponseStream(ctx, a.AwsReq.(*bedrockruntime.InvokeModelWithResponseStreamInput)) + awsResp, err := a.AwsClient.InvokeModelWithResponseStream(ctx, a.AwsReq.(*bedrockruntime.InvokeModelWithResponseStreamInput), a.invokeOptions()...) if err != nil { return newAwsInvokeError(requestContext, err, "InvokeModelWithResponseStream"), nil } @@ -320,7 +334,7 @@ func handleNovaRequest(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor) ctx, cancel := newAwsInvokeContext(requestContext) defer cancel() - awsResp, err := a.AwsClient.InvokeModel(ctx, a.AwsReq.(*bedrockruntime.InvokeModelInput)) + awsResp, err := a.AwsClient.InvokeModel(ctx, a.AwsReq.(*bedrockruntime.InvokeModelInput), a.invokeOptions()...) if err != nil { return newAwsInvokeError(requestContext, err, "InvokeModel"), nil } diff --git a/relay/channel/aws/relay_aws_test.go b/relay/channel/aws/relay_aws_test.go index 22d8373873ed..18695904dae1 100644 --- a/relay/channel/aws/relay_aws_test.go +++ b/relay/channel/aws/relay_aws_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/relay/channel/claude" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relaykit/dto" relaytypes "github.com/QuantumNous/new-api/relaykit/types" @@ -141,6 +142,34 @@ func newAwsStreamResponse(request *http.Request, body io.ReadCloser) *http.Respo } } +func TestAPIKeySetupRequestHeaderForwardsWorkspaceID(t *testing.T) { + gin.SetMode(gin.TestMode) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + ctx.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil) + ctx.Request.Header.Set(claude.AnthropicWorkspaceIDHeader, "wrkspc_client") + + info := &relaycommon.RelayInfo{ + OriginModelName: "claude-3-5-sonnet-20240620", + ChannelMeta: &relaycommon.ChannelMeta{ + ApiKey: "bedrock-api-key|us-east-1", + UpstreamModelName: "claude-3-5-sonnet-20240620", + ChannelOtherSettings: dto.ChannelOtherSettings{ + AwsKeyType: dto.AwsKeyTypeApiKey, + AnthropicWorkspaceID: " proj_configured ", + }, + }, + } + + adaptor := &Adaptor{} + _, err := adaptor.GetRequestURL(info) + require.NoError(t, err) + require.Equal(t, ClientModeApiKey, adaptor.ClientMode) + + header := http.Header{} + require.NoError(t, adaptor.SetupRequestHeader(ctx, &header, info)) + require.Equal(t, "proj_configured", header.Get(claude.AnthropicWorkspaceIDHeader)) +} + func TestDoAwsClientRequest_AppliesRuntimeHeaderOverrideToAnthropicBeta(t *testing.T) { t.Parallel() @@ -182,6 +211,112 @@ func TestDoAwsClientRequest_AppliesRuntimeHeaderOverrideToAnthropicBeta(t *testi require.Equal(t, []any{"computer-use-2025-01-24"}, values) } +func TestDoAwsClientRequest_AppliesWorkspaceIDHeaderOverride(t *testing.T) { + gin.SetMode(gin.TestMode) + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + ctx.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil) + ctx.Request.Header.Set(claude.AnthropicWorkspaceIDHeader, "wrkspc_client") + + info := &relaycommon.RelayInfo{ + OriginModelName: "claude-3-5-sonnet-20240620", + UseRuntimeHeadersOverride: true, + RuntimeHeadersOverride: map[string]any{ + claude.AnthropicWorkspaceIDHeader: "wrkspc_admin", + }, + ChannelMeta: &relaycommon.ChannelMeta{ + ApiKey: "access-key|secret-key|us-east-1", + UpstreamModelName: "claude-3-5-sonnet-20240620", + ChannelOtherSettings: dto.ChannelOtherSettings{ + AnthropicWorkspaceID: "proj_configured", + }, + }, + } + + requestBody := bytes.NewBufferString(`{"messages":[{"role":"user","content":"hello"}],"max_tokens":128}`) + adaptor := &Adaptor{} + + _, err := doAwsClientRequest(ctx, info, adaptor, requestBody) + require.NoError(t, err) + require.Equal(t, "wrkspc_admin", adaptor.anthropicWorkspaceID) +} + +func TestDoAwsClientRequest_UsesConfiguredWorkspaceID(t *testing.T) { + gin.SetMode(gin.TestMode) + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + ctx.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil) + ctx.Request.Header.Set(claude.AnthropicWorkspaceIDHeader, "wrkspc_client") + + info := &relaycommon.RelayInfo{ + OriginModelName: "claude-3-5-sonnet-20240620", + ChannelMeta: &relaycommon.ChannelMeta{ + ApiKey: "access-key|secret-key|us-east-1", + UpstreamModelName: "claude-3-5-sonnet-20240620", + ChannelOtherSettings: dto.ChannelOtherSettings{ + AnthropicWorkspaceID: " proj_configured ", + }, + }, + } + + requestBody := bytes.NewBufferString(`{"messages":[{"role":"user","content":"hello"}],"max_tokens":128}`) + adaptor := &Adaptor{} + + _, err := doAwsClientRequest(ctx, info, adaptor, requestBody) + require.NoError(t, err) + require.Equal(t, "proj_configured", adaptor.anthropicWorkspaceID) +} + +func TestAwsInvokeOptionsSendSignedWorkspaceID(t *testing.T) { + const workspaceID = "proj_abc123" + + tests := []struct { + name string + invoke func(*bedrockruntime.Client, ...func(*bedrockruntime.Options)) error + }{ + { + name: "invoke model", + invoke: func(client *bedrockruntime.Client, options ...func(*bedrockruntime.Options)) error { + _, err := client.InvokeModel(context.Background(), newAwsInvokeModelInput(), options...) + return err + }, + }, + { + name: "invoke model with response stream", + invoke: func(client *bedrockruntime.Client, options ...func(*bedrockruntime.Options)) error { + response, err := client.InvokeModelWithResponseStream(context.Background(), newAwsStreamInput(), options...) + if err != nil { + return err + } + return response.GetStream().Close() + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + client := newAwsTestClient(awsHTTPClientFunc(func(request *http.Request) (*http.Response, error) { + require.Equal(t, workspaceID, request.Header.Get(claude.AnthropicWorkspaceIDHeader)) + require.Contains(t, request.Header.Get("Authorization"), claude.AnthropicWorkspaceIDHeader) + + if request.Header.Get("Accept") == "application/vnd.amazon.eventstream" { + return newAwsStreamResponse(request, io.NopCloser(bytes.NewReader(nil))), nil + } + return &http.Response{ + StatusCode: http.StatusOK, + Status: "200 OK", + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewBufferString(`{}`)), + Request: request, + }, nil + })) + adaptor := &Adaptor{anthropicWorkspaceID: workspaceID} + + require.NoError(t, test.invoke(client, adaptor.invokeOptions()...)) + }) + } +} + func TestNewAwsInvokeContextInheritsParent(t *testing.T) { originalRelayTimeout := common.RelayTimeout t.Cleanup(func() { diff --git a/relay/channel/claude/adaptor.go b/relay/channel/claude/adaptor.go index bbd711ff2c7d..f4774559f3a4 100644 --- a/relay/channel/claude/adaptor.go +++ b/relay/channel/claude/adaptor.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "net/url" + "strings" "github.com/QuantumNous/new-api/relay/channel" relaycommon "github.com/QuantumNous/new-api/relay/common" @@ -80,6 +81,24 @@ func CommonClaudeHeadersOperation(c *gin.Context, req *http.Header, info *relayc model_setting.GetClaudeSettings().WriteHeaders(info.OriginModelName, req) } +// ForwardAnthropicWorkspaceIDHeader applies the configured workspace ID, falling +// back to the incoming request header. Generic header overrides are applied later. +func ForwardAnthropicWorkspaceIDHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) { + workspaceID := "" + if c != nil && c.Request != nil { + workspaceID = strings.TrimSpace(c.Request.Header.Get(AnthropicWorkspaceIDHeader)) + } + if info != nil { + configuredWorkspaceID := strings.TrimSpace(info.ChannelOtherSettings.AnthropicWorkspaceID) + if configuredWorkspaceID != "" { + workspaceID = configuredWorkspaceID + } + } + if workspaceID != "" { + req.Set(AnthropicWorkspaceIDHeader, workspaceID) + } +} + func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { channel.SetupApiRequestHeader(info, c, req) req.Set("x-api-key", info.ApiKey) @@ -89,6 +108,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel } req.Set("anthropic-version", anthropicVersion) CommonClaudeHeadersOperation(c, req, info) + ForwardAnthropicWorkspaceIDHeader(c, req, info) return nil } diff --git a/relay/channel/claude/adaptor_test.go b/relay/channel/claude/adaptor_test.go new file mode 100644 index 000000000000..811777888a34 --- /dev/null +++ b/relay/channel/claude/adaptor_test.go @@ -0,0 +1,84 @@ +package claude + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "testing" + + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +func TestAdaptorForwardsAnthropicWorkspaceID(t *testing.T) { + gin.SetMode(gin.TestMode) + + tests := []struct { + name string + configuredWorkspaceID string + headerOverride map[string]any + expected string + }{ + { + name: "incoming workspace", + expected: "wrkspc_client", + }, + { + name: "configured workspace wins", + configuredWorkspaceID: " proj_configured ", + expected: "proj_configured", + }, + { + name: "channel override wins", + configuredWorkspaceID: "proj_configured", + headerOverride: map[string]any{ + AnthropicWorkspaceIDHeader: "wrkspc_admin", + }, + expected: "wrkspc_admin", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + workspaceIDs := make(chan string, 1) + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + workspaceIDs <- request.Header.Get(AnthropicWorkspaceIDHeader) + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + _, _ = writer.Write([]byte(`{}`)) + })) + t.Cleanup(server.Close) + + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + ctx.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil) + ctx.Request.Header.Set("Content-Type", "application/json") + ctx.Request.Header.Set(AnthropicWorkspaceIDHeader, "wrkspc_client") + + info := &relaycommon.RelayInfo{ + OriginModelName: "claude-sonnet-4-6", + ChannelMeta: &relaycommon.ChannelMeta{ + ApiKey: "upstream-key", + ChannelBaseUrl: server.URL, + HeadersOverride: test.headerOverride, + UpstreamModelName: "claude-sonnet-4-6", + ChannelOtherSettings: dto.ChannelOtherSettings{ + AnthropicWorkspaceID: test.configuredWorkspaceID, + }, + }, + } + + result, err := (&Adaptor{}).DoRequest(ctx, info, bytes.NewBufferString(`{"messages":[]}`)) + require.NoError(t, err) + response, ok := result.(*http.Response) + require.True(t, ok) + t.Cleanup(func() { + _, _ = io.Copy(io.Discard, response.Body) + _ = response.Body.Close() + }) + require.Equal(t, test.expected, <-workspaceIDs) + }) + } +} diff --git a/relay/channel/claude/constants.go b/relay/channel/claude/constants.go index 0e7ba8652a38..12d2e94a93ea 100644 --- a/relay/channel/claude/constants.go +++ b/relay/channel/claude/constants.go @@ -1,5 +1,8 @@ package claude +// AnthropicWorkspaceIDHeader selects an Anthropic workspace or Bedrock Mantle project. +const AnthropicWorkspaceIDHeader = "anthropic-workspace-id" + var ModelList = []string{ "claude-3-sonnet-20240229", "claude-3-opus-20240229", diff --git a/relaykit/dto/channel_settings.go b/relaykit/dto/channel_settings.go index d3ede20d69c5..773f48712699 100644 --- a/relaykit/dto/channel_settings.go +++ b/relaykit/dto/channel_settings.go @@ -69,6 +69,7 @@ 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"` + AnthropicWorkspaceID string `json:"anthropic_workspace_id,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,默认过滤以满足数据驻留合规 diff --git a/web/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/src/features/channels/components/drawers/channel-mutate-drawer.tsx index 3380d9e52c24..5e03e034dc61 100644 --- a/web/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -280,6 +280,7 @@ const SENSITIVE_FORM_FIELDS = [ 'is_enterprise_account', 'vertex_key_type', 'aws_key_type', + 'anthropic_workspace_id', 'azure_responses_version', 'force_format', 'thinking_to_content', @@ -2351,6 +2352,35 @@ export function ChannelMutateDrawer({ /> )} + {(currentType === 14 || currentType === 33) && ( + ( + + + {t('Anthropic Workspace ID')} + + + + + + {t( + 'Sent as anthropic-workspace-id to select the configured Anthropic workspace or Bedrock Mantle project.' + )} + + + + )} + /> + )} + {/* AI Proxy Library (type 21) */} {currentType === 21 && ( . + +For commercial licensing, please contact support@quantumnous.com +*/ +import assert from 'node:assert/strict' +import { describe, test } from 'node:test' + +import { channelSchema } from '../../types' +import { + CHANNEL_FORM_DEFAULT_VALUES, + transformChannelToFormDefaults, + transformFormDataToCreatePayload, +} from '../channel-form' + +function formValues(type: number, workspaceID: string) { + return { + ...CHANNEL_FORM_DEFAULT_VALUES, + name: 'Claude upstream', + type, + key: 'test-key', + models: 'claude-sonnet-4-6', + anthropic_workspace_id: workspaceID, + } +} + +describe('Anthropic workspace ID channel setting', () => { + test('trims and serializes the administrator value for Anthropic and AWS channels', () => { + for (const [type, workspaceID] of [ + [14, ' wrkspc_admin '], + [33, ' proj_mantle '], + ] as const) { + const payload = transformFormDataToCreatePayload( + formValues(type, workspaceID) + ) + const settings = JSON.parse(String(payload.channel.settings)) + + assert.equal( + settings.anthropic_workspace_id, + workspaceID.trim(), + `channel type ${type}` + ) + } + }) + + test('loads a saved workspace ID when editing a channel', () => { + const channel = channelSchema.parse({ + id: 1, + type: 14, + key: '', + status: 1, + name: 'Claude upstream', + created_time: 0, + test_time: 0, + response_time: 0, + balance_updated_time: 0, + settings: JSON.stringify({ + anthropic_workspace_id: ' wrkspc_admin ', + }), + }) + + assert.equal( + transformChannelToFormDefaults(channel).anthropic_workspace_id, + 'wrkspc_admin' + ) + }) + + test('removes a stale workspace ID when the channel type does not support it', () => { + const payload = transformFormDataToCreatePayload({ + ...formValues(1, ''), + settings: JSON.stringify({ + anthropic_workspace_id: 'wrkspc_stale', + unrelated: true, + }), + }) + const settings = JSON.parse(String(payload.channel.settings)) + + assert.equal('anthropic_workspace_id' in settings, false) + assert.equal(settings.unrelated, true) + }) +}) diff --git a/web/src/features/channels/lib/channel-form.ts b/web/src/features/channels/lib/channel-form.ts index 22f07931e4e2..b8a7050c1a2a 100644 --- a/web/src/features/channels/lib/channel-form.ts +++ b/web/src/features/channels/lib/channel-form.ts @@ -265,6 +265,7 @@ export const channelFormSchema = z 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 + anthropic_workspace_id: z.string().optional(), // Anthropic/AWS workspace or project azure_responses_version: z.string().optional(), // Azure specific // Field passthrough controls (stored in settings JSON) allow_service_tier: z.boolean().optional(), // OpenAI/Anthropic @@ -437,6 +438,7 @@ export const CHANNEL_FORM_DEFAULT_VALUES: ChannelFormValues = { is_enterprise_account: false, vertex_key_type: 'json', aws_key_type: 'ak_sk', + anthropic_workspace_id: '', azure_responses_version: '', // Field passthrough controls allow_service_tier: false, @@ -504,6 +506,7 @@ export function transformChannelToFormDefaults( let azureResponsesVersion = '' let isEnterpriseAccount = false let awsKeyType: 'ak_sk' | 'api_key' = 'ak_sk' + let anthropicWorkspaceID = '' let allowServiceTier = false let disableStore = false let allowSafetyIdentifier = false @@ -524,6 +527,10 @@ export function transformChannelToFormDefaults( azureResponsesVersion = parsed.azure_responses_version || '' isEnterpriseAccount = parsed.openrouter_enterprise === true awsKeyType = parsed.aws_key_type || 'ak_sk' + anthropicWorkspaceID = + typeof parsed.anthropic_workspace_id === 'string' + ? parsed.anthropic_workspace_id.trim() + : '' allowServiceTier = parsed.allow_service_tier === true disableStore = parsed.disable_store === true allowSafetyIdentifier = parsed.allow_safety_identifier === true @@ -583,6 +590,7 @@ export function transformChannelToFormDefaults( vertex_key_type: vertexKeyType, azure_responses_version: azureResponsesVersion, aws_key_type: awsKeyType, + anthropic_workspace_id: anthropicWorkspaceID, allow_service_tier: allowServiceTier, disable_store: disableStore, allow_include_obfuscation: allowIncludeObfuscation, @@ -671,6 +679,13 @@ function buildSettingsJSON(formData: ChannelFormValues): string { delete settingsObj.aws_key_type } + const anthropicWorkspaceID = formData.anthropic_workspace_id?.trim() + if ((formData.type === 14 || formData.type === 33) && anthropicWorkspaceID) { + settingsObj.anthropic_workspace_id = anthropicWorkspaceID + } else if ('anthropic_workspace_id' in settingsObj) { + delete settingsObj.anthropic_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/src/features/channels/types.ts b/web/src/features/channels/types.ts index f7747fa21210..0b41002e12a7 100644 --- a/web/src/features/channels/types.ts +++ b/web/src/features/channels/types.ts @@ -94,6 +94,7 @@ export interface ChannelOtherSettings { azure_responses_version?: string vertex_key_type?: 'json' | 'api_key' openrouter_enterprise?: boolean + anthropic_workspace_id?: string aws_key_type?: 'ak_sk' | 'api_key' allow_service_tier?: boolean disable_store?: boolean diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index cbac6b6119d7..3a5fc0660393 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -372,6 +372,7 @@ "Answers for common access and billing questions": "Answers for common access and billing questions", "Anthropic": "Anthropic", "Anthropic Messages to OpenAI Chat": "Anthropic Messages to OpenAI Chat", + "Anthropic Workspace ID": "Anthropic Workspace ID", "Any Match (OR)": "Any Match (OR)", "API": "API", "API Access": "API Access", @@ -1517,6 +1518,7 @@ "e.g., SiAlipay": "e.g., SiAlipay", "e.g., us-central1 or JSON format for model-specific regions": "e.g., us-central1 or JSON format for model-specific regions", "e.g., v2.1": "e.g., v2.1", + "e.g., wrkspc_... or proj_...": "e.g., wrkspc_... or proj_...", "Each backup code can only be used once.": "Each backup code can only be used once.", "Each item must be an object with a single key-value pair.": "Each item must be an object with a single key-value pair.", "Each item must have exactly one key-value pair.": "Each item must have exactly one key-value pair.", @@ -4158,6 +4160,7 @@ "Sending...": "Sending...", "Sensitive channel settings are read-only for your account.": "Sensitive channel settings are read-only for your account.", "Sensitive Words": "Sensitive Words", + "Sent as anthropic-workspace-id to select the configured Anthropic workspace or Bedrock Mantle project.": "Sent as anthropic-workspace-id to select the configured Anthropic workspace or Bedrock Mantle project.", "Sent the API key to FluentRead.": "Sent the API key to FluentRead.", "Separate image/audio prices are enabled.": "Separate image/audio prices are enabled.", "Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.": "Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index fefe91d5cee3..5763e44d8f90 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -372,6 +372,7 @@ "Answers for common access and billing questions": "Réponses aux questions courantes sur l'accès et la facturation", "Anthropic": "Anthropic", "Anthropic Messages to OpenAI Chat": "Anthropic Messages vers OpenAI Chat", + "Anthropic Workspace ID": "ID d’espace de travail Anthropic", "Any Match (OR)": "N'importe laquelle (OR)", "API": "API", "API Access": "Accès API", @@ -1517,6 +1518,7 @@ "e.g., SiAlipay": "par ex., SiAlipay", "e.g., us-central1 or JSON format for model-specific regions": "par ex., us-central1 ou format JSON pour les régions spécifiques au modèle", "e.g., v2.1": "par ex., v2.1", + "e.g., wrkspc_... or proj_...": "par ex., wrkspc_... ou proj_...", "Each backup code can only be used once.": "Chaque code de sauvegarde ne peut être utilisé qu'une seule fois.", "Each item must be an object with a single key-value pair.": "Chaque élément doit être un objet avec une seule paire clé-valeur.", "Each item must have exactly one key-value pair.": "Chaque élément doit avoir exactement une paire clé-valeur.", @@ -4158,6 +4160,7 @@ "Sending...": "Envoi en cours...", "Sensitive channel settings are read-only for your account.": "Les paramètres sensibles des canaux sont en lecture seule pour votre compte.", "Sensitive Words": "Mots sensibles", + "Sent as anthropic-workspace-id to select the configured Anthropic workspace or Bedrock Mantle project.": "Envoyé dans l’en-tête anthropic-workspace-id pour sélectionner l’espace de travail Anthropic ou le projet Bedrock Mantle configuré.", "Sent the API key to FluentRead.": "Clé API envoyée à FluentRead.", "Separate image/audio prices are enabled.": "Les prix séparés pour l’image et l’audio sont activés.", "Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.": "Séparez plusieurs règles par des virgules anglaises. Pour les regex nécessitant des virgules, passez en JSON Text.", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index d1b56e3e77bb..41ee4bcd0a4f 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -372,6 +372,7 @@ "Answers for common access and billing questions": "アクセスと請求に関するよくある質問への回答", "Anthropic": "Anthropic", "Anthropic Messages to OpenAI Chat": "Anthropic Messages から OpenAI Chat", + "Anthropic Workspace ID": "Anthropic ワークスペース ID", "Any Match (OR)": "いずれか一致(OR)", "API": "API", "API Access": "API アクセス", @@ -1517,6 +1518,7 @@ "e.g., SiAlipay": "例: SiAlipay", "e.g., us-central1 or JSON format for model-specific regions": "例: us-central1 またはモデル固有のリージョンを示す JSON 形式", "e.g., v2.1": "例: v2.1", + "e.g., wrkspc_... or proj_...": "例: wrkspc_... または proj_...", "Each backup code can only be used once.": "各バックアップコードは1回しか使用できません。", "Each item must be an object with a single key-value pair.": "各項目は単一のキーと値のペアを持つオブジェクトでなければなりません。", "Each item must have exactly one key-value pair.": "各項目には正確に 1 つのキーと値のペアが必要です。", @@ -4158,6 +4160,7 @@ "Sending...": "送信中...", "Sensitive channel settings are read-only for your account.": "あなたのアカウントでは機密チャネル設定は読み取り専用です。", "Sensitive Words": "機密語", + "Sent as anthropic-workspace-id to select the configured Anthropic workspace or Bedrock Mantle project.": "anthropic-workspace-id ヘッダーとして送信され、設定済みの Anthropic ワークスペースまたは Bedrock Mantle プロジェクトを選択します。", "Sent the API key to FluentRead.": "API キーを FluentRead に送信しました。", "Separate image/audio prices are enabled.": "画像/音声の個別料金が有効です。", "Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.": "複数のルールは半角カンマで区切ります。カンマが必要な正規表現は JSON Text に切り替えてください。", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 895627bb1d29..126531581ee4 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -372,6 +372,7 @@ "Answers for common access and billing questions": "Ответы на частые вопросы о доступе и оплате", "Anthropic": "Anthropic", "Anthropic Messages to OpenAI Chat": "Anthropic Messages в OpenAI Chat", + "Anthropic Workspace ID": "ID рабочего пространства Anthropic", "Any Match (OR)": "Любое совпадение (OR)", "API": "API", "API Access": "Доступ к API", @@ -1517,6 +1518,7 @@ "e.g., SiAlipay": "например, SiAlipay", "e.g., us-central1 or JSON format for model-specific regions": "например, us-central1 или формат JSON для регионов, специфичных для модели", "e.g., v2.1": "например, v2.1", + "e.g., wrkspc_... or proj_...": "например, wrkspc_... или proj_...", "Each backup code can only be used once.": "Каждый код восстановления можно использовать только один раз.", "Each item must be an object with a single key-value pair.": "Каждый элемент должен быть объектом с одной парой ключ-значение.", "Each item must have exactly one key-value pair.": "Каждый элемент должен иметь ровно одну пару ключ-значение.", @@ -4158,6 +4160,7 @@ "Sending...": "Отправка...", "Sensitive channel settings are read-only for your account.": "Чувствительные настройки каналов доступны вашей учетной записи только для чтения.", "Sensitive Words": "Чувствительные слова", + "Sent as anthropic-workspace-id to select the configured Anthropic workspace or Bedrock Mantle project.": "Отправляется в заголовке anthropic-workspace-id для выбора настроенного рабочего пространства Anthropic или проекта Bedrock Mantle.", "Sent the API key to FluentRead.": "API-ключ отправлен в FluentRead.", "Separate image/audio prices are enabled.": "Отдельные цены для изображений и аудио включены.", "Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.": "Разделяйте несколько правил английскими запятыми. Если regex нужны запятые, переключитесь на JSON Text.", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 73b1b6c7c59a..99063790ce0b 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -372,6 +372,7 @@ "Answers for common access and billing questions": "Câu trả lời cho các câu hỏi thường gặp về truy cập và thanh toán", "Anthropic": "Anthropic", "Anthropic Messages to OpenAI Chat": "Anthropic Messages sang OpenAI Chat", + "Anthropic Workspace ID": "ID không gian làm việc Anthropic", "Any Match (OR)": "Bất kỳ khớp (OR)", "API": "API", "API Access": "Truy cập API", @@ -1517,6 +1518,7 @@ "e.g., SiAlipay": "ví dụ: SiAlipay", "e.g., us-central1 or JSON format for model-specific regions": "chẳng hạn như us-central1 hoặc định dạng JSON cho các khu vực dành riêng cho mô hình", "e.g., v2.1": "e.g., v2.1", + "e.g., wrkspc_... or proj_...": "ví dụ: wrkspc_... hoặc proj_...", "Each backup code can only be used once.": "Mỗi mã dự phòng chỉ có thể được sử dụng một lần.", "Each item must be an object with a single key-value pair.": "Mỗi mục phải là đối tượng với một cặp khóa-giá trị duy nhất.", "Each item must have exactly one key-value pair.": "Mỗi mục phải có chính xác một cặp khóa-giá trị.", @@ -4158,6 +4160,7 @@ "Sending...": "Đang gửi...", "Sensitive channel settings are read-only for your account.": "Các cài đặt kênh nhạy cảm chỉ đọc đối với tài khoản của bạn.", "Sensitive Words": "Từ ngữ nhạy cảm", + "Sent as anthropic-workspace-id to select the configured Anthropic workspace or Bedrock Mantle project.": "Được gửi dưới dạng tiêu đề anthropic-workspace-id để chọn không gian làm việc Anthropic hoặc dự án Bedrock Mantle đã cấu hình.", "Sent the API key to FluentRead.": "Đã gửi khóa API đến FluentRead.", "Separate image/audio prices are enabled.": "Giá riêng cho hình ảnh/âm thanh đã được bật.", "Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.": "Tách nhiều quy tắc bằng dấu phẩy tiếng Anh. Với regex cần dấu phẩy, hãy chuyển sang JSON Text.", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index a0fb9d1f3a31..d7e6a4fed365 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -372,6 +372,7 @@ "Answers for common access and billing questions": "存取與收費常見問題解答", "Anthropic": "Anthropic", "Anthropic Messages to OpenAI Chat": "Anthropic Messages 到 OpenAI Chat", + "Anthropic Workspace ID": "Anthropic 工作區 ID", "Any Match (OR)": "任一滿足(OR)", "API": "API", "API Access": "API 存取", @@ -1517,6 +1518,7 @@ "e.g., SiAlipay": "例如,SiAlipay", "e.g., us-central1 or JSON format for model-specific regions": "例如,us-central1 或模型特定區域的 JSON 格式", "e.g., v2.1": "例如,v2.1", + "e.g., wrkspc_... or proj_...": "例如,wrkspc_... 或 proj_...", "Each backup code can only be used once.": "每個備用代碼只能使用一次。", "Each item must be an object with a single key-value pair.": "每個條目必須是包含單個鍵值對的物件。", "Each item must have exactly one key-value pair.": "每個條目必須恰好包含一個鍵值對。", @@ -4158,6 +4160,7 @@ "Sending...": "發送中...", "Sensitive channel settings are read-only for your account.": "你的賬號只能查看敏感渠道設定。", "Sensitive Words": "敏感詞", + "Sent as anthropic-workspace-id to select the configured Anthropic workspace or Bedrock Mantle project.": "將以 anthropic-workspace-id 請求標頭傳送,用於選擇已設定的 Anthropic 工作區或 Bedrock Mantle 專案。", "Sent the API key to FluentRead.": "API 金鑰已發送至 FluentRead。", "Separate image/audio prices are enabled.": "已啟用圖像/音頻單獨定價。", "Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.": "多個規則用英文逗號分隔。正則裡需要逗號時,請切換到 JSON 文字。", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 9366feb653d3..c0d7246e02e1 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -372,6 +372,7 @@ "Answers for common access and billing questions": "访问与计费常见问题解答", "Anthropic": "Anthropic", "Anthropic Messages to OpenAI Chat": "Anthropic Messages 到 OpenAI Chat", + "Anthropic Workspace ID": "Anthropic 工作区 ID", "Any Match (OR)": "任一满足(OR)", "API": "API", "API Access": "API 访问", @@ -1517,6 +1518,7 @@ "e.g., SiAlipay": "例如,SiAlipay", "e.g., us-central1 or JSON format for model-specific regions": "例如,us-central1 或模型特定区域的 JSON 格式", "e.g., v2.1": "例如,v2.1", + "e.g., wrkspc_... or proj_...": "例如,wrkspc_... 或 proj_...", "Each backup code can only be used once.": "每个备份代码只能使用一次。", "Each item must be an object with a single key-value pair.": "每个条目必须是包含单个键值对的对象。", "Each item must have exactly one key-value pair.": "每个条目必须恰好包含一个键值对。", @@ -4158,6 +4160,7 @@ "Sending...": "发送中...", "Sensitive channel settings are read-only for your account.": "你的账号只能查看敏感渠道设置。", "Sensitive Words": "敏感词", + "Sent as anthropic-workspace-id to select the configured Anthropic workspace or Bedrock Mantle project.": "将以 anthropic-workspace-id 请求头发送,用于选择已配置的 Anthropic 工作区或 Bedrock Mantle 项目。", "Sent the API key to FluentRead.": "API 密钥已发送至 FluentRead。", "Separate image/audio prices are enabled.": "已启用图像/音频单独定价。", "Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.": "多个规则用英文逗号分隔。正则里需要逗号时,请切换到 JSON 文本。",