Skip to content

Part.ThoughtSignature bypass string is base64-encoded by standard JSON marshaling, making it non-functional #711

@peteski22

Description

@peteski22

Description

Part.ThoughtSignature is typed as []byte in the Go SDK. When set to a documented bypass value like "skip_thought_signature_validator", Go's encoding/json automatically base64-encodes the value during marshaling. The API receives "c2tpcF90aG91Z2h0X3NpZ25hdHVyZV92YWxpZGF0b3I=" instead of the literal string "skip_thought_signature_validator".

The Python SDK accepts str for thought_signature, so bypass values are sent as literal strings there.

Note: The API may be transparently handling both base64-encoded and literal string formats, in which case the []byte type works functionally but creates a confusing developer experience — the documented bypass values appear to be plain strings, and there is no indication in the SDK or docs that they will be base64-encoded on the wire.

Reproduction

The test below starts a fake HTTP server, creates a genai.Client pointed at it, and inspects the raw JSON body sent over the wire. It proves the bypass string is base64-encoded rather than sent as a literal.

package main

import (
	"context"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"

	"google.golang.org/genai"
)

func TestBypassStringBase64EncodedOnWire(t *testing.T) {
	bypass := "skip_thought_signature_validator"

	var (
		mu          sync.Mutex
		capturedRaw string
	)

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		raw, _ := io.ReadAll(r.Body)
		mu.Lock()
		capturedRaw = string(raw)
		mu.Unlock()

		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{
			"candidates": [{
				"content": {
					"parts": [{"text": "Response text."}],
					"role": "model"
				},
				"finishReason": "STOP"
			}]
		}`))
	}))
	defer srv.Close()

	client, err := genai.NewClient(context.Background(), &genai.ClientConfig{
		APIKey:  "fake-key",
		Backend: genai.BackendGeminiAPI,
		HTTPOptions: genai.HTTPOptions{
			BaseURL: srv.URL,
		},
	})
	if err != nil {
		t.Fatalf("creating client: %v", err)
	}

	_, _ = client.Models.GenerateContent(context.Background(),
		"gemini-2.5-pro",
		[]*genai.Content{
			{
				Role:  "user",
				Parts: []*genai.Part{{Text: "What's the weather?"}},
			},
			{
				Role: "model",
				Parts: []*genai.Part{{
					FunctionCall: &genai.FunctionCall{
						Name: "get_weather",
						Args: map[string]any{"location": "Paris"},
					},
					ThoughtSignature: []byte(bypass),
				}},
			},
			{
				Role: "user",
				Parts: []*genai.Part{{
					FunctionResponse: &genai.FunctionResponse{
						Name:     "get_weather",
						Response: map[string]any{"result": "sunny"},
					},
				}},
			},
		},
		nil,
	)

	mu.Lock()
	body := capturedRaw
	mu.Unlock()

	if body == "" {
		t.Fatal("no request captured")
	}

	// The literal bypass string should be in the body, but it is not.
	if strings.Contains(body, bypass) {
		t.Error("bypass string found as literal — bug may be fixed")
		return
	}

	// Instead, the base64-encoded version appears.
	b64 := "c2tpcF90aG91Z2h0X3NpZ25hdHVyZV92YWxpZGF0b3I="
	if !strings.Contains(body, b64) {
		t.Errorf("neither literal nor base64 bypass found in body:\n%s", body)
		return
	}

	t.Errorf("ThoughtSignature is base64-encoded on the wire.\n"+
		"Expected: %q\nActual:   %q", bypass, b64)
}

Test output

--- FAIL: TestBypassStringBase64EncodedOnWire (0.00s)
    main_test.go:96: ThoughtSignature is base64-encoded on the wire.
        Expected: "skip_thought_signature_validator"
        Actual:   "c2tpcF90aG91Z2h0X3NpZ25hdHVyZV92YWxpZGF0b3I="

Raw request body captured from the SDK

{
  "contents": [
    {
      "parts": [{"text": "What's the weather?"}],
      "role": "user"
    },
    {
      "parts": [
        {
          "functionCall": {
            "args": {"location": "Paris"},
            "name": "get_weather"
          },
          "thoughtSignature": "c2tpcF90aG91Z2h0X3NpZ25hdHVyZV92YWxpZGF0b3I="
        }
      ],
      "role": "model"
    },
    {
      "parts": [
        {
          "functionResponse": {
            "name": "get_weather",
            "response": {"result": "sunny"}
          }
        }
      ],
      "role": "user"
    }
  ]
}

Root Cause

Part.ThoughtSignature is declared as []byte in types.go:

ThoughtSignature []byte `json:"thoughtSignature,omitempty"`

Go's encoding/json package base64-encodes []byte fields during marshaling (spec). The documented bypass values are plain strings, not binary data, so they need to reach the API as literal string values.

Suggested Resolution

Option A (preferred): Change ThoughtSignature to string so bypass values and real signatures (which are opaque strings from the API) are sent as-is. This would match the Python SDK's behavior where thought_signature is a str.

Option B: If changing to string is a breaking change you'd prefer to avoid, update the Go SDK documentation and the Thought Signatures FAQ to clearly state that the Go SDK base64-encodes ThoughtSignature on the wire and that the API accepts this transparently. Currently the docs present bypass values as plain strings with no mention of encoding behavior, which is confusing for Go SDK users.

Environment

  • google.golang.org/genai v1.48.0
  • Go 1.25

References

Metadata

Metadata

Labels

api:gemini-apiIssues related to Gemini APIpriority: p2Moderately-important priority. Fix may not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions