From afa50d5c73d8397bc657ae270741aa91cd165054 Mon Sep 17 00:00:00 2001 From: Anatoly Lapkov Date: Wed, 20 May 2026 12:31:46 +0300 Subject: [PATCH 01/23] feat(channel): add OpenAIAdminKey field to ChannelOtherSettings --- dto/channel_settings.go | 1 + 1 file changed, 1 insertion(+) diff --git a/dto/channel_settings.go b/dto/channel_settings.go index b6a1ab9f713..4aa4f53e2cc 100644 --- a/dto/channel_settings.go +++ b/dto/channel_settings.go @@ -35,6 +35,7 @@ type ChannelOtherSettings struct { 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"` + OpenAIAdminKey string `json:"openai_admin_key,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"` // 上次检测时间 From 5bdcc593cd2cd47c96af7cfaf01970c719dce584 Mon Sep 17 00:00:00 2001 From: Anatoly Lapkov Date: Wed, 20 May 2026 12:33:00 +0300 Subject: [PATCH 02/23] test(channel): add scaffolding for channel_billing tests --- controller/channel_billing_test.go | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 controller/channel_billing_test.go diff --git a/controller/channel_billing_test.go b/controller/channel_billing_test.go new file mode 100644 index 00000000000..86f2b55d761 --- /dev/null +++ b/controller/channel_billing_test.go @@ -0,0 +1,42 @@ +package controller + +import ( + "testing" + "time" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/model" + + "github.com/stretchr/testify/require" +) + +// buildOpenAIChannelWithAdminKey creates an in-memory OpenAI channel pointing at the given baseURL. +// adminKey is stored inside ChannelOtherSettings.OpenAIAdminKey. If empty, the field is omitted. +func buildOpenAIChannelWithAdminKey(t *testing.T, baseURL, adminKey string) *model.Channel { + t.Helper() + settings := dto.ChannelOtherSettings{} + if adminKey != "" { + settings.OpenAIAdminKey = adminKey + } + encoded, err := common.Marshal(settings) + require.NoError(t, err) + bURL := baseURL + ch := &model.Channel{ + Type: constant.ChannelTypeOpenAI, + Key: "sk-fake-inference-key", + Status: 1, + Name: "test-openai", + BaseURL: &bURL, + OtherSettings: string(encoded), + } + require.NoError(t, model.DB.Create(ch).Error) + return ch +} + +// firstDayOfCurrentMonthUTC returns the Unix timestamp of the 1st day of the current month at 00:00 UTC. +func firstDayOfCurrentMonthUTC() int64 { + now := time.Now().UTC() + return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC).Unix() +} From f5a6d7674d3cd980da7eb007e03929439fa560f2 Mon Sep 17 00:00:00 2001 From: Anatoly Lapkov Date: Wed, 20 May 2026 12:42:09 +0300 Subject: [PATCH 03/23] feat(channel): implement OpenAI balance via /v1/organization/costs --- controller/channel-billing.go | 86 ++++++++++++++++++++++++++++++ controller/channel_billing_test.go | 46 ++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 751ee3600ac..76b56ecf784 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -6,7 +6,9 @@ import ( "fmt" "io" "net/http" + "net/url" "strconv" + "strings" "time" "github.com/QuantumNous/new-api/common" @@ -356,6 +358,90 @@ func updateChannelMoonshotBalance(channel *model.Channel) (float64, error) { return availableBalanceUsd, nil } +// Costs API response structures (https://platform.openai.com/docs/api-reference/usage/costs) + +type openAICostsResponse struct { + Object string `json:"object"` + Data []openAICostBucket `json:"data"` + HasMore bool `json:"has_more"` + NextPage string `json:"next_page"` +} + +type openAICostBucket struct { + Object string `json:"object"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + Results []openAICostResult `json:"results"` +} + +type openAICostResult struct { + Object string `json:"object"` + Amount openAICostAmount `json:"amount"` +} + +type openAICostAmount struct { + Value float64 `json:"value"` + Currency string `json:"currency"` +} + +// updateChannelOpenAIBalance fetches month-to-date OpenAI usage via the Costs API and writes +// it into channel.Balance. Requires an admin-scoped key (sk-admin-...) stored in +// channel.other_settings.openai_admin_key. The legacy /v1/dashboard/billing endpoints were +// deprecated by OpenAI; this function is their replacement. +func updateChannelOpenAIBalance(channel *model.Channel) (float64, error) { + settings := channel.GetOtherSettings() + adminKey := strings.TrimSpace(settings.OpenAIAdminKey) + if adminKey == "" { + return 0, errors.New("openai admin key is not set; configure channel.other_settings.openai_admin_key") + } + + baseURL := channel.GetBaseURL() + if baseURL == "" { + baseURL = constant.ChannelBaseURLs[channel.Type] + } + + now := time.Now().UTC() + start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC).Unix() + end := now.Unix() + + var total float64 + pageToken := "" + for { + query := url.Values{} + query.Set("start_time", strconv.FormatInt(start, 10)) + query.Set("end_time", strconv.FormatInt(end, 10)) + query.Set("limit", "31") + if pageToken != "" { + query.Set("page_token", pageToken) + } + fullURL := fmt.Sprintf("%s/v1/organization/costs?%s", baseURL, query.Encode()) + + body, err := GetResponseBody("GET", fullURL, channel, GetAuthHeader(adminKey)) + if err != nil { + return 0, fmt.Errorf("fetch openai usage: %w", err) + } + + var resp openAICostsResponse + if err := common.Unmarshal(body, &resp); err != nil { + return 0, fmt.Errorf("parse openai usage: %w", err) + } + + for _, bucket := range resp.Data { + for _, result := range bucket.Results { + total += result.Amount.Value + } + } + + if !resp.HasMore || resp.NextPage == "" { + break + } + pageToken = resp.NextPage + } + + channel.UpdateBalance(total) + return total, nil +} + func updateChannelBalance(channel *model.Channel) (float64, error) { baseURL := constant.ChannelBaseURLs[channel.Type] if channel.GetBaseURL() == "" { diff --git a/controller/channel_billing_test.go b/controller/channel_billing_test.go index 86f2b55d761..90ba3733093 100644 --- a/controller/channel_billing_test.go +++ b/controller/channel_billing_test.go @@ -1,6 +1,11 @@ package controller import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strconv" "testing" "time" @@ -16,6 +21,7 @@ import ( // adminKey is stored inside ChannelOtherSettings.OpenAIAdminKey. If empty, the field is omitted. func buildOpenAIChannelWithAdminKey(t *testing.T, baseURL, adminKey string) *model.Channel { t.Helper() + require.NoError(t, model.DB.AutoMigrate(&model.Channel{})) settings := dto.ChannelOtherSettings{} if adminKey != "" { settings.OpenAIAdminKey = adminKey @@ -40,3 +46,43 @@ func firstDayOfCurrentMonthUTC() int64 { now := time.Now().UTC() return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC).Unix() } + +func TestUpdateChannelOpenAIBalance_SingleBucket(t *testing.T) { + _ = openTokenControllerTestDB(t) + + var capturedAuth string + var capturedQuery url.Values + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedAuth = r.Header.Get("Authorization") + capturedQuery = r.URL.Query() + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{ + "object": "page", + "data": [ + { + "object": "bucket", + "start_time": 1747008000, + "end_time": 1747094400, + "results": [ + {"object": "organization.costs.result", "amount": {"value": 10.5, "currency": "usd"}} + ] + } + ], + "has_more": false, + "next_page": "" + }`) + })) + defer ts.Close() + + ch := buildOpenAIChannelWithAdminKey(t, ts.URL, "sk-admin-test") + + balance, err := updateChannelOpenAIBalance(ch) + require.NoError(t, err) + require.InDelta(t, 10.5, balance, 0.001) + require.Equal(t, "Bearer sk-admin-test", capturedAuth) + require.Equal(t, strconv.FormatInt(firstDayOfCurrentMonthUTC(), 10), capturedQuery.Get("start_time")) + + var fresh model.Channel + require.NoError(t, model.DB.First(&fresh, ch.Id).Error) + require.InDelta(t, 10.5, fresh.Balance, 0.001) +} From aced4d657d89b92ca7c94c21ad873a2d96fc14b5 Mon Sep 17 00:00:00 2001 From: Anatoly Lapkov Date: Wed, 20 May 2026 13:12:03 +0300 Subject: [PATCH 04/23] fix(channel): use 'page' param and add pagination iteration cap --- controller/channel-billing.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 76b56ecf784..754fc00d4f6 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -360,6 +360,8 @@ func updateChannelMoonshotBalance(channel *model.Channel) (float64, error) { // Costs API response structures (https://platform.openai.com/docs/api-reference/usage/costs) +const openAICostsMaxPages = 50 // safety cap for pagination; daily buckets over a month never exceed this + type openAICostsResponse struct { Object string `json:"object"` Data []openAICostBucket `json:"data"` @@ -406,13 +408,14 @@ func updateChannelOpenAIBalance(channel *model.Channel) (float64, error) { var total float64 pageToken := "" - for { + exhausted := true + for iter := 0; iter < openAICostsMaxPages; iter++ { query := url.Values{} query.Set("start_time", strconv.FormatInt(start, 10)) query.Set("end_time", strconv.FormatInt(end, 10)) query.Set("limit", "31") if pageToken != "" { - query.Set("page_token", pageToken) + query.Set("page", pageToken) } fullURL := fmt.Sprintf("%s/v1/organization/costs?%s", baseURL, query.Encode()) @@ -433,10 +436,14 @@ func updateChannelOpenAIBalance(channel *model.Channel) (float64, error) { } if !resp.HasMore || resp.NextPage == "" { + exhausted = false break } pageToken = resp.NextPage } + if exhausted { + return total, fmt.Errorf("openai costs pagination did not terminate after %d pages", openAICostsMaxPages) + } channel.UpdateBalance(total) return total, nil From b02cd1f8975087f27b4d1e2444f625336ff2a3ff Mon Sep 17 00:00:00 2001 From: Anatoly Lapkov Date: Wed, 20 May 2026 13:13:44 +0300 Subject: [PATCH 05/23] test(channel): cover paginated Costs API response --- controller/channel_billing_test.go | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/controller/channel_billing_test.go b/controller/channel_billing_test.go index 90ba3733093..262195b0e59 100644 --- a/controller/channel_billing_test.go +++ b/controller/channel_billing_test.go @@ -86,3 +86,41 @@ func TestUpdateChannelOpenAIBalance_SingleBucket(t *testing.T) { require.NoError(t, model.DB.First(&fresh, ch.Id).Error) require.InDelta(t, 10.5, fresh.Balance, 0.001) } + +func TestUpdateChannelOpenAIBalance_Pagination(t *testing.T) { + _ = openTokenControllerTestDB(t) + + callCount := 0 + var capturedTokenOnSecondCall string + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callCount++ + w.Header().Set("Content-Type", "application/json") + if callCount == 1 { + _, _ = fmt.Fprint(w, `{ + "object": "page", + "data": [{"object": "bucket","start_time": 1747008000,"end_time": 1747094400, + "results": [{"object": "organization.costs.result","amount": {"value": 5.0,"currency": "usd"}}]}], + "has_more": true, + "next_page": "page-token-2" + }`) + return + } + capturedTokenOnSecondCall = r.URL.Query().Get("page") + _, _ = fmt.Fprint(w, `{ + "object": "page", + "data": [{"object": "bucket","start_time": 1747094400,"end_time": 1747180800, + "results": [{"object": "organization.costs.result","amount": {"value": 7.25,"currency": "usd"}}]}], + "has_more": false, + "next_page": "" + }`) + })) + defer ts.Close() + + ch := buildOpenAIChannelWithAdminKey(t, ts.URL, "sk-admin-test") + + balance, err := updateChannelOpenAIBalance(ch) + require.NoError(t, err) + require.Equal(t, 2, callCount) + require.Equal(t, "page-token-2", capturedTokenOnSecondCall) + require.InDelta(t, 12.25, balance, 0.001) +} From 4278d40c9512b1b33bf3da9e7d4943ddc7fac8b4 Mon Sep 17 00:00:00 2001 From: Anatoly Lapkov Date: Wed, 20 May 2026 13:14:02 +0300 Subject: [PATCH 06/23] test(channel): empty admin key returns explicit error --- controller/channel_billing_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/controller/channel_billing_test.go b/controller/channel_billing_test.go index 262195b0e59..c06920e1d6b 100644 --- a/controller/channel_billing_test.go +++ b/controller/channel_billing_test.go @@ -124,3 +124,17 @@ func TestUpdateChannelOpenAIBalance_Pagination(t *testing.T) { require.Equal(t, "page-token-2", capturedTokenOnSecondCall) require.InDelta(t, 12.25, balance, 0.001) } + +func TestUpdateChannelOpenAIBalance_NoAdminKey(t *testing.T) { + _ = openTokenControllerTestDB(t) + ch := buildOpenAIChannelWithAdminKey(t, "http://unused", "") + + balance, err := updateChannelOpenAIBalance(ch) + require.Error(t, err) + require.Contains(t, err.Error(), "openai admin key is not set") + require.Equal(t, float64(0), balance) + + var fresh model.Channel + require.NoError(t, model.DB.First(&fresh, ch.Id).Error) + require.Equal(t, float64(0), fresh.Balance) +} From 8140ab3fd4d542ddfa5e26e1779e9d8e217bd48c Mon Sep 17 00:00:00 2001 From: Anatoly Lapkov Date: Wed, 20 May 2026 13:14:23 +0300 Subject: [PATCH 07/23] test(channel): surface upstream 403 errors verbatim --- controller/channel_billing_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/controller/channel_billing_test.go b/controller/channel_billing_test.go index c06920e1d6b..86f2b960a57 100644 --- a/controller/channel_billing_test.go +++ b/controller/channel_billing_test.go @@ -138,3 +138,24 @@ func TestUpdateChannelOpenAIBalance_NoAdminKey(t *testing.T) { require.NoError(t, model.DB.First(&fresh, ch.Id).Error) require.Equal(t, float64(0), fresh.Balance) } + +func TestUpdateChannelOpenAIBalance_Upstream403(t *testing.T) { + _ = openTokenControllerTestDB(t) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusForbidden) + _, _ = fmt.Fprint(w, `{"error":{"message":"missing scope","type":"insufficient_permissions"}}`) + })) + defer ts.Close() + + ch := buildOpenAIChannelWithAdminKey(t, ts.URL, "sk-admin-test") + + balance, err := updateChannelOpenAIBalance(ch) + require.Error(t, err) + require.Contains(t, err.Error(), "status code: 403") + require.Equal(t, float64(0), balance) + + var fresh model.Channel + require.NoError(t, model.DB.First(&fresh, ch.Id).Error) + require.Equal(t, float64(0), fresh.Balance) +} From 9e6e80293d5a0cbd59d82131c3f66366f288e347 Mon Sep 17 00:00:00 2001 From: Anatoly Lapkov Date: Wed, 20 May 2026 13:14:46 +0300 Subject: [PATCH 08/23] test(channel): wrap unmarshal failures with context --- controller/channel_billing_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/controller/channel_billing_test.go b/controller/channel_billing_test.go index 86f2b960a57..c0670efd733 100644 --- a/controller/channel_billing_test.go +++ b/controller/channel_billing_test.go @@ -159,3 +159,20 @@ func TestUpdateChannelOpenAIBalance_Upstream403(t *testing.T) { require.NoError(t, model.DB.First(&fresh, ch.Id).Error) require.Equal(t, float64(0), fresh.Balance) } + +func TestUpdateChannelOpenAIBalance_BadJSON(t *testing.T) { + _ = openTokenControllerTestDB(t) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `not-json-garbage`) + })) + defer ts.Close() + + ch := buildOpenAIChannelWithAdminKey(t, ts.URL, "sk-admin-test") + + balance, err := updateChannelOpenAIBalance(ch) + require.Error(t, err) + require.Contains(t, err.Error(), "parse openai usage") + require.Equal(t, float64(0), balance) +} From fa3f7a1061126f472ef4a6242eca83d2ea2ef25a Mon Sep 17 00:00:00 2001 From: Anatoly Lapkov Date: Wed, 20 May 2026 13:15:07 +0300 Subject: [PATCH 09/23] test(channel): assert Costs API query has correct start_time --- controller/channel_billing_test.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/controller/channel_billing_test.go b/controller/channel_billing_test.go index c0670efd733..5544e5ad276 100644 --- a/controller/channel_billing_test.go +++ b/controller/channel_billing_test.go @@ -176,3 +176,32 @@ func TestUpdateChannelOpenAIBalance_BadJSON(t *testing.T) { require.Contains(t, err.Error(), "parse openai usage") require.Equal(t, float64(0), balance) } + +func TestUpdateChannelOpenAIBalance_StartTimeIsFirstOfMonthUTC(t *testing.T) { + _ = openTokenControllerTestDB(t) + + var got url.Values + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got = r.URL.Query() + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"object":"page","data":[],"has_more":false,"next_page":""}`) + })) + defer ts.Close() + + ch := buildOpenAIChannelWithAdminKey(t, ts.URL, "sk-admin-test") + _, err := updateChannelOpenAIBalance(ch) + require.NoError(t, err) + + now := time.Now().UTC() + wantStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC).Unix() + require.Equal(t, strconv.FormatInt(wantStart, 10), got.Get("start_time")) + + endStr := got.Get("end_time") + require.NotEmpty(t, endStr) + endParsed, err := strconv.ParseInt(endStr, 10, 64) + require.NoError(t, err) + require.GreaterOrEqual(t, endParsed, wantStart) + require.LessOrEqual(t, endParsed-now.Unix(), int64(5)) + + require.Equal(t, "31", got.Get("limit")) +} From 27502b6d1cfca9399056dc74016f9ee6d94796cd Mon Sep 17 00:00:00 2001 From: Anatoly Lapkov Date: Wed, 20 May 2026 13:16:38 +0300 Subject: [PATCH 10/23] feat(channel): route OpenAI balance refresh to admin-key path --- controller/channel-billing.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 754fc00d4f6..f010ca730ac 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -456,9 +456,7 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { } switch channel.Type { case constant.ChannelTypeOpenAI: - if channel.GetBaseURL() != "" { - baseURL = channel.GetBaseURL() - } + return updateChannelOpenAIBalance(channel) case constant.ChannelTypeAzure: return 0, errors.New("尚未实现") case constant.ChannelTypeCustom: From 8eb8dad911d788b32be9b9e2024758f104444071 Mon Sep 17 00:00:00 2001 From: Anatoly Lapkov Date: Wed, 20 May 2026 13:20:21 +0300 Subject: [PATCH 11/23] feat(channel-form): wire openai_admin_key through form schema --- .../src/features/channels/lib/channel-form.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/default/src/features/channels/lib/channel-form.ts b/web/default/src/features/channels/lib/channel-form.ts index 03db2f2355f..af55f2b5ad0 100644 --- a/web/default/src/features/channels/lib/channel-form.ts +++ b/web/default/src/features/channels/lib/channel-form.ts @@ -66,6 +66,7 @@ export const channelFormSchema = z.object({ vertex_key_type: z.enum(['json', 'api_key']).optional(), // Vertex AI specific aws_key_type: z.enum(['ak_sk', 'api_key']).optional(), // AWS specific azure_responses_version: z.string().optional(), // Azure specific + openai_admin_key: z.string().optional(), // OpenAI specific: admin key for usage statistics // Field passthrough controls (stored in settings JSON) allow_service_tier: z.boolean().optional(), // OpenAI/Anthropic disable_store: z.boolean().optional(), // OpenAI only @@ -124,6 +125,7 @@ export const CHANNEL_FORM_DEFAULT_VALUES: ChannelFormValues = { vertex_key_type: 'json', aws_key_type: 'ak_sk', azure_responses_version: '', + openai_admin_key: '', // Field passthrough controls allow_service_tier: false, disable_store: false, @@ -177,6 +179,7 @@ export function transformChannelToFormDefaults( // Parse type-specific settings from settings field let vertexKeyType: 'json' | 'api_key' = 'json' let azureResponsesVersion = '' + let openaiAdminKey = '' let isEnterpriseAccount = false let awsKeyType: 'ak_sk' | 'api_key' = 'ak_sk' let allowServiceTier = false @@ -195,6 +198,7 @@ export function transformChannelToFormDefaults( const parsed = JSON.parse(channel.settings) vertexKeyType = parsed.vertex_key_type || 'json' azureResponsesVersion = parsed.azure_responses_version || '' + openaiAdminKey = parsed.openai_admin_key || '' isEnterpriseAccount = parsed.openrouter_enterprise === true awsKeyType = parsed.aws_key_type || 'ak_sk' allowServiceTier = parsed.allow_service_tier === true @@ -251,6 +255,7 @@ export function transformChannelToFormDefaults( is_enterprise_account: isEnterpriseAccount, vertex_key_type: vertexKeyType, azure_responses_version: azureResponsesVersion, + openai_admin_key: openaiAdminKey, aws_key_type: awsKeyType, allow_service_tier: allowServiceTier, disable_store: disableStore, @@ -310,6 +315,13 @@ function buildSettingsJSON(formData: ChannelFormValues): string { delete settingsObj.azure_responses_version } + // Add openai_admin_key for OpenAI channels (type 1) + if (formData.type === 1 && formData.openai_admin_key) { + settingsObj.openai_admin_key = formData.openai_admin_key + } else if ('openai_admin_key' in settingsObj) { + delete settingsObj.openai_admin_key + } + // Add enterprise account setting for OpenRouter (type 20) if (formData.type === 20) { settingsObj.openrouter_enterprise = formData.is_enterprise_account === true From 1f785258f5f32e262599841903a3417102e34a09 Mon Sep 17 00:00:00 2001 From: Anatoly Lapkov Date: Wed, 20 May 2026 13:20:38 +0300 Subject: [PATCH 12/23] feat(channel): add OpenAI Admin Key field to mutate drawer --- .../drawers/channel-mutate-drawer.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) 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 39a6e1527b5..4c7045e7525 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 @@ -2005,6 +2005,30 @@ export function ChannelMutateDrawer({ }} /> + {currentType === 1 && ( + ( + + {t('OpenAI Admin Key')} + +