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
32 changes: 23 additions & 9 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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: ")
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1125,6 +1122,23 @@ 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. 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 {
Comment thread
trangevi marked this conversation as resolved.
raw := resp.Header.Get("x-request-id")
if raw == "" {
raw = resp.Header.Get("apim-request-id")
}
for part := range strings.SplitSeq(raw, ",") {
if id := strings.TrimSpace(part); id != "" {
return id
}
}
return ""
}
Comment thread
therealjohn marked this conversation as resolved.

// 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 {
Expand Down
66 changes: 66 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1025,3 +1025,69 @@ 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",
},
{
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 {
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)
}
})
}
}
Loading