chore(useragent): propogate UA to objects helpers, normalize headers

Christian Rocha created

Change summary

object/object.go                                   |  4 +
providers/internal/httpheaders/httpheaders.go      |  6 ++
providers/internal/httpheaders/httpheaders_test.go | 38 +++++++++++----
3 files changed, 37 insertions(+), 11 deletions(-)

Detailed changes

object/object.go 🔗

@@ -120,6 +120,7 @@ func GenerateWithTool(
 		TopK:             call.TopK,
 		PresencePenalty:  call.PresencePenalty,
 		FrequencyPenalty: call.FrequencyPenalty,
+		UserAgent:        call.UserAgent,
 		ProviderOptions:  call.ProviderOptions,
 	})
 	if err != nil {
@@ -212,6 +213,7 @@ func GenerateWithText(
 		TopK:             call.TopK,
 		PresencePenalty:  call.PresencePenalty,
 		FrequencyPenalty: call.FrequencyPenalty,
+		UserAgent:        call.UserAgent,
 		ProviderOptions:  call.ProviderOptions,
 	})
 	if err != nil {
@@ -294,6 +296,7 @@ func StreamWithTool(
 		TopK:             call.TopK,
 		PresencePenalty:  call.PresencePenalty,
 		FrequencyPenalty: call.FrequencyPenalty,
+		UserAgent:        call.UserAgent,
 		ProviderOptions:  call.ProviderOptions,
 	})
 	if err != nil {
@@ -503,6 +506,7 @@ func StreamWithText(
 		TopK:             call.TopK,
 		PresencePenalty:  call.PresencePenalty,
 		FrequencyPenalty: call.FrequencyPenalty,
+		UserAgent:        call.UserAgent,
 		ProviderOptions:  call.ProviderOptions,
 	})
 	if err != nil {

providers/internal/httpheaders/httpheaders.go 🔗

@@ -34,7 +34,11 @@ func ResolveHeaders(headers map[string]string, explicitUA, defaultUA string) map
 		}
 		out["User-Agent"] = explicitUA
 	case len(uaKeys) > 0:
-		// keep the header-map value as-is
+		val := out[uaKeys[0]]
+		for _, k := range uaKeys {
+			delete(out, k)
+		}
+		out["User-Agent"] = val
 	default:
 		out["User-Agent"] = defaultUA
 	}

providers/internal/httpheaders/httpheaders_test.go 🔗

@@ -60,11 +60,13 @@ func TestResolveHeaders_Precedence(t *testing.T) {
 		assert.False(t, hasLower, "old case-insensitive key should be removed")
 	})
 
-	t.Run("case-insensitive header key preserved when no explicit UA", func(t *testing.T) {
+	t.Run("case-insensitive header key canonicalized when no explicit UA", func(t *testing.T) {
 		t.Parallel()
 		headers := map[string]string{"user-agent": "from-headers"}
 		got := ResolveHeaders(headers, "", "default-ua")
-		assert.Equal(t, "from-headers", got["user-agent"])
+		assert.Equal(t, "from-headers", got["User-Agent"])
+		_, hasLower := got["user-agent"]
+		assert.False(t, hasLower, "non-canonical key should be removed")
 	})
 }
 
@@ -95,14 +97,30 @@ func TestResolveHeaders_PreservesOtherHeaders(t *testing.T) {
 func TestResolveHeaders_DuplicateCaseInsensitiveKeys(t *testing.T) {
 	t.Parallel()
 
-	headers := map[string]string{
-		"User-Agent": "canonical",
-		"user-agent": "lowercase",
-	}
-	got := ResolveHeaders(headers, "explicit", "default")
-	assert.Equal(t, "explicit", got["User-Agent"])
-	_, hasLower := got["user-agent"]
-	assert.False(t, hasLower, "all case-insensitive UA keys must be removed")
+	t.Run("explicit UA removes all variants", func(t *testing.T) {
+		t.Parallel()
+		headers := map[string]string{
+			"User-Agent": "canonical",
+			"user-agent": "lowercase",
+		}
+		got := ResolveHeaders(headers, "explicit", "default")
+		assert.Equal(t, "explicit", got["User-Agent"])
+		_, hasLower := got["user-agent"]
+		assert.False(t, hasLower, "all case-insensitive UA keys must be removed")
+	})
+
+	t.Run("no explicit UA collapses to single canonical key", func(t *testing.T) {
+		t.Parallel()
+		headers := map[string]string{
+			"User-Agent": "canonical",
+			"user-agent": "lowercase",
+		}
+		got := ResolveHeaders(headers, "", "default")
+		_, hasLower := got["user-agent"]
+		hasCanonical := got["User-Agent"]
+		assert.False(t, hasLower, "non-canonical key should be removed")
+		assert.NotEmpty(t, hasCanonical, "canonical User-Agent key must exist")
+	})
 }
 
 func TestCallUserAgent(t *testing.T) {