From 67679a40c86506863fe493a7824b5d8d372a78a1 Mon Sep 17 00:00:00 2001 From: coldpatch Date: Wed, 24 Dec 2025 18:05:52 +0000 Subject: [PATCH 1/2] feat: add Google Vertex AI support, support thought signatures for function calls and fix reasoning option typo --- sdk-go/accumulator.go | 8 +- sdk-go/anthropic/anthropic.go | 2 +- sdk-go/google/google.go | 139 +++++++++++----- sdk-go/google/google_test.go | 287 +++++++++++++++++++++++++++++++- sdk-go/helpers.go | 20 ++- sdk-go/openai/openai.go | 2 +- sdk-go/types.go | 11 +- sdk-go/utils/partutil/stream.go | 9 +- sdk-tests/tests.json | 9 +- 9 files changed, 428 insertions(+), 59 deletions(-) diff --git a/sdk-go/accumulator.go b/sdk-go/accumulator.go index c5c4769..0b3b560 100644 --- a/sdk-go/accumulator.go +++ b/sdk-go/accumulator.go @@ -144,6 +144,9 @@ func mergeDelta(existing accumulatedData, delta ContentDelta) error { if toolCallPartDelta.ID != nil { existingData.ID = toolCallPartDelta.ID } + if toolCallPartDelta.ThoughtSignature != nil { + existingData.ThoughtSignature = toolCallPartDelta.ThoughtSignature + } case existing.Image != nil: imagePartDelta := delta.Part.ImagePartDelta if imagePartDelta == nil { @@ -294,6 +297,9 @@ func createToolCallPart(data *ToolCallPartDelta, index int) (Part, error) { if data.ID != nil { opts = append(opts, WithToolCallPartID(*data.ID)) } + if data.ThoughtSignature != nil { + opts = append(opts, WithToolCallThoughtSignature(*data.ThoughtSignature)) + } toolCallPart := NewToolCallPart(*data.ToolCallID, *data.ToolName, args, opts...) return toolCallPart, nil @@ -353,7 +359,7 @@ func createAudioPart(data *accumulatedAudioData) (Part, error) { // createReasoningPart creates a reasoning part from accumulated reasoning data func createReasoningPart(data *ReasoningPartDelta) Part { - var opts []ReasoingPartOption + var opts []ReasoningPartOption if data.Signature != nil { opts = append(opts, WithReasoningSignature(*data.Signature)) } diff --git a/sdk-go/anthropic/anthropic.go b/sdk-go/anthropic/anthropic.go index 55f6de2..03ed851 100644 --- a/sdk-go/anthropic/anthropic.go +++ b/sdk-go/anthropic/anthropic.go @@ -519,7 +519,7 @@ func mapAnthropicContentBlock(block anthropicapi.ContentBlock) (*llmsdk.Part, er return &part, nil case block.ResponseThinkingBlock != nil: - opts := []llmsdk.ReasoingPartOption{} + opts := []llmsdk.ReasoningPartOption{} if block.ResponseThinkingBlock.Signature != "" { opts = append(opts, llmsdk.WithReasoningSignature(block.ResponseThinkingBlock.Signature)) } diff --git a/sdk-go/google/google.go b/sdk-go/google/google.go index 33a3a67..aa76b29 100644 --- a/sdk-go/google/google.go +++ b/sdk-go/google/google.go @@ -1,9 +1,11 @@ package google import ( + "cmp" "context" "encoding/json" "fmt" + "maps" "net/http" "strings" @@ -20,42 +22,61 @@ import ( const Provider = "google" +type ProviderType string + +const ( + ProviderTypeGoogleAI ProviderType = "google-ai" + ProviderTypeVertexAI ProviderType = "vertex-ai" +) + type GoogleModelOptions struct { - BaseURL string - APIKey string - APIVersion string - Headers map[string]string - HTTPClient *http.Client + BaseURL string + APIKey string + APIVersion string + Headers map[string]string + HTTPClient *http.Client + ProviderType ProviderType + AccessToken string + ProjectID string + Location string } type GoogleModel struct { - baseURL string - apiKey string - apiVersion string - modelID string - client *http.Client - metadata *llmsdk.LanguageModelMetadata - headers map[string]string + baseURL string + apiKey string + apiVersion string + modelID string + client *http.Client + metadata *llmsdk.LanguageModelMetadata + headers map[string]string + providerType ProviderType + accessToken string + projectID string + location string } func NewGoogleModel(modelID string, options GoogleModelOptions) *GoogleModel { - baseURL := "https://generativelanguage.googleapis.com" - if options.BaseURL != "" { - baseURL = options.BaseURL - } - apiVersion := "v1beta" - if options.APIVersion != "" { - apiVersion = options.APIVersion - } - - client := options.HTTPClient - if client == nil { - client = &http.Client{} + providerType := cmp.Or(options.ProviderType, ProviderTypeGoogleAI) + + baseURL := options.BaseURL + if baseURL == "" { + baseURL = "https://generativelanguage.googleapis.com" + if providerType == ProviderTypeVertexAI { + if options.Location == "" || options.Location == "global" { + baseURL = "https://aiplatform.googleapis.com" + } else { + baseURL = "https://" + options.Location + "-aiplatform.googleapis.com" + } + } } - headers := map[string]string{} - for k, v := range options.Headers { - headers[k] = v + apiVersion := options.APIVersion + if apiVersion == "" { + if providerType == ProviderTypeVertexAI { + apiVersion = "v1beta1" + } else { + apiVersion = "v1beta" + } } return &GoogleModel{ @@ -63,8 +84,13 @@ func NewGoogleModel(modelID string, options GoogleModelOptions) *GoogleModel { apiKey: options.APIKey, apiVersion: apiVersion, modelID: modelID, - client: client, - headers: headers, + client: cmp.Or(options.HTTPClient, &http.Client{}), + headers: maps.Clone(options.Headers), + + providerType: providerType, + accessToken: options.AccessToken, + projectID: options.ProjectID, + location: options.Location, } } @@ -86,17 +112,44 @@ func (m *GoogleModel) Metadata() *llmsdk.LanguageModelMetadata { } func (m *GoogleModel) requestHeaders() map[string]string { - headers := map[string]string{ - "x-goog-api-key": m.apiKey, + headers := maps.Clone(m.headers) + if headers == nil { + headers = make(map[string]string, 1) } - for k, v := range m.headers { - headers[k] = v + if m.accessToken != "" { + headers["Authorization"] = "Bearer " + m.accessToken + } else if m.apiKey != "" { + headers["x-goog-api-key"] = m.apiKey } return headers } +func (m *GoogleModel) buildEndpointURL(action string) string { + var b strings.Builder + b.WriteString(m.baseURL) + b.WriteByte('/') + b.WriteString(m.apiVersion) + + if m.providerType == ProviderTypeVertexAI { + if m.projectID != "" { + b.WriteString("/projects/") + b.WriteString(m.projectID) + b.WriteString("/locations/") + b.WriteString(cmp.Or(m.location, "global")) + } + b.WriteString("/publishers/google") + } + + b.WriteString("/models/") + b.WriteString(m.modelID) + b.WriteByte(':') + b.WriteString(action) + + return b.String() +} + func (m *GoogleModel) Generate(ctx context.Context, input *llmsdk.LanguageModelInput) (*llmsdk.ModelResponse, error) { return tracing.TraceGenerate(ctx, string(Provider), m.modelID, input, func(ctx context.Context) (*llmsdk.ModelResponse, error) { params, err := convertToGenerateContentParameters(input, m.modelID) @@ -105,7 +158,7 @@ func (m *GoogleModel) Generate(ctx context.Context, input *llmsdk.LanguageModelI } response, err := clientutils.DoJSON[googleapi.GenerateContentResponse](ctx, m.client, clientutils.JSONRequestConfig{ - URL: fmt.Sprintf("%s/%s/models/%s:generateContent", m.baseURL, m.apiVersion, m.modelID), + URL: m.buildEndpointURL("generateContent"), Headers: m.requestHeaders(), Body: params, }) @@ -149,7 +202,7 @@ func (m *GoogleModel) Stream(ctx context.Context, input *llmsdk.LanguageModelInp } sseStream, err := clientutils.DoSSE[googleapi.GenerateContentResponse](ctx, m.client, clientutils.SSERequestConfig{ - URL: fmt.Sprintf("%s/%s/models/%s:streamGenerateContent?alt=sse", m.baseURL, m.apiVersion, m.modelID), + URL: m.buildEndpointURL("streamGenerateContent?alt=sse"), Headers: m.requestHeaders(), Body: params, }) @@ -345,13 +398,17 @@ func convertToGoogleParts(part llmsdk.Part) ([]googleapi.Part, error) { parts, ), nil case part.ToolCallPart != nil: - return []googleapi.Part{{ + googlePart := googleapi.Part{ FunctionCall: &googleapi.FunctionCall{ Name: &part.ToolCallPart.ToolName, Args: part.ToolCallPart.Args, Id: &part.ToolCallPart.ToolCallID, }, - }}, nil + } + if part.ToolCallPart.ThoughtSignature != nil { + googlePart.ThoughtSignature = part.ToolCallPart.ThoughtSignature + } + return []googleapi.Part{googlePart}, nil case part.ToolResultPart != nil: response, err := convertToGoogleFunctionResponseResponse(part.ToolResultPart.Content, part.ToolResultPart.IsError) if err != nil { @@ -493,7 +550,7 @@ func mapGoogleContent(parts []googleapi.Part) ([]llmsdk.Part, error) { if part.Text != nil { text = *part.Text } - opts := []llmsdk.ReasoingPartOption{} + opts := []llmsdk.ReasoningPartOption{} if part.ThoughtSignature != nil { opts = append(opts, llmsdk.WithReasoningSignature(*part.ThoughtSignature)) } @@ -534,7 +591,11 @@ func mapGoogleContent(parts []googleapi.Part) ([]llmsdk.Part, error) { } else { toolCallID = fmt.Sprintf("call_%s", randutil.String(10)) } - toolCallPart := llmsdk.NewToolCallPart(toolCallID, *part.FunctionCall.Name, json.RawMessage(part.FunctionCall.Args)) + opts := []llmsdk.ToolCallPartOption{} + if part.ThoughtSignature != nil { + opts = append(opts, llmsdk.WithToolCallThoughtSignature(*part.ThoughtSignature)) + } + toolCallPart := llmsdk.NewToolCallPart(toolCallID, *part.FunctionCall.Name, json.RawMessage(part.FunctionCall.Args), opts...) toolCallPart.ToolCallPart.Args = part.FunctionCall.Args result = append(result, toolCallPart) continue diff --git a/sdk-go/google/google_test.go b/sdk-go/google/google_test.go index 911007b..57adbb7 100644 --- a/sdk-go/google/google_test.go +++ b/sdk-go/google/google_test.go @@ -1,7 +1,11 @@ package google_test import ( + "context" + "io" + "net/http" "os" + "strings" "testing" llmsdk "github.com/hoangvvo/llm-sdk/sdk-go" @@ -15,15 +19,18 @@ var model *google.GoogleModel var audioModel *google.GoogleModel var imageModel *google.GoogleModel var reasoningModel *google.GoogleModel +var vertexGlobalModel *google.GoogleModel +var vertexProjectModel *google.GoogleModel func TestMain(m *testing.M) { godotenv.Load("../../.env") apiKey := os.Getenv("GOOGLE_API_KEY") + vertexAccessToken := os.Getenv("VERTEX_ACCESS_TOKEN") if apiKey == "" { panic("GOOGLE_API_KEY must be set") } - model = google.NewGoogleModel("gemini-2.5-flash", google.GoogleModelOptions{ + model = google.NewGoogleModel("gemini-3-flash-preview", google.GoogleModelOptions{ APIKey: apiKey, }) audioModel = google.NewGoogleModel("gemini-2.5-flash-preview-tts", google.GoogleModelOptions{ @@ -32,10 +39,28 @@ func TestMain(m *testing.M) { imageModel = google.NewGoogleModel("gemini-2.5-flash-image-preview", google.GoogleModelOptions{ APIKey: apiKey, }) - reasoningModel = google.NewGoogleModel("gemini-2.0-flash-thinking-exp-01-21", google.GoogleModelOptions{ + reasoningModel = google.NewGoogleModel("gemini-3-flash-preview", google.GoogleModelOptions{ APIKey: apiKey, }) + if vertexAccessToken != "" { + vertexGlobalModel = google.NewGoogleModel("gemini-2.5-flash-lite", google.GoogleModelOptions{ + AccessToken: vertexAccessToken, + ProviderType: google.ProviderTypeVertexAI, + }) + + vertexProjectID := os.Getenv("VERTEX_PROJECT_ID") + vertexLocation := os.Getenv("VERTEX_LOCATION") + if vertexProjectID != "" && vertexLocation != "" { + vertexProjectModel = google.NewGoogleModel("gemini-2.5-flash-lite", google.GoogleModelOptions{ + AccessToken: vertexAccessToken, + ProjectID: vertexProjectID, + Location: vertexLocation, + ProviderType: google.ProviderTypeVertexAI, + }) + } + } + m.Run() } @@ -151,3 +176,261 @@ func TestGenerateReasoning(t *testing.T) { func TestStreamReasoning(t *testing.T) { testcommon.RunTestCase(t, reasoningModel, "stream_reasoning") } + +func TestGenerateThoughtSignatures(t *testing.T) { + ctx := t.Context() + + input := &llmsdk.LanguageModelInput{ + Messages: []llmsdk.Message{ + llmsdk.NewUserMessage(llmsdk.NewTextPart("What's the weather like in San Francisco?")), + }, + Tools: []llmsdk.Tool{testcommon.GetWeatherTool()}, + } + + result, err := model.Generate(ctx, input) + if err != nil { + t.Fatalf("Initial generate failed: %v", err) + } + + var toolCallPart *llmsdk.ToolCallPart + for _, part := range result.Content { + if part.ToolCallPart != nil { + toolCallPart = part.ToolCallPart + break + } + } + + if toolCallPart == nil { + t.Fatal("Expected a tool call in the response") + } + + if toolCallPart.ThoughtSignature == nil { + t.Fatal("Expected thought signature on tool call for Gemini 3 model") + } + + input2 := &llmsdk.LanguageModelInput{ + Messages: []llmsdk.Message{ + llmsdk.NewUserMessage(llmsdk.NewTextPart("What's the weather like in San Francisco?")), + llmsdk.NewAssistantMessage(llmsdk.Part{ToolCallPart: toolCallPart}), + llmsdk.NewToolMessage(llmsdk.NewToolResultPart( + toolCallPart.ToolCallID, + toolCallPart.ToolName, + []llmsdk.Part{llmsdk.NewTextPart(`{"temperature": 65, "unit": "f", "description": "Foggy"}`)}, + )), + }, + Tools: []llmsdk.Tool{testcommon.GetWeatherTool()}, + } + + result2, err := model.Generate(ctx, input2) + if err != nil { + t.Fatalf("Generate with tool result failed: %v", err) + } + + var hasTextPart bool + for _, part := range result2.Content { + if part.TextPart != nil { + hasTextPart = true + break + } + } + + if !hasTextPart { + t.Fatal("Expected text response after providing tool result") + } +} + +func TestStreamThoughtSignatures(t *testing.T) { + ctx := t.Context() + + input := &llmsdk.LanguageModelInput{ + Messages: []llmsdk.Message{ + llmsdk.NewUserMessage(llmsdk.NewTextPart("What's the stock price of GOOG?")), + }, + Tools: []llmsdk.Tool{testcommon.GetStockPriceTool()}, + } + + stream, err := model.Stream(ctx, input) + if err != nil { + t.Fatalf("Initial stream failed: %v", err) + } + + accumulator := llmsdk.NewStreamAccumulator() + for stream.Next() { + partial := stream.Current() + if err := accumulator.AddPartial(*partial); err != nil { + t.Fatalf("Failed to add partial: %v", err) + } + } + if err := stream.Err(); err != nil { + t.Fatalf("Stream error: %v", err) + } + + result, err := accumulator.ComputeResponse() + if err != nil { + t.Fatalf("Failed to compute response: %v", err) + } + + var toolCallPart *llmsdk.ToolCallPart + for _, part := range result.Content { + if part.ToolCallPart != nil { + toolCallPart = part.ToolCallPart + break + } + } + + if toolCallPart == nil { + t.Fatal("Expected a tool call in the streamed response") + } + + if toolCallPart.ThoughtSignature == nil { + t.Fatal("Expected thought signature on streamed tool call for Gemini 3 model") + } + + input2 := &llmsdk.LanguageModelInput{ + Messages: []llmsdk.Message{ + llmsdk.NewUserMessage(llmsdk.NewTextPart("What's the stock price of GOOG?")), + llmsdk.NewAssistantMessage(llmsdk.Part{ToolCallPart: toolCallPart}), + llmsdk.NewToolMessage(llmsdk.NewToolResultPart( + toolCallPart.ToolCallID, + toolCallPart.ToolName, + []llmsdk.Part{llmsdk.NewTextPart(`{"price": 175.50, "currency": "USD"}`)}, + )), + }, + Tools: []llmsdk.Tool{testcommon.GetStockPriceTool()}, + } + + stream2, err := model.Stream(ctx, input2) + if err != nil { + t.Fatalf("Stream with tool result failed: %v", err) + } + + accumulator2 := llmsdk.NewStreamAccumulator() + for stream2.Next() { + partial := stream2.Current() + if err := accumulator2.AddPartial(*partial); err != nil { + t.Fatalf("Failed to add partial: %v", err) + } + } + if err := stream2.Err(); err != nil { + t.Fatalf("Stream error: %v", err) + } + + result2, err := accumulator2.ComputeResponse() + if err != nil { + t.Fatalf("Failed to compute response: %v", err) + } + + var hasTextPart bool + for _, part := range result2.Content { + if part.TextPart != nil { + hasTextPart = true + break + } + } + + if !hasTextPart { + t.Fatal("Expected text response after providing tool result in stream") + } +} + +type roundTripFunc func(req *http.Request) *http.Response + +func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +func TestVertexAIGlobalGenerateText(t *testing.T) { + if vertexGlobalModel == nil { + t.Skip("VERTEX_ACCESS_TOKEN not set") + } + testcommon.RunTestCase(t, vertexGlobalModel, "generate_text") +} + +func TestVertexAIGlobalStreamText(t *testing.T) { + if vertexGlobalModel == nil { + t.Skip("VERTEX_ACCESS_TOKEN not set") + } + testcommon.RunTestCase(t, vertexGlobalModel, "stream_text") +} + +func TestVertexAIProjectGenerateText(t *testing.T) { + if vertexProjectModel == nil { + t.Skip("VERTEX_PROJECT_ID, VERTEX_LOCATION or VERTEX_ACCESS_TOKEN not set") + } + testcommon.RunTestCase(t, vertexProjectModel, "generate_text") +} + +func TestVertexAIProjectStreamText(t *testing.T) { + if vertexProjectModel == nil { + t.Skip("VERTEX_PROJECT_ID, VERTEX_LOCATION or VERTEX_ACCESS_TOKEN not set") + } + testcommon.RunTestCase(t, vertexProjectModel, "stream_text") +} + +func TestVertexAIURLAndHeaders(t *testing.T) { + type vertexURLHeaderTestCase struct { + name string + modelName string + options google.GoogleModelOptions + expectedURL string + headers map[string]string + } + testCases := []vertexURLHeaderTestCase{ + { + name: "Global", + modelName: "gemini-3-flash", + options: google.GoogleModelOptions{ + APIKey: "test-api-key", + ProviderType: google.ProviderTypeVertexAI, + APIVersion: "v1", + }, + expectedURL: "https://aiplatform.googleapis.com/v1/publishers/google/models/gemini-3-flash:generateContent", + headers: map[string]string{"x-goog-api-key": "test-api-key"}, + }, + { + name: "Project", + modelName: "gemini-3-flash", + options: google.GoogleModelOptions{ + AccessToken: "test-key", + ProjectID: "test-project", + Location: "us-central1", + ProviderType: google.ProviderTypeVertexAI, + }, + expectedURL: "https://us-central1-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/us-central1/publishers/google/models/gemini-3-flash:generateContent", + headers: map[string]string{"Authorization": "Bearer test-key"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + opts := tc.options + opts.HTTPClient = &http.Client{ + Transport: roundTripFunc(func(req *http.Request) *http.Response { + if req.URL.String() != tc.expectedURL { + t.Errorf("expected URL %s, got %s", tc.expectedURL, req.URL.String()) + } + for header, expected := range tc.headers { + if got := req.Header.Get(header); got != expected { + t.Errorf("expected header %s=%q, got %q", header, expected, got) + } + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"candidates":[{"content":{"parts":[{"text":"Hello"}]}}]}`)), + Header: make(http.Header), + } + }), + } + + m := google.NewGoogleModel(tc.modelName, opts) + _, err := m.Generate(context.Background(), &llmsdk.LanguageModelInput{ + Messages: []llmsdk.Message{ + llmsdk.NewUserMessage(llmsdk.NewTextPart("Hi")), + }, + }) + if err != nil { + t.Fatalf("Generate failed: %v", err) + } + }) + } +} diff --git a/sdk-go/helpers.go b/sdk-go/helpers.go index 3bffa33..d645c60 100644 --- a/sdk-go/helpers.go +++ b/sdk-go/helpers.go @@ -114,7 +114,7 @@ func NewSourcePart(source string, title string, content []Part) Part { } } -func NewReasoningPart(text string, opts ...ReasoingPartOption) Part { +func NewReasoningPart(text string, opts ...ReasoningPartOption) Part { reasoningPart := &ReasoningPart{ Text: text, } @@ -128,15 +128,15 @@ func NewReasoningPart(text string, opts ...ReasoingPartOption) Part { } } -type ReasoingPartOption func(*ReasoningPart) +type ReasoningPartOption func(*ReasoningPart) -func WithReasoningSignature(signature string) ReasoingPartOption { +func WithReasoningSignature(signature string) ReasoningPartOption { return func(p *ReasoningPart) { p.Signature = &signature } } -func WithReasoningID(id string) ReasoingPartOption { +func WithReasoningID(id string) ReasoningPartOption { return func(p *ReasoningPart) { p.ID = &id } @@ -180,6 +180,12 @@ func WithToolCallPartID(id string) ToolCallPartOption { } } +func WithToolCallThoughtSignature(signature string) ToolCallPartOption { + return func(p *ToolCallPart) { + p.ThoughtSignature = &signature + } +} + // NewToolResultPart creates a new tool result part func NewToolResultPart(toolCallID, toolName string, content []Part, opts ...ToolResultPartOption) Part { toolResultPart := Part{ @@ -302,6 +308,12 @@ func WithToolCallPartDeltaID(id string) ToolCallPartDeltaOption { } } +func WithToolCallPartDeltaThoughtSignature(signature string) ToolCallPartDeltaOption { + return func(p *ToolCallPartDelta) { + p.ThoughtSignature = &signature + } +} + // NewImagePartDelta constructs an image part delta for incremental image updates. func NewImagePartDelta(opts ...ImagePartDeltaOption) PartDelta { imageDelta := &ImagePartDelta{} diff --git a/sdk-go/openai/openai.go b/sdk-go/openai/openai.go index d8f1332..e980baa 100644 --- a/sdk-go/openai/openai.go +++ b/sdk-go/openai/openai.go @@ -636,7 +636,7 @@ func mapOpenAIOutputItems(items []openaiapi.ResponseOutputItem) ([]llmsdk.Part, } } - reasoningOpts := []llmsdk.ReasoingPartOption{} + reasoningOpts := []llmsdk.ReasoningPartOption{} if item.ResponseReasoningItem.EncryptedContent != nil { reasoningOpts = append(reasoningOpts, llmsdk.WithReasoningSignature(*item.ResponseReasoningItem.EncryptedContent)) } diff --git a/sdk-go/types.go b/sdk-go/types.go index 834a528..09f8b15 100644 --- a/sdk-go/types.go +++ b/sdk-go/types.go @@ -120,6 +120,8 @@ type ToolCallPart struct { // The ID of the part, if applicable. // This is different from ToolCallID which is used to match tool results. ID *string `json:"id,omitempty"` + // ThoughtSignature is an encrypted representation of the model's internal thought process - used for Gemini 3 models during function calling. + ThoughtSignature *string `json:"thought_signature,omitempty"` } // ToolResultPart represents a part of the message that represents the result of a tool call. @@ -321,10 +323,11 @@ func (c CitationDelta) MarshalJSON() ([]byte, error) { // ToolCallPartDelta represents a delta update for a tool call part, used in streaming of a tool invocation. type ToolCallPartDelta struct { - ToolCallID *string `json:"tool_call_id,omitempty"` - ToolName *string `json:"tool_name,omitempty"` - Args *string `json:"args,omitempty"` - ID *string `json:"id,omitempty"` + ToolCallID *string `json:"tool_call_id,omitempty"` + ToolName *string `json:"tool_name,omitempty"` + Args *string `json:"args,omitempty"` + ID *string `json:"id,omitempty"` + ThoughtSignature *string `json:"thought_signature,omitempty"` } // ImagePartDelta represents a delta update for an image part, used in streaming of an image message. diff --git a/sdk-go/utils/partutil/stream.go b/sdk-go/utils/partutil/stream.go index fd64183..50968e1 100644 --- a/sdk-go/utils/partutil/stream.go +++ b/sdk-go/utils/partutil/stream.go @@ -106,10 +106,11 @@ func LooselyConvertPartToPartDelta(part llmsdk.Part) llmsdk.PartDelta { argsStr := string(part.ToolCallPart.Args) return llmsdk.PartDelta{ ToolCallPartDelta: &llmsdk.ToolCallPartDelta{ - ToolCallID: &part.ToolCallPart.ToolCallID, - ToolName: &part.ToolCallPart.ToolName, - Args: &argsStr, - ID: part.ToolCallPart.ID, + ToolCallID: &part.ToolCallPart.ToolCallID, + ToolName: &part.ToolCallPart.ToolName, + Args: &argsStr, + ID: part.ToolCallPart.ID, + ThoughtSignature: part.ToolCallPart.ThoughtSignature, }, } case part.ReasoningPart != nil: diff --git a/sdk-tests/tests.json b/sdk-tests/tests.json index ddf201e..4349bc4 100644 --- a/sdk-tests/tests.json +++ b/sdk-tests/tests.json @@ -201,7 +201,8 @@ "tool_name": "get_weather", "args": { "location": "Boston" - } + }, + "thought_signature": "skip_thought_signature_validator" } ] }, @@ -256,7 +257,8 @@ "tool_name": "get_weather", "args": { "location": "Boston" - } + }, + "thought_signature": "skip_thought_signature_validator" } ] }, @@ -484,7 +486,8 @@ "type": "tool-call", "tool_call_id": "0mbnj08nt", "tool_name": "get_first_secret_number", - "args": {} + "args": {}, + "thought_signature": "skip_thought_signature_validator" } ] }, From 5667a699805e25d1be4e5821c0de86a2a456e850 Mon Sep 17 00:00:00 2001 From: coldpatch Date: Wed, 24 Dec 2025 19:32:23 +0000 Subject: [PATCH 2/2] feat: clarify Vertex AI configuration options in readme --- sdk-go/README.md | 4 ++++ sdk-go/google/google_test.go | 27 +++++++++++++++------------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/sdk-go/README.md b/sdk-go/README.md index c08a501..075bf83 100644 --- a/sdk-go/README.md +++ b/sdk-go/README.md @@ -64,6 +64,10 @@ func GetModel(provider, modelID string) llmsdk.LanguageModel { } return google.NewGoogleModel(modelID, google.GoogleModelOptions{ APIKey: apiKey, + // ProviderType: google.ProviderTypeVertexAI, + // AccessToken: "your-access-token", + // Location: "us-central1", + // ProjectID: "your-project-id", }) default: panic(fmt.Sprintf("Unsupported provider: %s", provider)) diff --git a/sdk-go/google/google_test.go b/sdk-go/google/google_test.go index 57adbb7..ebf527d 100644 --- a/sdk-go/google/google_test.go +++ b/sdk-go/google/google_test.go @@ -25,7 +25,6 @@ var vertexProjectModel *google.GoogleModel func TestMain(m *testing.M) { godotenv.Load("../../.env") apiKey := os.Getenv("GOOGLE_API_KEY") - vertexAccessToken := os.Getenv("VERTEX_ACCESS_TOKEN") if apiKey == "" { panic("GOOGLE_API_KEY must be set") } @@ -43,21 +42,25 @@ func TestMain(m *testing.M) { APIKey: apiKey, }) - if vertexAccessToken != "" { - vertexGlobalModel = google.NewGoogleModel("gemini-2.5-flash-lite", google.GoogleModelOptions{ - AccessToken: vertexAccessToken, + vertexAccessToken := os.Getenv("VERTEX_ACCESS_TOKEN") + vertexAPIKey := os.Getenv("VERTEX_API_KEY") + if vertexAccessToken != "" || vertexAPIKey != "" { + globalOptions := google.GoogleModelOptions{ ProviderType: google.ProviderTypeVertexAI, - }) + } + if vertexAccessToken != "" { + globalOptions.AccessToken = vertexAccessToken + } else if vertexAPIKey != "" { + globalOptions.APIKey = vertexAPIKey + } + vertexGlobalModel = google.NewGoogleModel("gemini-2.5-flash-lite", globalOptions) vertexProjectID := os.Getenv("VERTEX_PROJECT_ID") vertexLocation := os.Getenv("VERTEX_LOCATION") - if vertexProjectID != "" && vertexLocation != "" { - vertexProjectModel = google.NewGoogleModel("gemini-2.5-flash-lite", google.GoogleModelOptions{ - AccessToken: vertexAccessToken, - ProjectID: vertexProjectID, - Location: vertexLocation, - ProviderType: google.ProviderTypeVertexAI, - }) + if vertexProjectID != "" && vertexLocation != "" && vertexAccessToken != "" { + globalOptions.ProjectID = vertexProjectID + globalOptions.Location = vertexLocation + vertexProjectModel = google.NewGoogleModel("gemini-2.5-flash-lite", globalOptions) } }