From 66edb89031fffb2c9655eefea86891be42b81a90 Mon Sep 17 00:00:00 2001 From: John Miller Date: Wed, 22 Apr 2026 07:33:38 -0400 Subject: [PATCH 1/5] Refactor trace ID retrieval in invocation responses for consistency. Fixes #7864 --- .../azure.ai.agents/internal/cmd/invoke.go | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go index 082020f1e32..2cbc0b49381 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go @@ -378,9 +378,8 @@ func (a *InvokeAction) responsesLocal(ctx context.Context) error { } if resp.StatusCode >= 400 { - requestID := resp.Header.Get("apim-request-id") - if requestID != "" { - fmt.Printf("Trace ID: %s\n", requestID) + if traceID := responseTraceID(resp); traceID != "" { + fmt.Printf("Trace ID: %s\n", traceID) } return fmt.Errorf( "POST %s failed with HTTP %d: %s\n%s", @@ -614,9 +613,8 @@ func (a *InvokeAction) responsesRemote(ctx context.Context) error { } defer resp.Body.Close() - requestID := resp.Header.Get("apim-request-id") - if requestID != "" { - fmt.Printf("Trace ID: %s\n", requestID) + if traceID := responseTraceID(resp); traceID != "" { + fmt.Printf("Trace ID: %s\n", traceID) } captureResponseSession(ctx, rc.azdClient, agentKey, sid, resp, "Session: ") @@ -816,9 +814,8 @@ func handleInvocationResponse( agentName string, timeout time.Duration, ) error { - requestID := resp.Header.Get("apim-request-id") - if requestID != "" { - fmt.Printf("Trace ID: %s\n", requestID) + if traceID := responseTraceID(resp); traceID != "" { + fmt.Printf("Trace ID: %s\n", traceID) } if resp.StatusCode >= 400 { @@ -1125,6 +1122,15 @@ func createConversation(ctx context.Context, projectEndpoint, agentName, bearerT return "", fmt.Errorf("conversation response missing 'id' field") } +// responseTraceID returns the trace ID from the response, preferring x-request-id +// and falling back to apim-request-id. +func responseTraceID(resp *http.Response) string { + if id := resp.Header.Get("x-request-id"); id != "" { + return id + } + return resp.Header.Get("apim-request-id") +} + // readSSEStream reads a Server-Sent Events stream from the Foundry Responses API, // printing text deltas in real-time and returning the final response or any error. func readSSEStream(body io.Reader, agentName string) error { From 5379041a7835fa3b723be18c42620214afe1c55d Mon Sep 17 00:00:00 2001 From: John Miller Date: Wed, 22 Apr 2026 08:52:15 -0400 Subject: [PATCH 2/5] test: add unit tests for responseTraceID function to validate header preference --- .../internal/cmd/invoke_test.go | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go index 27aa72d4ab7..530958ac598 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go @@ -1025,3 +1025,49 @@ func TestCreateConversation(t *testing.T) { }) } } + +func TestResponseTraceID(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + headers map[string]string + want string + }{ + { + name: "prefers x-request-id when both present", + headers: map[string]string{"X-Request-ID": "trace-abc", "apim-request-id": "apim-xyz"}, + want: "trace-abc", + }, + { + name: "falls back to apim-request-id", + headers: map[string]string{"apim-request-id": "apim-xyz"}, + want: "apim-xyz", + }, + { + name: "returns empty when neither present", + headers: map[string]string{}, + want: "", + }, + { + name: "returns x-request-id when only it is present", + headers: map[string]string{"X-Request-ID": "trace-only"}, + want: "trace-only", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + resp := &http.Response{Header: http.Header{}} + for k, v := range tt.headers { + resp.Header.Set(k, v) + } + + if got := responseTraceID(resp); got != tt.want { + t.Errorf("responseTraceID() = %q, want %q", got, tt.want) + } + }) + } +} From fa45006cf360ca298962741cba2b90e48ed98636 Mon Sep 17 00:00:00 2001 From: John Miller Date: Wed, 22 Apr 2026 21:27:16 -0400 Subject: [PATCH 3/5] Refactor index calculations to prevent overflow in resource and model selection --- cli/azd/extensions/azure.ai.agents/internal/cmd/init.go | 3 ++- .../azure.ai.agents/internal/cmd/init_from_code.go | 5 +++-- .../extensions/azure.ai.agents/internal/cmd/init_models.go | 3 ++- .../extensions/azure.ai.agents/internal/cmd/invoke_test.go | 2 +- .../internal/pkg/agents/agent_yaml/parameters.go | 3 ++- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index d5838ff6769..1ffc4240dab 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -12,6 +12,7 @@ import ( "io" "log" "maps" + "math" "net/http" "net/url" "os" @@ -1829,7 +1830,7 @@ func (a *InitAction) populateContainerSettings( if manifestResources != nil { for i, t := range project.ResourceTiers { if t.Cpu == manifestResources.Cpu && t.Memory == manifestResources.Memory { - defaultIndex = int32(i) + defaultIndex = int32(min(i, math.MaxInt32)) //nolint:gosec // index bounded by small ResourceTiers slice break } } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go index c6d14c9b4a5..b93cd2a7cca 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "io" + "math" "net/http" "os" posixpath "path" @@ -755,13 +756,13 @@ func findDefaultModelIndex(modelNames []string) int32 { // Look for exact gpt-4o first for i, name := range modelNames { if name == "gpt-4o" { - return int32(i) + return int32(min(i, math.MaxInt32)) //nolint:gosec // index bounded by model list } } // Fall back to first gpt-4 match for i, name := range modelNames { if strings.HasPrefix(name, "gpt-4") { - return int32(i) + return int32(min(i, math.MaxInt32)) //nolint:gosec // index bounded by model list } } return 0 diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models.go index b9bedd09429..f08188c1cbd 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models.go @@ -6,6 +6,7 @@ package cmd import ( "context" "fmt" + "math" "slices" "strings" @@ -129,7 +130,7 @@ func (a *InitAction) selectFromList( Label: val, } if val == defaultStr { - defaultIndex = int32(i) + defaultIndex = int32(min(i, math.MaxInt32)) //nolint:gosec // index bounded by small options slice } } resp, err := a.azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go index 530958ac598..6895d8b95b0 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go @@ -1030,7 +1030,7 @@ func TestResponseTraceID(t *testing.T) { t.Parallel() tests := []struct { - name string + name string headers map[string]string want string }{ diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parameters.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parameters.go index 80cc7dff32b..c2578da1109 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parameters.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parameters.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "log" + "math" "strings" "github.com/azure/azure-dev/cli/azd/pkg/azdext" @@ -167,7 +168,7 @@ func promptForEnumValue( Label: val, } if val == defaultStr { - defaultIndex = int32(i) + defaultIndex = int32(min(i, math.MaxInt32)) //nolint:gosec // index bounded by small enum list } } From bd2d2834feaa879a43e74ea2f4578c8672e3d296 Mon Sep 17 00:00:00 2001 From: John Miller Date: Wed, 13 May 2026 12:19:56 -0400 Subject: [PATCH 4/5] fix: dedupe comma-folded trace ID headers in agent invoke output When intermediaries like APIM combine duplicate response headers per RFC 7230 section 3.2.2, x-request-id can arrive as a comma-joined value (e.g. `,`), causing the printed trace ID to display the same value twice. Split on comma and return the first non-empty token so a single ID is shown regardless of folding. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure.ai.agents/internal/cmd/invoke.go | 16 +++++++++++---- .../internal/cmd/invoke_test.go | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go index 2cbc0b49381..0ce052005ca 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go @@ -1123,12 +1123,20 @@ func createConversation(ctx context.Context, projectEndpoint, agentName, bearerT } // responseTraceID returns the trace ID from the response, preferring x-request-id -// and falling back to apim-request-id. +// and falling back to apim-request-id. If a header value is comma-folded (which +// can happen when an intermediary like APIM combines duplicate headers per +// RFC 7230 ยง3.2.2), the first non-empty token is returned. func responseTraceID(resp *http.Response) string { - if id := resp.Header.Get("x-request-id"); id != "" { - return id + raw := resp.Header.Get("x-request-id") + if raw == "" { + raw = resp.Header.Get("apim-request-id") } - return resp.Header.Get("apim-request-id") + for part := range strings.SplitSeq(raw, ",") { + if id := strings.TrimSpace(part); id != "" { + return id + } + } + return "" } // readSSEStream reads a Server-Sent Events stream from the Foundry Responses API, diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go index 6895d8b95b0..c7f16326ccc 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go @@ -1054,6 +1054,26 @@ func TestResponseTraceID(t *testing.T) { headers: map[string]string{"X-Request-ID": "trace-only"}, want: "trace-only", }, + { + name: "deduplicates comma-folded x-request-id", + headers: map[string]string{"X-Request-ID": "trace-abc,trace-abc"}, + want: "trace-abc", + }, + { + name: "returns first token when x-request-id is comma-list", + headers: map[string]string{"X-Request-ID": "trace-first, trace-second"}, + want: "trace-first", + }, + { + name: "skips leading empty token in comma-folded x-request-id", + headers: map[string]string{"X-Request-ID": ", trace-second"}, + want: "trace-second", + }, + { + name: "deduplicates comma-folded apim-request-id fallback", + headers: map[string]string{"apim-request-id": "apim-xyz, apim-xyz"}, + want: "apim-xyz", + }, } for _, tt := range tests { From a4a9a314984ee77310cb83f37f229e8069956c8f Mon Sep 17 00:00:00 2001 From: John Miller Date: Wed, 13 May 2026 16:21:22 -0400 Subject: [PATCH 5/5] fix: remove unnecessary math.MaxInt32 usage for index calculations --- cli/azd/extensions/azure.ai.agents/internal/cmd/init.go | 3 +-- .../azure.ai.agents/internal/cmd/init_from_code.go | 5 ++--- .../extensions/azure.ai.agents/internal/cmd/init_models.go | 3 +-- .../internal/pkg/agents/agent_yaml/parameters.go | 3 +-- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 1ffc4240dab..d5838ff6769 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -12,7 +12,6 @@ import ( "io" "log" "maps" - "math" "net/http" "net/url" "os" @@ -1830,7 +1829,7 @@ func (a *InitAction) populateContainerSettings( if manifestResources != nil { for i, t := range project.ResourceTiers { if t.Cpu == manifestResources.Cpu && t.Memory == manifestResources.Memory { - defaultIndex = int32(min(i, math.MaxInt32)) //nolint:gosec // index bounded by small ResourceTiers slice + defaultIndex = int32(i) break } } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go index b93cd2a7cca..c6d14c9b4a5 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go @@ -11,7 +11,6 @@ import ( "encoding/json" "fmt" "io" - "math" "net/http" "os" posixpath "path" @@ -756,13 +755,13 @@ func findDefaultModelIndex(modelNames []string) int32 { // Look for exact gpt-4o first for i, name := range modelNames { if name == "gpt-4o" { - return int32(min(i, math.MaxInt32)) //nolint:gosec // index bounded by model list + return int32(i) } } // Fall back to first gpt-4 match for i, name := range modelNames { if strings.HasPrefix(name, "gpt-4") { - return int32(min(i, math.MaxInt32)) //nolint:gosec // index bounded by model list + return int32(i) } } return 0 diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models.go index f08188c1cbd..b9bedd09429 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models.go @@ -6,7 +6,6 @@ package cmd import ( "context" "fmt" - "math" "slices" "strings" @@ -130,7 +129,7 @@ func (a *InitAction) selectFromList( Label: val, } if val == defaultStr { - defaultIndex = int32(min(i, math.MaxInt32)) //nolint:gosec // index bounded by small options slice + defaultIndex = int32(i) } } resp, err := a.azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parameters.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parameters.go index c2578da1109..80cc7dff32b 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parameters.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parameters.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "log" - "math" "strings" "github.com/azure/azure-dev/cli/azd/pkg/azdext" @@ -168,7 +167,7 @@ func promptForEnumValue( Label: val, } if val == defaultStr { - defaultIndex = int32(min(i, math.MaxInt32)) //nolint:gosec // index bounded by small enum list + defaultIndex = int32(i) } }