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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dto/claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ func ProcessTools(tools []any) ([]*Tool, []*ClaudeWebSearchTool) {
}

type Thinking struct {
Type string `json:"type"`
Type string `json:"type,omitempty"`
BudgetTokens *int `json:"budget_tokens,omitempty"`
}
Comment on lines 429 to 432

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Anthropic Claude API thinking type field specification documentation

💡 Result:

thinking field (request) — Claude Messages API

To enable extended thinking, include a top-level thinking object in your Messages API request with:

"thinking": { "type": "enabled", "budget_tokens": 2048 }
  • type: "enabled" (to turn on extended thinking)
  • budget_tokens: maximum token budget for thinking; must be at least 1024 and (in the normal/non-interleaved case) less than max_tokens; the thinking budget counts toward your max_tokens limit. [1][2]

To disable:

"thinking": { "type": "disabled" }
``` [2]

### `thinking` content blocks (response)

When enabled, the assistant response `content` will include one or more blocks with:

* `type`: `"thinking"`
* `thinking`: the model’s thinking text (what you can display, depending on model/version)
* `signature`: an opaque verification value; Anthropic notes full thinking may be encrypted and returned via `signature` (not meant to be parsed). [1][3]

### Streaming events for thinking

In streaming mode, thinking arrives via:

* `thinking_delta` events (incremental thinking text)
* a `signature_delta` event **just before** `content_block_stop` for that thinking block. [3]

---

**Sources:** Anthropic “Building with extended thinking” docs [1], Anthropic API token counting docs (request schema notes for `thinking`) [2], Anthropic “Streaming Messages” docs (thinking/signature deltas) [3], Anthropic API release notes (signature behavior) [4].

---



</details>

**Do not add `omitempty` to `Thinking.Type` — the `type` field is required by the Anthropic API.**

The Anthropic Claude API documentation explicitly requires the `type` field in thinking objects, with values of either `"enabled"` or `"disabled"`. Using `omitempty` allows this field to be omitted when empty, which violates the API specification and could cause requests to fail. Either ensure `Type` is always set to a valid value before serialization, or remove the `omitempty` tag.

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @dto/claude.go around lines 429 - 432, The Thinking struct currently marks
the Type field with omitempty, which allows omission but the Anthropic Claude
API requires the type field; remove the omitempty tag from Thinking.Type in
the Thinking struct definition (or ensure callers always set a valid
"enabled"/"disabled" value before JSON serialization) so the serialized payload
always includes the required type field; update any constructors or code paths
that build Thinking to set a default if necessary.


</details>

<!-- This is an auto-generated comment by CodeRabbit -->


Expand Down
14 changes: 7 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ require (
github.com/abema/go-mp4 v1.4.1
github.com/andybalholm/brotli v1.1.1
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0
github.com/aws/aws-sdk-go-v2 v1.37.2
github.com/aws/aws-sdk-go-v2/credentials v1.17.11
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.33.0
github.com/aws/smithy-go v1.22.5
github.com/aws/aws-sdk-go-v2 v1.41.2
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.0
github.com/aws/smithy-go v1.24.2
github.com/bytedance/gopkg v0.1.3
github.com/gin-contrib/cors v1.7.2
github.com/gin-contrib/gzip v0.0.6
Expand Down Expand Up @@ -62,9 +62,9 @@ require (
require (
github.com/DmitriyVTitov/size v1.5.0 // indirect
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
Expand Down
16 changes: 16 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,34 @@ github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 h1:HblK3eJHq54yET63q
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6/go.mod h1:pbiaLIeYLUbgMY1kwEAdwO6UKD5ZNwdPGQlwokS9fe8=
github.com/aws/aws-sdk-go-v2 v1.37.2 h1:xkW1iMYawzcmYFYEV0UCMxc8gSsjCGEhBXQkdQywVbo=
github.com/aws/aws-sdk-go-v2 v1.37.2/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 h1:6GMWV6CNpA/6fbFHnoAjrv4+LGfyTqZz2LtCHnspgDg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0/go.mod h1:/mXlTIVG9jbxkqDnr5UQNQxW1HRYxeGklkM9vAFeabg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHHvIq0l/pX3fwO+Tzs=
github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 h1:sPiRHLVUIIQcoVZTNwqQcdtjkqkPopyYmIX0M5ElRf4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2/go.mod h1:ik86P3sgV+Bk7c1tBFCwI3VxMoSEwl4YkRB9xn1s340=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 h1:ZdzDAg075H6stMZtbD2o+PyB933M/f20e9WmCBC17wA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2/go.mod h1:eE1IIzXG9sdZCB0pNNpMpsYTLl4YdOQD3njiVN1e/E4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.33.0 h1:JzidOz4Hcn2RbP5fvIS1iAP+DcRv5VJtgixbEYDsI5g=
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.33.0/go.mod h1:9A4/PJYlWjvjEzzoOLGQjkLt4bYK9fRWi7uz1GSsAcA=
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.0 h1:TDKR8ACRw7G+GFaQlhoy6biu+8q6ZtSddQCy9avMdMI=
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.0/go.mod h1:XlhOh5Ax/lesqN4aZCUgj9vVJed5VoXYHHFYGAlJEwU=
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
Expand Down
4 changes: 4 additions & 0 deletions relay/channel/api_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,10 @@ func processHeaderOverride(info *common.RelayInfo, c *gin.Context) (map[string]s
return headerOverride, nil
}

func ResolveHeaderOverride(info *common.RelayInfo, c *gin.Context) (map[string]string, error) {
return processHeaderOverride(info, c)
}

func applyHeaderOverrideToRequest(req *http.Request, headerOverride map[string]string) {
if req == nil {
return
Expand Down
1 change: 1 addition & 0 deletions relay/channel/aws/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type AwsClaudeRequest struct {
ToolChoice any `json:"tool_choice,omitempty"`
Thinking *dto.Thinking `json:"thinking,omitempty"`
OutputConfig json.RawMessage `json:"output_config,omitempty"`
//Metadata json.RawMessage `json:"metadata,omitempty"`
}

func formatRequest(requestBody io.Reader, requestHeader http.Header) (*AwsClaudeRequest, error) {
Expand Down
8 changes: 8 additions & 0 deletions relay/channel/aws/relay-aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/QuantumNous/new-api/common"
"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/relay/helper"
Expand Down Expand Up @@ -106,6 +107,13 @@ func doAwsClientRequest(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor,
// init empty request.header
requestHeader := http.Header{}
a.SetupRequestHeader(c, &requestHeader, info)
headerOverride, err := channel.ResolveHeaderOverride(info, c)
if err != nil {
return nil, err
}
for key, value := range headerOverride {
requestHeader.Set(key, value)
}

if isNovaModel(awsModelId) {
var novaReq *NovaRequest
Expand Down
55 changes: 55 additions & 0 deletions relay/channel/aws/relay_aws_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package aws

import (
"bytes"
"net/http"
"net/http/httptest"
"testing"

"github.com/QuantumNous/new-api/common"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)

func TestDoAwsClientRequest_AppliesRuntimeHeaderOverrideToAnthropicBeta(t *testing.T) {
t.Parallel()

gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)

info := &relaycommon.RelayInfo{
OriginModelName: "claude-3-5-sonnet-20240620",
IsStream: false,
UseRuntimeHeadersOverride: true,
RuntimeHeadersOverride: map[string]any{
"anthropic-beta": "computer-use-2025-01-24",
},
ChannelMeta: &relaycommon.ChannelMeta{
ApiKey: "access-key|secret-key|us-east-1",
UpstreamModelName: "claude-3-5-sonnet-20240620",
},
}

requestBody := bytes.NewBufferString(`{"messages":[{"role":"user","content":"hello"}],"max_tokens":128}`)
adaptor := &Adaptor{}

_, err := doAwsClientRequest(ctx, info, adaptor, requestBody)
require.NoError(t, err)

awsReq, ok := adaptor.AwsReq.(*bedrockruntime.InvokeModelInput)
require.True(t, ok)

var payload map[string]any
require.NoError(t, common.Unmarshal(awsReq.Body, &payload))

anthropicBeta, exists := payload["anthropic_beta"]
require.True(t, exists)

values, ok := anthropicBeta.([]any)
require.True(t, ok)
require.Equal(t, []any{"computer-use-2025-01-24"}, values)
}
1 change: 1 addition & 0 deletions relay/channel/vertex/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type VertexAIClaudeRequest struct {
ToolChoice any `json:"tool_choice,omitempty"`
Thinking *dto.Thinking `json:"thinking,omitempty"`
OutputConfig json.RawMessage `json:"output_config,omitempty"`
//Metadata json.RawMessage `json:"metadata,omitempty"`
}

func copyRequest(req *dto.ClaudeRequest, version string) *VertexAIClaudeRequest {
Expand Down
26 changes: 25 additions & 1 deletion relay/common/override.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,39 @@ func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, c

// 尝试断言为操作格式
if operations, ok := tryParseOperations(paramOverride); ok {
legacyOverride := buildLegacyParamOverride(paramOverride)
workingJSON := jsonData
var err error
if len(legacyOverride) > 0 {
workingJSON, err = applyOperationsLegacy(workingJSON, legacyOverride)
if err != nil {
return nil, err
}
}

// 使用新方法
result, err := applyOperations(string(jsonData), operations, conditionContext)
result, err := applyOperations(string(workingJSON), operations, conditionContext)
return []byte(result), err
}

// 直接使用旧方法
return applyOperationsLegacy(jsonData, paramOverride)
}

func buildLegacyParamOverride(paramOverride map[string]interface{}) map[string]interface{} {
if len(paramOverride) == 0 {
return nil
}
legacy := make(map[string]interface{}, len(paramOverride))
for key, value := range paramOverride {
if strings.EqualFold(strings.TrimSpace(key), "operations") {
continue
}
legacy[key] = value
}
return legacy
}

func ApplyParamOverrideWithRelayInfo(jsonData []byte, info *RelayInfo) ([]byte, error) {
paramOverride := getParamOverrideMap(info)
if len(paramOverride) == 0 {
Expand Down
80 changes: 80 additions & 0 deletions relay/common/override_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,48 @@ func TestApplyParamOverrideTrimNoop(t *testing.T) {
assertJSONEqual(t, `{"model":"gpt-4","temperature":0.7}`, string(out))
}

func TestApplyParamOverrideMixedLegacyAndOperations(t *testing.T) {
input := []byte(`{"model":"openai/gpt-4","temperature":0.7}`)
override := map[string]interface{}{
"temperature": 0.2,
"top_p": 0.95,
"operations": []interface{}{
map[string]interface{}{
"path": "model",
"mode": "trim_prefix",
"value": "openai/",
},
},
}

out, err := ApplyParamOverride(input, override, nil)
if err != nil {
t.Fatalf("ApplyParamOverride returned error: %v", err)
}
assertJSONEqual(t, `{"model":"gpt-4","temperature":0.2,"top_p":0.95}`, string(out))
}

func TestApplyParamOverrideMixedLegacyAndOperationsConflictPrefersOperations(t *testing.T) {
input := []byte(`{"model":"openai/gpt-4","temperature":0.7}`)
override := map[string]interface{}{
"model": "legacy-model",
"temperature": 0.2,
"operations": []interface{}{
map[string]interface{}{
"path": "model",
"mode": "set",
"value": "op-model",
},
},
}

out, err := ApplyParamOverride(input, override, nil)
if err != nil {
t.Fatalf("ApplyParamOverride returned error: %v", err)
}
assertJSONEqual(t, `{"model":"op-model","temperature":0.2}`, string(out))
}

func TestApplyParamOverrideTrimRequiresValue(t *testing.T) {
// trim_prefix requires value example:
// {"operations":[{"path":"model","mode":"trim_prefix"}]}
Expand Down Expand Up @@ -1429,6 +1471,44 @@ func TestApplyParamOverrideWithRelayInfoSyncRuntimeHeaders(t *testing.T) {
}
}

func TestApplyParamOverrideWithRelayInfoMixedLegacyAndOperations(t *testing.T) {
info := &RelayInfo{
RequestHeaders: map[string]string{
"Originator": "Codex CLI",
},
ChannelMeta: &ChannelMeta{
ParamOverride: map[string]interface{}{
"temperature": 0.2,
"operations": []interface{}{
map[string]interface{}{
"mode": "pass_headers",
"value": []interface{}{"Originator"},
},
},
},
HeadersOverride: map[string]interface{}{
"X-Static": "legacy-static",
},
},
}

out, err := ApplyParamOverrideWithRelayInfo([]byte(`{"model":"gpt-5","temperature":0.7}`), info)
if err != nil {
t.Fatalf("ApplyParamOverrideWithRelayInfo returned error: %v", err)
}
assertJSONEqual(t, `{"model":"gpt-5","temperature":0.2}`, string(out))

if !info.UseRuntimeHeadersOverride {
t.Fatalf("expected runtime header override to be enabled")
}
if info.RuntimeHeadersOverride["x-static"] != "legacy-static" {
t.Fatalf("expected x-static to be preserved, got: %v", info.RuntimeHeadersOverride["x-static"])
}
if info.RuntimeHeadersOverride["originator"] != "Codex CLI" {
t.Fatalf("expected originator header to be passed, got: %v", info.RuntimeHeadersOverride["originator"])
}
}

func TestApplyParamOverrideWithRelayInfoMoveAndCopyHeaders(t *testing.T) {
info := &RelayInfo{
ChannelMeta: &ChannelMeta{
Expand Down
2 changes: 1 addition & 1 deletion relay/helper/valid_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func GetAndValidOpenAIImageRequest(c *gin.Context, relayMode int) (*dto.ImageReq

func GetAndValidateClaudeRequest(c *gin.Context) (textRequest *dto.ClaudeRequest, err error) {
textRequest = &dto.ClaudeRequest{}
err = c.ShouldBindJSON(textRequest)
err = common.UnmarshalBodyReusable(c, textRequest)
if err != nil {
return nil, err
}
Expand Down
35 changes: 35 additions & 0 deletions service/channel_affinity.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,11 +436,46 @@ func mergeChannelOverride(base map[string]interface{}, tpl map[string]interface{
}
out := cloneStringAnyMap(base)
for k, v := range tpl {
if strings.EqualFold(strings.TrimSpace(k), "operations") {
baseOps, hasBaseOps := extractParamOperations(out[k])
tplOps, hasTplOps := extractParamOperations(v)
if hasTplOps {
if hasBaseOps {
out[k] = append(tplOps, baseOps...)
} else {
out[k] = tplOps
}
continue
}
}
Comment on lines +439 to +450

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize the target operations key before reading/writing.

Line 440 reads out[k] using the raw template key. If template uses " Operations " and base uses "operations", base ops won’t merge and you can end up with duplicate operation keys.

🔧 Proposed fix
 	for k, v := range tpl {
 		if strings.EqualFold(strings.TrimSpace(k), "operations") {
-			baseOps, hasBaseOps := extractParamOperations(out[k])
+			opsKey := "operations"
+			for existingKey := range out {
+				if strings.EqualFold(strings.TrimSpace(existingKey), "operations") {
+					opsKey = existingKey
+					break
+				}
+			}
+			baseOps, hasBaseOps := extractParamOperations(out[opsKey])
 			tplOps, hasTplOps := extractParamOperations(v)
 			if hasTplOps {
-				if hasBaseOps {
-					out[k] = append(tplOps, baseOps...)
-				} else {
-					out[k] = tplOps
-				}
+				mergedOps := tplOps
+				if hasBaseOps {
+					mergedOps = append(tplOps, baseOps...)
+				}
+				out[opsKey] = mergedOps
+				if opsKey != k {
+					delete(out, k)
+				}
 				continue
 			}
 		}
 		out[k] = v
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/channel_affinity.go` around lines 439 - 450, The merge logic for the
"operations" key uses the raw template key k, so differently-cased/whitespace
keys (e.g., " Operations ") won't match existing base entries; normalize the key
before reading/writing by computing normKey :=
strings.ToLower(strings.TrimSpace(k)) (or constant "operations") and use normKey
when calling extractParamOperations and when reading/writing out (e.g., check
out[normKey], assign out[normKey] = ...); if the original raw key differs from
normKey, remove or replace out[k] to avoid duplicate keys. Ensure you still call
extractParamOperations(v) for the template value v and preserve the existing
append order (tplOps before baseOps).

if _, exists := out[k]; exists {
continue
}
out[k] = v
}
return out
}

func extractParamOperations(value interface{}) ([]interface{}, bool) {
switch ops := value.(type) {
case []interface{}:
if len(ops) == 0 {
return []interface{}{}, true
}
cloned := make([]interface{}, 0, len(ops))
cloned = append(cloned, ops...)
return cloned, true
case []map[string]interface{}:
cloned := make([]interface{}, 0, len(ops))
for _, op := range ops {
cloned = append(cloned, op)
}
return cloned, true
default:
return nil, false
}
}

func appendChannelAffinityTemplateAdminInfo(c *gin.Context, meta channelAffinityMeta) {
if c == nil {
return
Expand Down
Loading