diff --git a/README.md b/README.md index 277f3993..abbd107c 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,10 @@ Google's generative models into their Go applications. It supports the [Gemini Enterprise Agent Platform](https://docs.cloud.google.com/gemini-enterprise-agent-platform) APIs. +> [!NOTE] +> The GenAI SDK now supports the [Interactions API](#interactions) (Experimental). + + The Google Gen AI Go SDK enables developers to use Google's state-of-the-art generative AI models (like Gemini) to build AI-powered features and applications. This SDK supports use cases like: @@ -80,6 +84,52 @@ export GOOGLE_CLOUD_LOCATION='us-central1' client, err := genai.NewClient(ctx, &genai.ClientConfig{}) ``` +## Interactions + +The Interactions API allows you to interact with agents and models in a multi-turn conversation. + +Here is a simple example of creating a new interaction with a model: + +```go +package main + +import ( + "context" + "log" + + "google.golang.org/genai" + "google.golang.org/genai/interactions" +) + +func main() { + ctx := context.Background() + client, err := genai.NewClient(ctx, nil) + if err != nil { + log.Fatal(err) + } + + params := interactions.NewModelParams{ + Model: "gemini-2.5-flash", + Input: interactions.Input{String: ptr("Tell me a short joke about programming.")}, + } + + interaction, err := client.Interactions.NewModel(ctx, params) + if err != nil { + log.Fatal(err) + } + + for _, output := range interaction.Outputs { + if output.Text != nil { + println(output.Text.Text) + } + } +} + +func ptr[T any](v T) *T { + return &v +} +``` + ## License The contents of this repository are licensed under the diff --git a/client.go b/client.go index 1e24561a..2178d413 100644 --- a/client.go +++ b/client.go @@ -20,7 +20,12 @@ import ( "log" "net/http" "os" + "strings" + "sync" + + "google.golang.org/genai/interactions" + interactionsopt "google.golang.org/genai/interactions/option" "cloud.google.com/go/auth" "cloud.google.com/go/auth/credentials" @@ -50,6 +55,11 @@ type Client struct { Tunings *Tunings // Tokens provides access to the Tokens service. AuthTokens *Tokens + + // Interactions provides access to the Interactions service. + // + // Experimental: This field is experimental and may change in future versions. + Interactions *interactions.InteractionService } // Backend is the GenAI backend to use for the client. @@ -387,6 +397,41 @@ func NewInternalAPIClient(ctx context.Context, cc *ClientConfig) (*InternalAPICl } return &apiClient{clientConfig: cc}, nil } +var experimentalWarningInteractions sync.Once + +func newInteractionsClient(apiClient *InternalAPIClient) interactions.Client { + experimentalWarningInteractions.Do(func() { + log.Println("Warning: The Interactions service is experimental and may change in future versions.") + }) + cc := apiClient.clientConfig + + var opts []interactionsopt.RequestOption + if cc.APIKey != "" { + opts = append(opts, interactionsopt.WithAPIKey(cc.APIKey)) + } + if cc.HTTPClient != nil { + opts = append(opts, interactionsopt.WithHTTPClient(cc.HTTPClient)) + } + if cc.HTTPOptions.BaseURL != "" { + opts = append(opts, interactionsopt.WithBaseURL(cc.HTTPOptions.BaseURL)) + } + if cc.HTTPOptions.APIVersion != "" { + opts = append(opts, interactionsopt.WithAPIVersion(cc.HTTPOptions.APIVersion)) + } + if cc.HTTPOptions.Headers != nil { + for key, values := range cc.HTTPOptions.Headers { + for _, value := range values { + opts = append(opts, interactionsopt.WithHeaderAdd(key, value)) + } + } + } + if cc.HTTPOptions.Timeout != nil { + opts = append(opts, interactionsopt.WithRequestTimeout(*cc.HTTPOptions.Timeout)) + } + + return interactions.NewClient(opts...) + +} // NewClient creates a new GenAI client. // @@ -422,6 +467,7 @@ func NewClient(ctx context.Context, cc *ClientConfig) (*Client, error) { if err != nil { return nil, err } + interactionsClient := newInteractionsClient(ac) c := &Client{ clientConfig: *cc, Models: &Models{apiClient: ac}, @@ -434,6 +480,7 @@ func NewClient(ctx context.Context, cc *ClientConfig) (*Client, error) { Batches: &Batches{apiClient: ac}, Tunings: &Tunings{apiClient: ac}, AuthTokens: &Tokens{apiClient: ac}, + Interactions: &interactionsClient.Interactions, } return c, nil } diff --git a/examples/interactions/interactions.go b/examples/interactions/interactions.go new file mode 100644 index 00000000..3386618e --- /dev/null +++ b/examples/interactions/interactions.go @@ -0,0 +1,60 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build ignore_vet + +package main + +import ( + "context" + "log" + + "github.com/sanity-io/litter" + "google.golang.org/genai" + "google.golang.org/genai/interactions" +) + +func main() { + ctx := context.Background() + client, err := genai.NewClient(ctx, &genai.ClientConfig{HTTPOptions: genai.HTTPOptions{BaseURL: "https://staging-generativelanguage.sandbox.googleapis.com/"}}) + if err != nil { + log.Fatal(err) + } + + litter.Config.HideZeroValues = true // cleaner output + + params := interactions.NewModelParams{ + Model: "gemini-2.5-flash", + Input: interactions.Input{String: ptr("Tell me a short joke about programming.")}, + } + + interaction, err := client.Interactions.NewModel(ctx, params) + if err != nil { + log.Fatal(err) + } + + litter.Dump(interaction) + + for _, output := range interaction.Outputs { + if output.Text != nil { + println(output.Text.Text) + } + } +} + +func ptr[T any](v T) *T { + return &v +} + +} diff --git a/go.mod b/go.mod index 89aac845..1b568643 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,8 @@ require ( github.com/eliben/go-sentencepiece v0.6.0 github.com/google/go-cmp v0.6.0 github.com/gorilla/websocket v1.5.3 + github.com/tidwall/gjson v1.18.0 + github.com/tidwall/sjson v1.2.5 ) require ( @@ -17,6 +19,9 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/sanity-io/litter v1.5.8 + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/net v0.38.0 // indirect diff --git a/go.sum b/go.sum index 9547f913..6209b3df 100644 --- a/go.sum +++ b/go.sum @@ -9,7 +9,9 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eliben/go-sentencepiece v0.6.0 h1:wbnefMCxYyVYmeTVtiMJet+mS9CVwq5klveLpfQLsnk= github.com/eliben/go-sentencepiece v0.6.0/go.mod h1:nNYk4aMzgBoI6QFp4LUG8Eu1uO9fHD9L5ZEre93o9+c= @@ -46,14 +48,30 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gT github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg= +github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -121,6 +139,7 @@ google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6h google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/interaction_test.go b/interaction_test.go new file mode 100644 index 00000000..1be1861f --- /dev/null +++ b/interaction_test.go @@ -0,0 +1,86 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package genai + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "google.golang.org/genai/interactions" +) + +func TestInteractionsWorkflow(t *testing.T) { + // Create a mock server + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1beta/interactions" { + t.Errorf("Path unexpected: %s", r.URL.Path) + } + if r.Method != "POST" { + t.Errorf("Method unexpected: %s", r.Method) + } + + // Return a mock response + resp := map[string]any{ + "id": "mock_interaction_id", + "created": "2026-03-30T22:20:00Z", + "updated": "2026-03-30T22:20:00Z", + "status": "completed", + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(resp); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + })) + defer ts.Close() + + // Create client pointing to the mock server + client, err := NewClient(context.Background(), &ClientConfig{ + Backend: BackendGeminiAPI, + HTTPOptions: HTTPOptions{ + BaseURL: ts.URL, + }, + APIKey: "dummy_key", + }) + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + + if client.Interactions == nil { + t.Fatalf("client.Interactions is nil") + } + + // Call NewModel + res, err := client.Interactions.NewModel(context.Background(), interactions.NewModelParams{ + Model: "gemini-2.5-flash", + Input: interactions.Input{ + ContentList: []interactions.Content{{ + Text: &interactions.TextContent{ + Text: "Hello", + }, + }}, + }, + }) + if err != nil { + t.Fatalf("Failed to call NewModel: %v", err) + } + + if res.ID != "mock_interaction_id" { + t.Errorf("Expected ID 'mock_interaction_id', got '%s'", res.ID) + } +} diff --git a/interactions/aliases.go b/interactions/aliases.go new file mode 100644 index 00000000..49258833 --- /dev/null +++ b/interactions/aliases.go @@ -0,0 +1,27 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package interactions + +import ( + "google.golang.org/genai/interactions/internal/apierror" + "google.golang.org/genai/interactions/internal/apijson/unmarshalinfo" +) + +// aliased to make [unmarshalinfo.Metadata] private when embedding +type metadata = unmarshalinfo.Metadata + +type Error = apierror.Error diff --git a/interactions/client.go b/interactions/client.go new file mode 100644 index 00000000..d0b070b4 --- /dev/null +++ b/interactions/client.go @@ -0,0 +1,141 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package interactions + +import ( + "context" + "net/http" + "os" + "slices" + "strings" + + "google.golang.org/genai/interactions/internal/requestconfig" + "google.golang.org/genai/interactions/option" +) + +// Client creates a struct with services and top level methods that help with +// interacting with the gemini-next-gen-api API. You should not instantiate this +// client directly, and instead use the [NewClient] method instead. +type Client struct { + Options []option.RequestOption + Interactions InteractionService +} + +// DefaultClientOptions read from the environment (GEMINI_API_KEY, +// GEMINI_NEXT_GEN_API_BASE_URL). This should be used to initialize new clients. +func DefaultClientOptions() []option.RequestOption { + defaults := []option.RequestOption{option.WithHTTPClient(defaultHTTPClient()), option.WithEnvironmentProduction()} + if o, ok := os.LookupEnv("GEMINI_NEXT_GEN_API_BASE_URL"); ok { + defaults = append(defaults, option.WithBaseURL(o)) + } + defaults = append(defaults, option.WithAPIVersion("v1beta")) + if o, ok := os.LookupEnv("GEMINI_API_KEY"); ok { + defaults = append(defaults, option.WithAPIKey(o)) + } + if o, ok := os.LookupEnv("GEMINI_NEXT_GEN_API_CUSTOM_HEADERS"); ok { + for _, line := range strings.Split(o, "\n") { + colon := strings.Index(line, ":") + if colon >= 0 { + defaults = append(defaults, option.WithHeader(strings.TrimSpace(line[:colon]), strings.TrimSpace(line[colon+1:]))) + } + } + } + return defaults +} + +// NewClient generates a new client with the default option read from the +// environment (GEMINI_API_KEY, GEMINI_NEXT_GEN_API_BASE_URL). The option passed in +// as arguments are applied after these default arguments, and all option will be +// passed down to the services and requests that this client makes. +func NewClient(opts ...option.RequestOption) (r Client) { + opts = append(DefaultClientOptions(), opts...) + + r = Client{Options: opts} + + r.Interactions = NewInteractionService(opts...) + + return +} + +// Execute makes a request with the given context, method, URL, request params, +// response, and request options. This is useful for hitting undocumented endpoints +// while retaining the base URL, auth, retries, and other options from the client. +// +// If a byte slice or an [io.Reader] is supplied to params, it will be used as-is +// for the request body. +// +// The params is by default serialized into the body using [encoding/json]. If your +// type implements a MarshalJSON function, it will be used instead to serialize the +// request. If a URLQuery method is implemented, the returned [url.Values] will be +// used as query strings to the url. +// +// If your params struct uses [param.Field], you must provide either [MarshalJSON], +// [URLQuery], and/or [MarshalForm] functions. It is undefined behavior to use a +// struct uses [param.Field] without specifying how it is serialized. +// +// Any "…Params" object defined in this library can be used as the request +// argument. Note that 'path' arguments will not be forwarded into the url. +// +// The response body will be deserialized into the res variable, depending on its +// type: +// +// - A pointer to a [*http.Response] is populated by the raw response. +// - A pointer to a byte array will be populated with the contents of the request +// body. +// - A pointer to any other type uses this library's default JSON decoding, which +// respects UnmarshalJSON if it is defined on the type. +// - A nil value will not read the response body. +// +// For even greater flexibility, see [option.WithResponseInto] and +// [option.WithResponseBodyInto]. +func (r *Client) Execute(ctx context.Context, method string, path string, params any, res any, opts ...option.RequestOption) error { + opts = slices.Concat(r.Options, opts) + return requestconfig.ExecuteNewRequest(ctx, method, path, params, res, opts...) +} + +// Get makes a GET request with the given URL, params, and optionally deserializes +// to a response. See [Execute] documentation on the params and response. +func (r *Client) Get(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodGet, path, params, res, opts...) +} + +// Post makes a POST request with the given URL, params, and optionally +// deserializes to a response. See [Execute] documentation on the params and +// response. +func (r *Client) Post(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodPost, path, params, res, opts...) +} + +// Put makes a PUT request with the given URL, params, and optionally deserializes +// to a response. See [Execute] documentation on the params and response. +func (r *Client) Put(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodPut, path, params, res, opts...) +} + +// Patch makes a PATCH request with the given URL, params, and optionally +// deserializes to a response. See [Execute] documentation on the params and +// response. +func (r *Client) Patch(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodPatch, path, params, res, opts...) +} + +// Delete makes a DELETE request with the given URL, params, and optionally +// deserializes to a response. See [Execute] documentation on the params and +// response. +func (r *Client) Delete(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodDelete, path, params, res, opts...) +} diff --git a/interactions/client_test.go b/interactions/client_test.go new file mode 100644 index 00000000..f465e182 --- /dev/null +++ b/interactions/client_test.go @@ -0,0 +1,337 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package interactions_test + +import ( + "context" + "fmt" + "io" + "net/http" + "testing" + "time" + + "google.golang.org/genai/interactions" + "google.golang.org/genai/interactions/internal" + "google.golang.org/genai/interactions/option" +) + +type closureTransport struct { + fn func(req *http.Request) (*http.Response, error) +} + +func (t *closureTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return t.fn(req) +} + +func TestUserAgentHeader(t *testing.T) { + var userAgent string + client := interactions.NewClient( + option.WithAPIKey("My API Key"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + userAgent = req.Header.Get("User-Agent") + return &http.Response{ + StatusCode: http.StatusOK, + }, nil + }, + }, + }), + ) + _, _ = client.Interactions.NewModel(context.Background(), interactions.NewModelParams{ + Input: interactions.Input{ + String: "Tell me a joke", + }, + Model: "gemini-3-flash-preview", + }) + if userAgent != fmt.Sprintf("GeminiNextGenAPI/Go %s", internal.PackageVersion) { + t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent) + } +} + +func TestRetryAfter(t *testing.T) { + attempts := 0 + client := interactions.NewClient( + option.WithAPIKey("My API Key"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + attempts += 1 + return &http.Response{ + StatusCode: http.StatusTooManyRequests, + Header: http.Header{ + http.CanonicalHeaderKey("Retry-After"): []string{"0.1"}, + }, + }, nil + }, + }, + }), + ) + _, err := client.Interactions.NewModel(context.Background(), interactions.NewModelParams{ + Input: interactions.Input{ + String: "Tell me a joke", + }, + Model: "gemini-3-flash-preview", + }) + if err == nil { + t.Error("Expected there to be a cancel error") + } + + if attempts != 3 { + t.Errorf("Expected %d attempts, got %d", 3, attempts) + } +} + +func TestRetryAfterMs(t *testing.T) { + attempts := 0 + client := interactions.NewClient( + option.WithAPIKey("My API Key"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + attempts++ + return &http.Response{ + StatusCode: http.StatusTooManyRequests, + Header: http.Header{ + http.CanonicalHeaderKey("Retry-After-Ms"): []string{"100"}, + }, + }, nil + }, + }, + }), + ) + _, err := client.Interactions.NewModel(context.Background(), interactions.NewModelParams{ + Input: interactions.Input{ + String: "Tell me a joke", + }, + Model: "gemini-3-flash-preview", + }) + if err == nil { + t.Error("Expected there to be a cancel error") + } + if want := 3; attempts != want { + t.Errorf("Expected %d attempts, got %d", want, attempts) + } +} + +func TestContextCancel(t *testing.T) { + client := interactions.NewClient( + option.WithAPIKey("My API Key"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + <-req.Context().Done() + return nil, req.Context().Err() + }, + }, + }), + ) + cancelCtx, cancel := context.WithCancel(context.Background()) + cancel() + _, err := client.Interactions.NewModel(cancelCtx, interactions.NewModelParams{ + Input: interactions.Input{ + String: "Tell me a joke", + }, + Model: "gemini-3-flash-preview", + }) + if err == nil { + t.Error("Expected there to be a cancel error") + } +} + +func TestContextCancelDelay(t *testing.T) { + client := interactions.NewClient( + option.WithAPIKey("My API Key"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + <-req.Context().Done() + return nil, req.Context().Err() + }, + }, + }), + ) + cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond) + defer cancel() + _, err := client.Interactions.NewModel(cancelCtx, interactions.NewModelParams{ + Input: interactions.Input{ + String: "Tell me a joke", + }, + Model: "gemini-3-flash-preview", + }) + if err == nil { + t.Error("expected there to be a cancel error") + } +} + +func TestContextDeadline(t *testing.T) { + testTimeout := time.After(3 * time.Second) + testDone := make(chan struct{}) + + deadline := time.Now().Add(100 * time.Millisecond) + deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline) + defer cancel() + + go func() { + client := interactions.NewClient( + option.WithAPIKey("My API Key"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + <-req.Context().Done() + return nil, req.Context().Err() + }, + }, + }), + ) + _, err := client.Interactions.NewModel(deadlineCtx, interactions.NewModelParams{ + Input: interactions.Input{ + String: "Tell me a joke", + }, + Model: "gemini-3-flash-preview", + }) + if err == nil { + t.Error("expected there to be a deadline error") + } + close(testDone) + }() + + select { + case <-testTimeout: + t.Fatal("client didn't finish in time") + case <-testDone: + if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff { + t.Fatalf("client did not return within 30ms of context deadline, got %s", diff) + } + } +} + +func TestContextDeadlineStreaming(t *testing.T) { + testTimeout := time.After(3 * time.Second) + testDone := make(chan struct{}) + + deadline := time.Now().Add(100 * time.Millisecond) + deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline) + defer cancel() + + go func() { + client := interactions.NewClient( + option.WithAPIKey("My API Key"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Status: "200 OK", + Body: io.NopCloser( + io.Reader(readerFunc(func([]byte) (int, error) { + <-req.Context().Done() + return 0, req.Context().Err() + })), + ), + }, nil + }, + }, + }), + ) + stream := client.Interactions.NewModelStreaming(deadlineCtx, interactions.NewModelParams{ + Input: interactions.Input{ + TextContent: &interactions.TextContent{ + Text: "text", + }, + }, + Model: "gemini-2.5-computer-use-preview-10-2025", + }) + for stream.Next() { + _ = stream.Current() + } + if stream.Err() == nil { + t.Error("expected there to be a deadline error") + } + close(testDone) + }() + + select { + case <-testTimeout: + t.Fatal("client didn't finish in time") + case <-testDone: + if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff { + t.Fatalf("client did not return within 30ms of context deadline, got %s", diff) + } + } +} + +func TestContextDeadlineStreamingWithRequestTimeout(t *testing.T) { + testTimeout := time.After(3 * time.Second) + testDone := make(chan struct{}) + deadline := time.Now().Add(100 * time.Millisecond) + + go func() { + client := interactions.NewClient( + option.WithAPIKey("My API Key"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Status: "200 OK", + Body: io.NopCloser( + io.Reader(readerFunc(func([]byte) (int, error) { + <-req.Context().Done() + return 0, req.Context().Err() + })), + ), + }, nil + }, + }, + }), + ) + stream := client.Interactions.NewModelStreaming( + context.Background(), + interactions.NewModelParams{ + Input: interactions.Input{ + TextContent: &interactions.TextContent{ + Text: "text", + }, + }, + Model: "gemini-2.5-computer-use-preview-10-2025", + }, + option.WithRequestTimeout((100 * time.Millisecond)), + ) + for stream.Next() { + _ = stream.Current() + } + if stream.Err() == nil { + t.Error("expected there to be a deadline error") + } + close(testDone) + }() + + select { + case <-testTimeout: + t.Fatal("client didn't finish in time") + case <-testDone: + if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff { + t.Fatalf("client did not return within 30ms of context deadline, got %s", diff) + } + } +} + +type readerFunc func([]byte) (int, error) + +func (f readerFunc) Read(p []byte) (int, error) { return f(p) } +func (f readerFunc) Close() error { return nil } diff --git a/interactions/default_http_client.go b/interactions/default_http_client.go new file mode 100644 index 00000000..b29352a6 --- /dev/null +++ b/interactions/default_http_client.go @@ -0,0 +1,44 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package interactions + +import ( + "net/http" + "time" +) + +// defaultResponseHeaderTimeout bounds the time between a fully written request +// and the server's response headers. It does not apply to the response body, +// so long-running streams are unaffected. Without this, a server that accepts +// the connection but never responds would hang the request indefinitely. +const defaultResponseHeaderTimeout = 10 * time.Minute + +// defaultHTTPClient returns an [*http.Client] used when the caller does not +// supply one via [option.WithHTTPClient]. When [http.DefaultTransport] is the +// stdlib [*http.Transport], it is cloned and a [http.Transport.ResponseHeaderTimeout] +// is set so stuck connections fail fast instead of compounding across retries. +// If [http.DefaultTransport] has been wrapped (for example by otelhttp for +// distributed tracing), the wrapping is preserved and the header timeout is +// skipped. +func defaultHTTPClient() *http.Client { + if t, ok := http.DefaultTransport.(*http.Transport); ok { + t = t.Clone() + t.ResponseHeaderTimeout = defaultResponseHeaderTimeout + return &http.Client{Transport: t} + } + return &http.Client{Transport: http.DefaultTransport} +} diff --git a/interactions/field.go b/interactions/field.go new file mode 100644 index 00000000..511a4488 --- /dev/null +++ b/interactions/field.go @@ -0,0 +1,49 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package interactions + +import ( + "io" + "time" +) + +func Int(v int64) *int64 { return &v } +func Bool(v bool) *bool { return &v } +func Float(v float64) *float64 { return &v } +func String(v string) *string { return &v } +func Time(v time.Time) *time.Time { return &v } + +func File(rdr io.Reader, filename string, contentType string) file { + return file{rdr, filename, contentType} +} + +type file struct { + io.Reader + name string + contentType string +} + +func (f file) Filename() string { + if f.name != "" { + return f.name + } else if named, ok := f.Reader.(interface{ Name() string }); ok { + return named.Name() + } + return "" +} + +func (f file) ContentType() string { + return f.contentType +} diff --git a/interactions/interaction.go b/interactions/interaction.go new file mode 100644 index 00000000..8698d39e --- /dev/null +++ b/interactions/interaction.go @@ -0,0 +1,3719 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package interactions + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "slices" + "time" + + "google.golang.org/genai/interactions/internal/apijson" + "google.golang.org/genai/interactions/internal/apijson/unmarshalinfo" + "google.golang.org/genai/interactions/internal/apiquery" + "google.golang.org/genai/interactions/internal/requestconfig" + "google.golang.org/genai/interactions/option" + "google.golang.org/genai/interactions/packages/apidata" + "google.golang.org/genai/interactions/packages/ssestream" + "google.golang.org/genai/interactions/shared/constant" +) + +// InteractionService contains methods and other services that help with +// interacting with the gemini-next-gen-api API. +// +// Experimental: This service is experimental and may change in future versions. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewInteractionService] method instead. +type InteractionService struct { + Options []option.RequestOption +} + +// NewInteractionService generates a new service that applies the given options to +// each request. These options are applied after the parent client's options (if +// there is one), and before any request-specific options. +func NewInteractionService(opts ...option.RequestOption) (r InteractionService) { + r = InteractionService{} + r.Options = opts + return +} + +// Deletes the interaction by id. +func (r *InteractionService) Delete(ctx context.Context, id string, body DeleteParams, opts ...option.RequestOption) (res *DeleteResponse, err error) { + opts = slices.Concat(r.Options, opts) + precfg, err := requestconfig.PreRequestOptions(opts...) + if err != nil { + return nil, err + } + if body.APIVersion == "" && precfg.APIVersion != nil { + body.APIVersion = *precfg.APIVersion + } + if body.APIVersion == "" { + err = errors.New("missing required api_version parameter") + return nil, err + } + if id == "" { + err = errors.New("missing required id parameter") + return nil, err + } + path := fmt.Sprintf("%s/interactions/%s", url.PathEscape(body.APIVersion), url.PathEscape(id)) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...) + return res, err +} + +// Cancels an interaction by id. This only applies to background interactions that +// are still running. +func (r *InteractionService) Cancel(ctx context.Context, id string, body CancelParams, opts ...option.RequestOption) (res *Interaction, err error) { + opts = slices.Concat(r.Options, opts) + precfg, err := requestconfig.PreRequestOptions(opts...) + if err != nil { + return nil, err + } + if body.APIVersion == "" && precfg.APIVersion != nil { + body.APIVersion = *precfg.APIVersion + } + if body.APIVersion == "" { + err = errors.New("missing required api_version parameter") + return nil, err + } + if id == "" { + err = errors.New("missing required id parameter") + return nil, err + } + path := fmt.Sprintf("%s/interactions/%s/cancel", url.PathEscape(body.APIVersion), url.PathEscape(id)) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) + return res, err +} + +// Creates a new interaction. +func (r *InteractionService) NewAgent(ctx context.Context, params NewAgentParams, opts ...option.RequestOption) (res *Interaction, err error) { + opts = slices.Concat(r.Options, opts) + precfg, err := requestconfig.PreRequestOptions(opts...) + if err != nil { + return nil, err + } + if params.APIVersion == "" && precfg.APIVersion != nil { + params.APIVersion = *precfg.APIVersion + } + if params.APIVersion == "" { + err = errors.New("missing required api_version parameter") + return nil, err + } + path := fmt.Sprintf("%s/interactions?agent", url.PathEscape(params.APIVersion)) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) + return res, err +} + +// Creates a new interaction. +func (r *InteractionService) NewAgentStreaming(ctx context.Context, params NewAgentParams, opts ...option.RequestOption) (stream *ssestream.Stream[InteractionSSEEvent]) { + var ( + raw *http.Response + err error + ) + opts = slices.Concat(r.Options, opts) + opts = append(opts, option.WithJSONSet("stream", true)) + precfg, err := requestconfig.PreRequestOptions(opts...) + if err != nil { + return ssestream.NewStream[InteractionSSEEvent](nil, err) + } + if params.APIVersion == "" && precfg.APIVersion != nil { + params.APIVersion = *precfg.APIVersion + } + if params.APIVersion == "" { + err = errors.New("missing required api_version parameter") + return ssestream.NewStream[InteractionSSEEvent](nil, err) + } + path := fmt.Sprintf("%s/interactions?agent", url.PathEscape(params.APIVersion)) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &raw, opts...) + return ssestream.NewStream[InteractionSSEEvent](ssestream.NewDecoder(raw), err) +} + +// Creates a new interaction. +func (r *InteractionService) NewModel(ctx context.Context, params NewModelParams, opts ...option.RequestOption) (res *Interaction, err error) { + opts = slices.Concat(r.Options, opts) + precfg, err := requestconfig.PreRequestOptions(opts...) + if err != nil { + return nil, err + } + if params.APIVersion == "" && precfg.APIVersion != nil { + params.APIVersion = *precfg.APIVersion + } + if params.APIVersion == "" { + err = errors.New("missing required api_version parameter") + return nil, err + } + path := fmt.Sprintf("%s/interactions?model", url.PathEscape(params.APIVersion)) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) + return res, err +} + +// Creates a new interaction. +func (r *InteractionService) NewModelStreaming(ctx context.Context, params NewModelParams, opts ...option.RequestOption) (stream *ssestream.Stream[InteractionSSEEvent]) { + var ( + raw *http.Response + err error + ) + opts = slices.Concat(r.Options, opts) + opts = append(opts, option.WithJSONSet("stream", true)) + precfg, err := requestconfig.PreRequestOptions(opts...) + if err != nil { + return ssestream.NewStream[InteractionSSEEvent](nil, err) + } + if params.APIVersion == "" && precfg.APIVersion != nil { + params.APIVersion = *precfg.APIVersion + } + if params.APIVersion == "" { + err = errors.New("missing required api_version parameter") + return ssestream.NewStream[InteractionSSEEvent](nil, err) + } + path := fmt.Sprintf("%s/interactions?model", url.PathEscape(params.APIVersion)) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &raw, opts...) + return ssestream.NewStream[InteractionSSEEvent](ssestream.NewDecoder(raw), err) +} + +// Retrieves the full details of a single interaction based on its +// `Interaction.id`. +func (r *InteractionService) Get(ctx context.Context, id string, params GetParams, opts ...option.RequestOption) (res *Interaction, err error) { + opts = slices.Concat(r.Options, opts) + precfg, err := requestconfig.PreRequestOptions(opts...) + if err != nil { + return nil, err + } + if params.APIVersion == "" && precfg.APIVersion != nil { + params.APIVersion = *precfg.APIVersion + } + if params.APIVersion == "" { + err = errors.New("missing required api_version parameter") + return nil, err + } + if id == "" { + err = errors.New("missing required id parameter") + return nil, err + } + path := fmt.Sprintf("%s/interactions/%s", url.PathEscape(params.APIVersion), url.PathEscape(id)) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, params, &res, opts...) + return res, err +} + +// Retrieves the full details of a single interaction based on its +// `Interaction.id`. +func (r *InteractionService) GetStreaming(ctx context.Context, id string, params GetParams, opts ...option.RequestOption) (stream *ssestream.Stream[InteractionSSEEvent]) { + var ( + raw *http.Response + err error + ) + opts = slices.Concat(r.Options, opts) + opts = append(opts, option.WithJSONSet("stream", true)) + precfg, err := requestconfig.PreRequestOptions(opts...) + if err != nil { + return ssestream.NewStream[InteractionSSEEvent](nil, err) + } + if params.APIVersion == "" && precfg.APIVersion != nil { + params.APIVersion = *precfg.APIVersion + } + if params.APIVersion == "" { + err = errors.New("missing required api_version parameter") + return ssestream.NewStream[InteractionSSEEvent](nil, err) + } + if id == "" { + err = errors.New("missing required id parameter") + return ssestream.NewStream[InteractionSSEEvent](nil, err) + } + path := fmt.Sprintf("%s/interactions/%s", url.PathEscape(params.APIVersion), url.PathEscape(id)) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, params, &raw, opts...) + return ssestream.NewStream[InteractionSSEEvent](ssestream.NewDecoder(raw), err) +} + +// The configuration for allowed tools. +type AllowedTools struct { + // The mode of the tool choice. + // + // Any of "auto", "any", "none", "validated". + Mode ToolChoiceType `json:"mode,omitzero"` + // The names of the allowed tools. + Tools []string `json:"tools,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Only one field in this union will be nonzero +type Annotation struct { + URLCitation *URLCitation `json:",omitzero,inline" discriminator:"url_citation"` + FileCitation *FileCitation `json:",omitzero,inline" discriminator:"file_citation"` + PlaceCitation *PlaceCitation `json:",omitzero,inline" discriminator:"place_citation"` + + metadata `api:"union"` +} + +func (u Annotation) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *Annotation) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalDiscriminatedUnion(data, "type", u, &u.metadata) +} + +type ArgumentsDelta struct { + Arguments string `json:"arguments,omitzero"` + // This field doesn't need to be set. + Type constant.ArgumentsDelta `json:"type" default:"arguments_delta"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// An audio content block. +type AudioContent struct { + // The number of audio channels. + Channels *int `json:"channels,omitzero"` + // The audio content. + Data string `json:"data,omitzero" format:"byte"` + // The mime type of the audio. + // + // Any of "audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", + // "audio/flac", "audio/mpeg", "audio/m4a", "audio/l16", "audio/opus", + // "audio/alaw", "audio/mulaw". + MimeType string `json:"mime_type,omitzero"` + // The sample rate of the audio. + SampleRate *int `json:"sample_rate,omitzero"` + // The URI of the audio. + Uri string `json:"uri,omitzero"` + // This field doesn't need to be set. + Type constant.Audio `json:"type" default:"audio"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type AudioDelta struct { + // The number of audio channels. + Channels *int `json:"channels,omitzero"` + Data string `json:"data,omitzero" format:"byte"` + // Any of "audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", + // "audio/flac", "audio/mpeg", "audio/m4a", "audio/l16", "audio/opus", + // "audio/alaw", "audio/mulaw". + MimeType string `json:"mime_type,omitzero"` + // Deprecated. Use sample_rate instead. The value is ignored. + // + // Deprecated: deprecated + Rate *int `json:"rate,omitzero"` + // The sample rate of the audio. + SampleRate *int `json:"sample_rate,omitzero"` + Uri string `json:"uri,omitzero"` + // This field doesn't need to be set. + Type constant.Audio `json:"type" default:"audio"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Configuration for audio output format. +type AudioResponseFormat struct { + // Bit rate in bits per second (bps). Only applicable for compressed formats (MP3, + // Opus). + BitRate *int `json:"bit_rate,omitzero"` + // The delivery mode for the audio output. + // + // Any of "inline", "uri". + Delivery string `json:"delivery,omitzero"` + // The MIME type of the audio output. + // + // Any of "audio/mp3", "audio/ogg_opus", "audio/l16", "audio/wav", "audio/alaw", + // "audio/mulaw". + MimeType string `json:"mime_type,omitzero"` + // Sample rate in Hz. + SampleRate *int `json:"sample_rate,omitzero"` + // This field doesn't need to be set. + Type constant.Audio `json:"type" default:"audio"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The arguments to pass to the code execution. +type CodeExecutionCallArguments struct { + // The code to be executed. + Code string `json:"code,omitzero"` + // Programming language of the `code`. + // + // Any of "python". + Language string `json:"language,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type CodeExecutionCallDelta struct { + // The arguments to pass to the code execution. + Arguments CodeExecutionCallArguments `json:"arguments" api:"required"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.CodeExecutionCall `json:"type" default:"code_execution_call"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Code execution call step. +type CodeExecutionCallStep struct { + // Required. A unique ID for this specific tool call. + ID string `json:"id" api:"required"` + // Required. The arguments to pass to the code execution. + Arguments CodeExecutionCallStepArguments `json:"arguments" api:"required"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.CodeExecutionCall `json:"type" default:"code_execution_call"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Required. The arguments to pass to the code execution. +type CodeExecutionCallStepArguments struct { + // The code to be executed. + Code string `json:"code,omitzero"` + // Programming language of the `code`. + // + // Any of "python". + Language string `json:"language,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type CodeExecutionResultDelta struct { + Result string `json:"result" api:"required"` + IsError *bool `json:"is_error,omitzero"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.CodeExecutionResult `json:"type" default:"code_execution_result"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Code execution result step. +type CodeExecutionResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id" api:"required"` + // Required. The output of the code execution. + Result string `json:"result" api:"required"` + // Whether the code execution resulted in an error. + IsError *bool `json:"is_error,omitzero"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.CodeExecutionResult `json:"type" default:"code_execution_result"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Only one field in this union will be nonzero +type Content struct { + Text *TextContent `json:",omitzero,inline" discriminator:"text"` + Image *ImageContent `json:",omitzero,inline" discriminator:"image"` + Audio *AudioContent `json:",omitzero,inline" discriminator:"audio"` + Document *DocumentContent `json:",omitzero,inline" discriminator:"document"` + Video *VideoContent `json:",omitzero,inline" discriminator:"video"` + + metadata `api:"union"` +} + +func (u Content) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *Content) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalDiscriminatedUnion(data, "type", u, &u.metadata) +} + +// Configuration for the Deep Research agent. +type DeepResearchAgentConfig struct { + // Enables human-in-the-loop planning for the Deep Research agent. If set to true, + // the Deep Research agent will provide a research plan in its response. The agent + // will then proceed only if the user confirms the plan in the next turn. + CollaborativePlanning *bool `json:"collaborative_planning,omitzero"` + // Whether to include thought summaries in the response. + // + // Any of "auto", "none". + ThinkingSummaries string `json:"thinking_summaries,omitzero"` + // Whether to include visualizations in the response. + // + // Any of "off", "auto". + Visualization string `json:"visualization,omitzero"` + // This field doesn't need to be set. + Type constant.DeepResearch `json:"type" default:"deep-research"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A document content block. +type DocumentContent struct { + // The document content. + Data string `json:"data,omitzero" format:"byte"` + // The mime type of the document. + // + // Any of "application/pdf". + MimeType string `json:"mime_type,omitzero"` + // The URI of the document. + Uri string `json:"uri,omitzero"` + // This field doesn't need to be set. + Type constant.Document `json:"type" default:"document"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type DocumentDelta struct { + Data string `json:"data,omitzero" format:"byte"` + // Any of "application/pdf". + MimeType string `json:"mime_type,omitzero"` + Uri string `json:"uri,omitzero"` + // This field doesn't need to be set. + Type constant.Document `json:"type" default:"document"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Configuration for dynamic agents. +type DynamicAgentConfig struct { + // This field doesn't need to be set. + Type constant.Dynamic `json:"type" default:"dynamic"` + Fields map[string]any `json:",inline"` + + metadata +} + +type ErrorEvent struct { + // Error message from an interaction. + Error ErrorEventError `json:"error,omitzero"` + // The event_id token to be used to resume the interaction stream, from this event. + EventID string `json:"event_id,omitzero"` + // This field doesn't need to be set. + EventType constant.Error `json:"event_type" default:"error"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Error message from an interaction. +type ErrorEventError struct { + // A URI that identifies the error type. + Code string `json:"code,omitzero"` + // A human-readable error message. + Message string `json:"message,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A file citation annotation. +type FileCitation struct { + // User provided metadata about the retrieved context. + CustomMetadata map[string]any `json:"custom_metadata,omitzero"` + // The URI of the file. + DocumentUri string `json:"document_uri,omitzero"` + // End of the attributed segment, exclusive. + EndIndex *int `json:"end_index,omitzero"` + // The name of the file. + FileName string `json:"file_name,omitzero"` + // Media ID in-case of image citations, if applicable. + MediaID string `json:"media_id,omitzero"` + // Page number of the cited document, if applicable. + PageNumber *int `json:"page_number,omitzero"` + // Source attributed for a portion of the text. + Source string `json:"source,omitzero"` + // Start of segment of the response that is attributed to this source. + // + // Index indicates the start of the segment, measured in bytes. + StartIndex *int `json:"start_index,omitzero"` + // This field doesn't need to be set. + Type constant.FileCitation `json:"type" default:"file_citation"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type FileSearchCallDelta struct { + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.FileSearchCall `json:"type" default:"file_search_call"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// File Search call step. +type FileSearchCallStep struct { + // Required. A unique ID for this specific tool call. + ID string `json:"id" api:"required"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.FileSearchCall `json:"type" default:"file_search_call"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type FileSearchResult = any + +type FileSearchResultDelta struct { + Result []FileSearchResult `json:"result" api:"required"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.FileSearchResult `json:"type" default:"file_search_result"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// File Search result step. +type FileSearchResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id" api:"required"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.FileSearchResult `json:"type" default:"file_search_result"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A tool that can be used by the model. +type Function struct { + // A description of the function. + Description string `json:"description,omitzero"` + // The name of the function. + Name string `json:"name,omitzero"` + // The JSON Schema for the function's parameters. + Parameters any `json:"parameters,omitzero"` + // This field doesn't need to be set. + Type constant.Function `json:"type" default:"function"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type FunctionCallDelta struct { + // Required. A unique ID for this specific tool call. + ID string `json:"id" api:"required"` + Arguments map[string]any `json:"arguments" api:"required"` + Name string `json:"name" api:"required"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.FunctionCall `json:"type" default:"function_call"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A function tool call step. +type FunctionCallStep struct { + // Required. A unique ID for this specific tool call. + ID string `json:"id" api:"required"` + // Required. The arguments to pass to the function. + Arguments map[string]any `json:"arguments" api:"required"` + // Required. The name of the tool to call. + Name string `json:"name" api:"required"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.FunctionCall `json:"type" default:"function_call"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type FunctionResultDelta struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id" api:"required"` + Result FunctionResultDeltaResult `json:"result" api:"required"` + IsError *bool `json:"is_error,omitzero"` + Name string `json:"name,omitzero"` + // This field doesn't need to be set. + Type constant.FunctionResult `json:"type" default:"function_result"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Only one field in this union will be nonzero +type FunctionResultDeltaResult struct { + FunctionResultSubcontentList []FunctionResultDeltaResultFunctionResultSubcontentListItem `json:",omitzero,inline"` + String string `json:",omitzero,inline"` + + metadata `api:"union"` +} + +func (u FunctionResultDeltaResult) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *FunctionResultDeltaResult) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalUnion(data, u, &u.metadata) +} + +// Only one field in this union will be nonzero +type FunctionResultDeltaResultFunctionResultSubcontentListItem struct { + Text *TextContent `json:",omitzero,inline" discriminator:"text"` + Image *ImageContent `json:",omitzero,inline" discriminator:"image"` + + metadata `api:"union"` +} + +func (u FunctionResultDeltaResultFunctionResultSubcontentListItem) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *FunctionResultDeltaResultFunctionResultSubcontentListItem) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalDiscriminatedUnion(data, "type", u, &u.metadata) +} + +// Result of a function tool call. +type FunctionResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id" api:"required"` + // The result of the tool call. + Result FunctionResultStepResult `json:"result" api:"required"` + // Whether the tool call resulted in an error. + IsError *bool `json:"is_error,omitzero"` + // The name of the tool that was called. + Name string `json:"name,omitzero"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.FunctionResult `json:"type" default:"function_result"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Only one field in this union will be nonzero +type FunctionResultStepResult struct { + FunctionResultSubcontentList []FunctionResultStepResultFunctionResultSubcontentListItem `json:",omitzero,inline"` + String string `json:",omitzero,inline"` + + metadata `api:"union"` +} + +func (u FunctionResultStepResult) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *FunctionResultStepResult) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalUnion(data, u, &u.metadata) +} + +// Only one field in this union will be nonzero +type FunctionResultStepResultFunctionResultSubcontentListItem struct { + Text *TextContent `json:",omitzero,inline" discriminator:"text"` + Image *ImageContent `json:",omitzero,inline" discriminator:"image"` + + metadata `api:"union"` +} + +func (u FunctionResultStepResultFunctionResultSubcontentListItem) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *FunctionResultStepResultFunctionResultSubcontentListItem) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalDiscriminatedUnion(data, "type", u, &u.metadata) +} + +// Configuration parameters for model interactions. +type GenerationConfig struct { + // Configuration for image interaction. + ImageConfig ImageConfig `json:"image_config,omitzero"` + // The maximum number of tokens to include in the response. + MaxOutputTokens *int `json:"max_output_tokens,omitzero"` + // Seed used in decoding for reproducibility. + Seed *int `json:"seed,omitzero"` + // Configuration for speech interaction. + SpeechConfig []SpeechConfig `json:"speech_config,omitzero"` + // A list of character sequences that will stop output interaction. + StopSequences []string `json:"stop_sequences,omitzero"` + // Controls the randomness of the output. + Temperature *float64 `json:"temperature,omitzero"` + // The level of thought tokens that the model should generate. + // + // Any of "minimal", "low", "medium", "high". + ThinkingLevel ThinkingLevel `json:"thinking_level,omitzero"` + // Whether to include thought summaries in the response. + // + // Any of "auto", "none". + ThinkingSummaries string `json:"thinking_summaries,omitzero"` + // The tool choice configuration. + ToolChoice *GenerationConfigToolChoice `json:"tool_choice,omitzero"` + // The maximum cumulative probability of tokens to consider when sampling. + TopP *float64 `json:"top_p,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Only one field in this union will be nonzero +type GenerationConfigToolChoice struct { + ToolChoiceType ToolChoiceType `json:",omitzero,inline"` + ToolChoiceConfig *ToolChoiceConfig `json:",omitzero,inline"` + + metadata `api:"union"` +} + +func (u GenerationConfigToolChoice) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *GenerationConfigToolChoice) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalUnion(data, u, &u.metadata) +} + +// The arguments to pass to the Google Maps tool. +type GoogleMapsCallArguments struct { + // The queries to be executed. + Queries []string `json:"queries,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type GoogleMapsCallDelta struct { + // The arguments to pass to the Google Maps tool. + Arguments GoogleMapsCallArguments `json:"arguments,omitzero"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.GoogleMapsCall `json:"type" default:"google_maps_call"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Google Maps call step. +type GoogleMapsCallStep struct { + // Required. A unique ID for this specific tool call. + ID string `json:"id" api:"required"` + // The arguments to pass to the Google Maps tool. + Arguments GoogleMapsCallStepArguments `json:"arguments,omitzero"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.GoogleMapsCall `json:"type" default:"google_maps_call"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The arguments to pass to the Google Maps tool. +type GoogleMapsCallStepArguments struct { + // The queries to be executed. + Queries []string `json:"queries,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The result of the Google Maps. +type GoogleMapsResult struct { + // The places that were found. + Places []GoogleMapsResultPlace `json:"places,omitzero"` + // Resource name of the Google Maps widget context token. + WidgetContextToken string `json:"widget_context_token,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type GoogleMapsResultPlace struct { + // Title of the place. + Name string `json:"name,omitzero"` + // The ID of the place, in `places/{place_id}` format. + PlaceID string `json:"place_id,omitzero"` + // Snippets of reviews that are used to generate answers about the features of a + // given place in Google Maps. + ReviewSnippets []GoogleMapsResultPlaceReviewSnippet `json:"review_snippets,omitzero"` + // URI reference of the place. + URL string `json:"url,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Encapsulates a snippet of a user review that answers a question about the +// features of a specific place in Google Maps. +type GoogleMapsResultPlaceReviewSnippet struct { + // The ID of the review snippet. + ReviewID string `json:"review_id,omitzero"` + // Title of the review. + Title string `json:"title,omitzero"` + // A link that corresponds to the user review on Google Maps. + URL string `json:"url,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type GoogleMapsResultDelta struct { + // The results of the Google Maps. + Result []GoogleMapsResult `json:"result,omitzero"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.GoogleMapsResult `json:"type" default:"google_maps_result"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Google Maps result step. +type GoogleMapsResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id" api:"required"` + Result []GoogleMapsResultStepResult `json:"result" api:"required"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.GoogleMapsResult `json:"type" default:"google_maps_result"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The result of the Google Maps. +type GoogleMapsResultStepResult struct { + Places []GoogleMapsResultStepResultPlace `json:"places,omitzero"` + WidgetContextToken string `json:"widget_context_token,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type GoogleMapsResultStepResultPlace struct { + Name string `json:"name,omitzero"` + PlaceID string `json:"place_id,omitzero"` + ReviewSnippets []GoogleMapsResultStepResultPlaceReviewSnippet `json:"review_snippets,omitzero"` + URL string `json:"url,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Encapsulates a snippet of a user review that answers a question about the +// features of a specific place in Google Maps. +type GoogleMapsResultStepResultPlaceReviewSnippet struct { + // The ID of the review snippet. + ReviewID string `json:"review_id,omitzero"` + // Title of the review. + Title string `json:"title,omitzero"` + // A link that corresponds to the user review on Google Maps. + URL string `json:"url,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The arguments to pass to Google Search. +type GoogleSearchCallArguments struct { + // Web search queries for the following-up web search. + Queries []string `json:"queries,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type GoogleSearchCallDelta struct { + // The arguments to pass to Google Search. + Arguments GoogleSearchCallArguments `json:"arguments" api:"required"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.GoogleSearchCall `json:"type" default:"google_search_call"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Google Search call step. +type GoogleSearchCallStep struct { + // Required. A unique ID for this specific tool call. + ID string `json:"id" api:"required"` + // Required. The arguments to pass to Google Search. + Arguments GoogleSearchCallStepArguments `json:"arguments" api:"required"` + // The type of search grounding enabled. + // + // Any of "web_search", "image_search", "enterprise_web_search". + SearchType string `json:"search_type,omitzero"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.GoogleSearchCall `json:"type" default:"google_search_call"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Required. The arguments to pass to Google Search. +type GoogleSearchCallStepArguments struct { + // Web search queries for the following-up web search. + Queries []string `json:"queries,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The result of the Google Search. +type GoogleSearchResult struct { + // Web content snippet that can be embedded in a web page or an app webview. + SearchSuggestions string `json:"search_suggestions,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type GoogleSearchResultDelta struct { + Result []GoogleSearchResult `json:"result" api:"required"` + IsError *bool `json:"is_error,omitzero"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.GoogleSearchResult `json:"type" default:"google_search_result"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Google Search result step. +type GoogleSearchResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id" api:"required"` + // Required. The results of the Google Search. + Result []GoogleSearchResultStepResult `json:"result" api:"required"` + // Whether the Google Search resulted in an error. + IsError *bool `json:"is_error,omitzero"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.GoogleSearchResult `json:"type" default:"google_search_result"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The result of the Google Search. +type GoogleSearchResultStepResult struct { + // Web content snippet that can be embedded in a web page or an app webview. + SearchSuggestions string `json:"search_suggestions,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The configuration for image interaction. +type ImageConfig struct { + // Any of "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", + // "1:8", "8:1", "1:4", "4:1". + AspectRatio string `json:"aspect_ratio,omitzero"` + // Any of "1K", "2K", "4K", "512". + ImageSize string `json:"image_size,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// An image content block. +type ImageContent struct { + // The image content. + Data string `json:"data,omitzero" format:"byte"` + // The mime type of the image. + // + // Any of "image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", + // "image/gif", "image/bmp", "image/tiff". + MimeType string `json:"mime_type,omitzero"` + // The resolution of the media. + // + // Any of "low", "medium", "high", "ultra_high". + Resolution string `json:"resolution,omitzero"` + // The URI of the image. + Uri string `json:"uri,omitzero"` + // This field doesn't need to be set. + Type constant.Image `json:"type" default:"image"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type ImageDelta struct { + Data string `json:"data,omitzero" format:"byte"` + // Any of "image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", + // "image/gif", "image/bmp", "image/tiff". + MimeType string `json:"mime_type,omitzero"` + // The resolution of the media. + // + // Any of "low", "medium", "high", "ultra_high". + Resolution string `json:"resolution,omitzero"` + Uri string `json:"uri,omitzero"` + // This field doesn't need to be set. + Type constant.Image `json:"type" default:"image"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Configuration for image output format. +type ImageResponseFormat struct { + // The aspect ratio for the image output. + // + // Any of "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", + // "1:8", "8:1", "1:4", "4:1". + AspectRatio string `json:"aspect_ratio,omitzero"` + // The delivery mode for the image output. + // + // Any of "inline", "uri". + Delivery string `json:"delivery,omitzero"` + // The size of the image output. + // + // Any of "512", "1K", "2K", "4K". + ImageSize string `json:"image_size,omitzero"` + // The MIME type of the image output. + // + // Any of "image/jpeg". + MimeType string `json:"mime_type,omitzero"` + // This field doesn't need to be set. + Type constant.Image `json:"type" default:"image"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Only one field in this union will be nonzero +type Input struct { + String string `json:",omitzero,inline"` + StepList []Step `json:",omitzero,inline"` + ContentList []Content `json:",omitzero,inline"` + TextContent *TextContent `json:",omitzero,inline"` + ImageContent *ImageContent `json:",omitzero,inline"` + AudioContent *AudioContent `json:",omitzero,inline"` + DocumentContent *DocumentContent `json:",omitzero,inline"` + VideoContent *VideoContent `json:",omitzero,inline"` + + metadata `api:"union"` +} + +func (u Input) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *Input) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalUnion(data, u, &u.metadata) +} + +// The Interaction resource. +type Interaction struct { + // Required. Output only. A unique identifier for the interaction completion. + ID string `json:"id" api:"required"` + // Required. Output only. The time at which the response was created in ISO 8601 + // format (YYYY-MM-DDThh:mm:ssZ). + Created time.Time `json:"created" api:"required" format:"date-time"` + // Required. Output only. The status of the interaction. + // + // Any of "in_progress", "requires_action", "completed", "failed", "cancelled", + // "incomplete". + Status string `json:"status" api:"required"` + // Required. Output only. The steps that make up the interaction. + Steps []Step `json:"steps" api:"required"` + // Required. Output only. The time at which the response was last updated in ISO + // 8601 format (YYYY-MM-DDThh:mm:ssZ). + Updated time.Time `json:"updated" api:"required" format:"date-time"` + // The name of the `Agent` used for generating the interaction. + Agent string `json:"agent,omitzero"` + // Configuration parameters for the agent interaction. + AgentConfig *InteractionAgentConfig `json:"agent_config,omitzero"` + // Input only. Configuration parameters for the model interaction. + GenerationConfig GenerationConfig `json:"generation_config,omitzero"` + // The input for the interaction. + Input *Input `json:"input,omitzero"` + // The name of the `Model` used for generating the interaction. + Model string `json:"model,omitzero"` + // The ID of the previous interaction, if any. + PreviousInteractionID string `json:"previous_interaction_id,omitzero"` + // Enforces that the generated response is a JSON object that complies with the + // JSON schema specified in this field. + ResponseFormat *InteractionResponseFormat `json:"response_format,omitzero"` + // The mime type of the response. This is required if response_format is set. + ResponseMimeType string `json:"response_mime_type,omitzero"` + // The requested modalities of the response (TEXT, IMAGE, AUDIO). + // + // Any of "text", "image", "audio", "video", "document". + ResponseModalities []string `json:"response_modalities,omitzero"` + // Output only. The role of the interaction. + // + // Deprecated: deprecated + Role string `json:"role,omitzero"` + // The service tier for the interaction. + // + // Any of "flex", "standard", "priority". + ServiceTier string `json:"service_tier,omitzero"` + // System instruction for the interaction. + SystemInstruction string `json:"system_instruction,omitzero"` + // A list of tool declarations the model may call during interaction. + Tools []Tool `json:"tools,omitzero"` + // Output only. Statistics on the interaction request's token usage. + Usage Usage `json:"usage,omitzero"` + // Optional. Webhook configuration for receiving notifications when the interaction + // completes. + WebhookConfig WebhookConfig `json:"webhook_config,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Only one field in this union will be nonzero +type InteractionAgentConfig struct { + Dynamic *DynamicAgentConfig `json:",omitzero,inline" discriminator:"dynamic"` + DeepResearch *DeepResearchAgentConfig `json:",omitzero,inline" discriminator:"deep-research"` + + metadata `api:"union"` +} + +func (u InteractionAgentConfig) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *InteractionAgentConfig) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalDiscriminatedUnion(data, "type", u, &u.metadata) +} + +// Only one field in this union will be nonzero +type InteractionResponseFormat struct { + ResponseFormatListArray ResponseFormatList `json:",omitzero,inline"` + AudioResponseFormat *AudioResponseFormat `json:",omitzero,inline"` + TextResponseFormat *TextResponseFormat `json:",omitzero,inline"` + ImageResponseFormat *ImageResponseFormat `json:",omitzero,inline"` + VideoResponseFormat *VideoResponseFormat `json:",omitzero,inline"` + + metadata `api:"union"` +} + +func (u InteractionResponseFormat) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *InteractionResponseFormat) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalUnion(data, u, &u.metadata) +} + +type InteractionCompletedEvent struct { + // Required. The completed interaction with empty outputs to reduce the payload + // size. Use the preceding ContentDelta events for the actual output. + Interaction Interaction `json:"interaction" api:"required"` + // The event_id token to be used to resume the interaction stream, from this event. + EventID string `json:"event_id,omitzero"` + // This field doesn't need to be set. + EventType constant.InteractionCompleted `json:"event_type" default:"interaction.completed"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type InteractionCreatedEvent struct { + // The Interaction resource. + Interaction Interaction `json:"interaction" api:"required"` + // The event_id token to be used to resume the interaction stream, from this event. + EventID string `json:"event_id,omitzero"` + // This field doesn't need to be set. + EventType constant.InteractionCreated `json:"event_type" default:"interaction.created"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Only one field in this union will be nonzero +type InteractionSSEEvent struct { + InteractionCreated *InteractionCreatedEvent `json:",omitzero,inline" discriminator:"interaction.created"` + InteractionCompleted *InteractionCompletedEvent `json:",omitzero,inline" discriminator:"interaction.completed"` + InteractionStatusUpdate *InteractionStatusUpdate `json:",omitzero,inline" discriminator:"interaction.status_update"` + Error *ErrorEvent `json:",omitzero,inline" discriminator:"error"` + StepStart *StepStart `json:",omitzero,inline" discriminator:"step.start"` + StepDelta *StepDelta `json:",omitzero,inline" discriminator:"step.delta"` + StepStop *StepStop `json:",omitzero,inline" discriminator:"step.stop"` + + metadata `api:"union"` +} + +func (u InteractionSSEEvent) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *InteractionSSEEvent) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalDiscriminatedUnion(data, "event_type", u, &u.metadata) +} + +type InteractionStatusUpdate struct { + InteractionID string `json:"interaction_id" api:"required"` + // Any of "in_progress", "requires_action", "completed", "failed", "cancelled", + // "incomplete". + Status string `json:"status" api:"required"` + // The event_id token to be used to resume the interaction stream, from this event. + EventID string `json:"event_id,omitzero"` + // This field doesn't need to be set. + EventType constant.InteractionStatusUpdate `json:"event_type" default:"interaction.status_update"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type MCPServerToolCallDelta struct { + Arguments map[string]any `json:"arguments" api:"required"` + Name string `json:"name" api:"required"` + ServerName string `json:"server_name" api:"required"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.MCPServerToolCall `json:"type" default:"mcp_server_tool_call"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// MCPServer tool call step. +type MCPServerToolCallStep struct { + // Required. A unique ID for this specific tool call. + ID string `json:"id" api:"required"` + // Required. The JSON object of arguments for the function. + Arguments map[string]any `json:"arguments" api:"required"` + // Required. The name of the tool which was called. + Name string `json:"name" api:"required"` + // Required. The name of the used MCP server. + ServerName string `json:"server_name" api:"required"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.MCPServerToolCall `json:"type" default:"mcp_server_tool_call"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type MCPServerToolResultDelta struct { + Result MCPServerToolResultDeltaResult `json:"result" api:"required"` + Name string `json:"name,omitzero"` + ServerName string `json:"server_name,omitzero"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.MCPServerToolResult `json:"type" default:"mcp_server_tool_result"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Only one field in this union will be nonzero +type MCPServerToolResultDeltaResult struct { + FunctionResultSubcontentList []MCPServerToolResultDeltaResultFunctionResultSubcontentListItem `json:",omitzero,inline"` + String string `json:",omitzero,inline"` + + metadata `api:"union"` +} + +func (u MCPServerToolResultDeltaResult) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *MCPServerToolResultDeltaResult) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalUnion(data, u, &u.metadata) +} + +// Only one field in this union will be nonzero +type MCPServerToolResultDeltaResultFunctionResultSubcontentListItem struct { + Text *TextContent `json:",omitzero,inline" discriminator:"text"` + Image *ImageContent `json:",omitzero,inline" discriminator:"image"` + + metadata `api:"union"` +} + +func (u MCPServerToolResultDeltaResultFunctionResultSubcontentListItem) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *MCPServerToolResultDeltaResultFunctionResultSubcontentListItem) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalDiscriminatedUnion(data, "type", u, &u.metadata) +} + +// MCPServer tool result step. +type MCPServerToolResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id" api:"required"` + // The output from the MCP server call. Can be simple text or rich content. + Result MCPServerToolResultStepResult `json:"result" api:"required"` + // Name of the tool which is called for this specific tool call. + Name string `json:"name,omitzero"` + // The name of the used MCP server. + ServerName string `json:"server_name,omitzero"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.MCPServerToolResult `json:"type" default:"mcp_server_tool_result"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Only one field in this union will be nonzero +type MCPServerToolResultStepResult struct { + String string `json:",omitzero,inline"` + FunctionResultSubcontentList []MCPServerToolResultStepResultFunctionResultSubcontentListItem `json:",omitzero,inline"` + + metadata `api:"union"` +} + +func (u MCPServerToolResultStepResult) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *MCPServerToolResultStepResult) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalUnion(data, u, &u.metadata) +} + +// Only one field in this union will be nonzero +type MCPServerToolResultStepResultFunctionResultSubcontentListItem struct { + Text *TextContent `json:",omitzero,inline" discriminator:"text"` + Image *ImageContent `json:",omitzero,inline" discriminator:"image"` + + metadata `api:"union"` +} + +func (u MCPServerToolResultStepResultFunctionResultSubcontentListItem) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *MCPServerToolResultStepResultFunctionResultSubcontentListItem) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalDiscriminatedUnion(data, "type", u, &u.metadata) +} + +// Only one field in this union will be nonzero +type Model struct { + Model string `json:",omitzero,inline"` + String string `json:",omitzero,inline"` + + metadata `api:"union"` +} + +func (u Model) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *Model) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalUnion(data, u, &u.metadata) +} + +// Output generated by the model. +type ModelOutputStep struct { + Content []Content `json:"content,omitzero"` + // This field doesn't need to be set. + Type constant.ModelOutput `json:"type" default:"model_output"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A place citation annotation. +type PlaceCitation struct { + // End of the attributed segment, exclusive. + EndIndex *int `json:"end_index,omitzero"` + // Title of the place. + Name string `json:"name,omitzero"` + // The ID of the place, in `places/{place_id}` format. + PlaceID string `json:"place_id,omitzero"` + // Snippets of reviews that are used to generate answers about the features of a + // given place in Google Maps. + ReviewSnippets []PlaceCitationReviewSnippet `json:"review_snippets,omitzero"` + // Start of segment of the response that is attributed to this source. + // + // Index indicates the start of the segment, measured in bytes. + StartIndex *int `json:"start_index,omitzero"` + // URI reference of the place. + URL string `json:"url,omitzero"` + // This field doesn't need to be set. + Type constant.PlaceCitation `json:"type" default:"place_citation"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Encapsulates a snippet of a user review that answers a question about the +// features of a specific place in Google Maps. +type PlaceCitationReviewSnippet struct { + // The ID of the review snippet. + ReviewID string `json:"review_id,omitzero"` + // Title of the review. + Title string `json:"title,omitzero"` + // A link that corresponds to the user review on Google Maps. + URL string `json:"url,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type ResponseFormatList []ResponseFormatListItem + +// Only one field in this union will be nonzero +type ResponseFormatListItem struct { + AudioResponseFormat *AudioResponseFormat `json:",omitzero,inline"` + TextResponseFormat *TextResponseFormat `json:",omitzero,inline"` + ImageResponseFormat *ImageResponseFormat `json:",omitzero,inline"` + VideoResponseFormat *VideoResponseFormat `json:",omitzero,inline"` + + metadata `api:"union"` +} + +func (u ResponseFormatListItem) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *ResponseFormatListItem) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalUnion(data, u, &u.metadata) +} + +// The configuration for speech interaction. +type SpeechConfig struct { + // The language of the speech. + Language string `json:"language,omitzero"` + // The speaker's name, it should match the speaker name given in the prompt. + Speaker string `json:"speaker,omitzero"` + // The voice of the speaker. + Voice string `json:"voice,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Only one field in this union will be nonzero +type Step struct { + UserInput *UserInputStep `json:",omitzero,inline" discriminator:"user_input"` + ModelOutput *ModelOutputStep `json:",omitzero,inline" discriminator:"model_output"` + Thought *ThoughtStep `json:",omitzero,inline" discriminator:"thought"` + FunctionCall *FunctionCallStep `json:",omitzero,inline" discriminator:"function_call"` + CodeExecutionCall *CodeExecutionCallStep `json:",omitzero,inline" discriminator:"code_execution_call"` + URLContextCall *URLContextCallStep `json:",omitzero,inline" discriminator:"url_context_call"` + MCPServerToolCall *MCPServerToolCallStep `json:",omitzero,inline" discriminator:"mcp_server_tool_call"` + GoogleSearchCall *GoogleSearchCallStep `json:",omitzero,inline" discriminator:"google_search_call"` + FileSearchCall *FileSearchCallStep `json:",omitzero,inline" discriminator:"file_search_call"` + GoogleMapsCall *GoogleMapsCallStep `json:",omitzero,inline" discriminator:"google_maps_call"` + FunctionResult *FunctionResultStep `json:",omitzero,inline" discriminator:"function_result"` + CodeExecutionResult *CodeExecutionResultStep `json:",omitzero,inline" discriminator:"code_execution_result"` + URLContextResult *URLContextResultStep `json:",omitzero,inline" discriminator:"url_context_result"` + GoogleSearchResult *GoogleSearchResultStep `json:",omitzero,inline" discriminator:"google_search_result"` + MCPServerToolResult *MCPServerToolResultStep `json:",omitzero,inline" discriminator:"mcp_server_tool_result"` + FileSearchResult *FileSearchResultStep `json:",omitzero,inline" discriminator:"file_search_result"` + GoogleMapsResult *GoogleMapsResultStep `json:",omitzero,inline" discriminator:"google_maps_result"` + + metadata `api:"union"` +} + +func (u Step) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *Step) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalDiscriminatedUnion(data, "type", u, &u.metadata) +} + +type StepDelta struct { + Delta StepDeltaDelta `json:"delta" api:"required"` + Index int `json:"index" api:"required"` + // The event_id token to be used to resume the interaction stream, from this event. + EventID string `json:"event_id,omitzero"` + // This field doesn't need to be set. + EventType constant.StepDelta `json:"event_type" default:"step.delta"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Only one field in this union will be nonzero +type StepDeltaDelta struct { + Text *TextDelta `json:",omitzero,inline" discriminator:"text"` + Image *ImageDelta `json:",omitzero,inline" discriminator:"image"` + Audio *AudioDelta `json:",omitzero,inline" discriminator:"audio"` + Document *DocumentDelta `json:",omitzero,inline" discriminator:"document"` + Video *VideoDelta `json:",omitzero,inline" discriminator:"video"` + ThoughtSummary *ThoughtSummaryDelta `json:",omitzero,inline" discriminator:"thought_summary"` + ThoughtSignature *ThoughtSignatureDelta `json:",omitzero,inline" discriminator:"thought_signature"` + TextAnnotationDelta *TextAnnotationDelta `json:",omitzero,inline" discriminator:"text_annotation_delta"` + ArgumentsDelta *ArgumentsDelta `json:",omitzero,inline" discriminator:"arguments_delta"` + CodeExecutionCall *CodeExecutionCallDelta `json:",omitzero,inline" discriminator:"code_execution_call"` + URLContextCall *URLContextCallDelta `json:",omitzero,inline" discriminator:"url_context_call"` + GoogleSearchCall *GoogleSearchCallDelta `json:",omitzero,inline" discriminator:"google_search_call"` + MCPServerToolCall *MCPServerToolCallDelta `json:",omitzero,inline" discriminator:"mcp_server_tool_call"` + FileSearchCall *FileSearchCallDelta `json:",omitzero,inline" discriminator:"file_search_call"` + GoogleMapsCall *GoogleMapsCallDelta `json:",omitzero,inline" discriminator:"google_maps_call"` + CodeExecutionResult *CodeExecutionResultDelta `json:",omitzero,inline" discriminator:"code_execution_result"` + URLContextResult *URLContextResultDelta `json:",omitzero,inline" discriminator:"url_context_result"` + GoogleSearchResult *GoogleSearchResultDelta `json:",omitzero,inline" discriminator:"google_search_result"` + MCPServerToolResult *MCPServerToolResultDelta `json:",omitzero,inline" discriminator:"mcp_server_tool_result"` + FileSearchResult *FileSearchResultDelta `json:",omitzero,inline" discriminator:"file_search_result"` + GoogleMapsResult *GoogleMapsResultDelta `json:",omitzero,inline" discriminator:"google_maps_result"` + FunctionResult *FunctionResultDelta `json:",omitzero,inline" discriminator:"function_result"` + + metadata `api:"union"` +} + +func (u StepDeltaDelta) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *StepDeltaDelta) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalDiscriminatedUnion(data, "type", u, &u.metadata) +} + +type StepStart struct { + Index int `json:"index" api:"required"` + // A step in the interaction. + Step Step `json:"step" api:"required"` + // The event_id token to be used to resume the interaction stream, from this event. + EventID string `json:"event_id,omitzero"` + // This field doesn't need to be set. + EventType constant.StepStart `json:"event_type" default:"step.start"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type StepStop struct { + Index int `json:"index" api:"required"` + // The event_id token to be used to resume the interaction stream, from this event. + EventID string `json:"event_id,omitzero"` + // This field doesn't need to be set. + EventType constant.StepStop `json:"event_type" default:"step.stop"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type TextAnnotationDelta struct { + // Citation information for model-generated content. + Annotations []Annotation `json:"annotations,omitzero"` + // This field doesn't need to be set. + Type constant.TextAnnotationDelta `json:"type" default:"text_annotation_delta"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A text content block. +type TextContent struct { + // Required. The text content. + Text string `json:"text" api:"required"` + // Citation information for model-generated content. + Annotations []Annotation `json:"annotations,omitzero"` + // This field doesn't need to be set. + Type constant.Text `json:"type" default:"text"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type TextDelta struct { + Text string `json:"text" api:"required"` + // This field doesn't need to be set. + Type constant.Text `json:"type" default:"text"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Configuration for text output format. +type TextResponseFormat struct { + // The MIME type of the text output. + // + // Any of "application/json", "text/plain". + MimeType string `json:"mime_type,omitzero"` + // The JSON schema that the output should conform to. Only applicable when + // mime_type is application/json. + Schema map[string]any `json:"schema,omitzero"` + // This field doesn't need to be set. + Type constant.Text `json:"type" default:"text"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type ThinkingLevel string + +const ( + ThinkingLevelMinimal ThinkingLevel = "minimal" + ThinkingLevelLow ThinkingLevel = "low" + ThinkingLevelMedium ThinkingLevel = "medium" + ThinkingLevelHigh ThinkingLevel = "high" +) + +type ThoughtSignatureDelta struct { + // Signature to match the backend source to be part of the generation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.ThoughtSignature `json:"type" default:"thought_signature"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A thought step. +type ThoughtStep struct { + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // A summary of the thought. + Summary []ThoughtSummaryContent `json:"summary,omitzero"` + // This field doesn't need to be set. + Type constant.Thought `json:"type" default:"thought"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Only one field in this union will be nonzero +type ThoughtSummaryContent struct { + Text *TextContent `json:",omitzero,inline" discriminator:"text"` + Image *ImageContent `json:",omitzero,inline" discriminator:"image"` + + metadata `api:"union"` +} + +func (u ThoughtSummaryContent) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *ThoughtSummaryContent) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalDiscriminatedUnion(data, "type", u, &u.metadata) +} + +type ThoughtSummaryDelta struct { + // A new summary item to be added to the thought. + Content *ThoughtSummaryContent `json:"content,omitzero"` + // This field doesn't need to be set. + Type constant.ThoughtSummary `json:"type" default:"thought_summary"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Only one field in this union will be nonzero +type Tool struct { + Function *Function `json:",omitzero,inline" discriminator:"function"` + CodeExecution *ToolCodeExecution `json:",omitzero,inline" discriminator:"code_execution"` + URLContext *ToolURLContext `json:",omitzero,inline" discriminator:"url_context"` + ComputerUse *ToolComputerUse `json:",omitzero,inline" discriminator:"computer_use"` + MCPServer *ToolMCPServer `json:",omitzero,inline" discriminator:"mcp_server"` + GoogleSearch *ToolGoogleSearch `json:",omitzero,inline" discriminator:"google_search"` + FileSearch *ToolFileSearch `json:",omitzero,inline" discriminator:"file_search"` + GoogleMaps *ToolGoogleMaps `json:",omitzero,inline" discriminator:"google_maps"` + Retrieval *ToolRetrieval `json:",omitzero,inline" discriminator:"retrieval"` + + metadata `api:"union"` +} + +func (u Tool) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *Tool) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalDiscriminatedUnion(data, "type", u, &u.metadata) +} + +// A tool that can be used by the model to execute code. +type ToolCodeExecution struct { + // This field doesn't need to be set. + Type constant.CodeExecution `json:"type" default:"code_execution"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A tool that can be used by the model to fetch URL context. +type ToolURLContext struct { + // This field doesn't need to be set. + Type constant.URLContext `json:"type" default:"url_context"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A tool that can be used by the model to interact with the computer. +type ToolComputerUse struct { + // The environment being operated. + // + // Any of "browser". + Environment string `json:"environment,omitzero"` + // The list of predefined functions that are excluded from the model call. + ExcludedPredefinedFunctions []string `json:"excluded_predefined_functions,omitzero"` + // This field doesn't need to be set. + Type constant.ComputerUse `json:"type" default:"computer_use"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A MCPServer is a server that can be called by the model to perform actions. +type ToolMCPServer struct { + // The allowed tools. + AllowedTools []AllowedTools `json:"allowed_tools,omitzero"` + // Optional: Fields for authentication headers, timeouts, etc., if needed. + Headers map[string]string `json:"headers,omitzero"` + // The name of the MCPServer. + Name string `json:"name,omitzero"` + // The full URL for the MCPServer endpoint. Example: "https://api.example.com/mcp" + URL string `json:"url,omitzero"` + // This field doesn't need to be set. + Type constant.MCPServer `json:"type" default:"mcp_server"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A tool that can be used by the model to search Google. +type ToolGoogleSearch struct { + // The types of search grounding to enable. + // + // Any of "web_search", "image_search", "enterprise_web_search". + SearchTypes []string `json:"search_types,omitzero"` + // This field doesn't need to be set. + Type constant.GoogleSearch `json:"type" default:"google_search"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A tool that can be used by the model to search files. +type ToolFileSearch struct { + // The file search store names to search. + FileSearchStoreNames []string `json:"file_search_store_names,omitzero"` + // Metadata filter to apply to the semantic retrieval documents and chunks. + MetadataFilter string `json:"metadata_filter,omitzero"` + // The number of semantic retrieval chunks to retrieve. + TopK *int `json:"top_k,omitzero"` + // This field doesn't need to be set. + Type constant.FileSearch `json:"type" default:"file_search"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A tool that can be used by the model to call Google Maps. +type ToolGoogleMaps struct { + // Whether to return a widget context token in the tool call result of the + // response. + EnableWidget *bool `json:"enable_widget,omitzero"` + // The latitude of the user's location. + Latitude *float64 `json:"latitude,omitzero"` + // The longitude of the user's location. + Longitude *float64 `json:"longitude,omitzero"` + // This field doesn't need to be set. + Type constant.GoogleMaps `json:"type" default:"google_maps"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A tool that can be used by the model to retrieve files. +type ToolRetrieval struct { + // The types of file retrieval to enable. + // + // Any of "vertex_ai_search". + RetrievalTypes []string `json:"retrieval_types,omitzero"` + // Used to specify configuration for VertexAISearch. + VertexAISearchConfig ToolRetrievalVertexAISearchConfig `json:"vertex_ai_search_config,omitzero"` + // This field doesn't need to be set. + Type constant.Retrieval `json:"type" default:"retrieval"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Used to specify configuration for VertexAISearch. +type ToolRetrievalVertexAISearchConfig struct { + // Optional. Used to specify Vertex AI Search datastores. + Datastores []string `json:"datastores,omitzero"` + // Optional. Used to specify Vertex AI Search engine. + Engine string `json:"engine,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The tool choice configuration containing allowed tools. +type ToolChoiceConfig struct { + // The allowed tools. + AllowedTools AllowedTools `json:"allowed_tools,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type ToolChoiceType string + +const ( + ToolChoiceTypeAuto ToolChoiceType = "auto" + ToolChoiceTypeAny ToolChoiceType = "any" + ToolChoiceTypeNone ToolChoiceType = "none" + ToolChoiceTypeValidated ToolChoiceType = "validated" +) + +// A URL citation annotation. +type URLCitation struct { + // End of the attributed segment, exclusive. + EndIndex *int `json:"end_index,omitzero"` + // Start of segment of the response that is attributed to this source. + // + // Index indicates the start of the segment, measured in bytes. + StartIndex *int `json:"start_index,omitzero"` + // The title of the URL. + Title string `json:"title,omitzero"` + // The URL. + URL string `json:"url,omitzero"` + // This field doesn't need to be set. + Type constant.URLCitation `json:"type" default:"url_citation"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The arguments to pass to the URL context. +type URLContextCallArguments struct { + // The URLs to fetch. + URLs []string `json:"urls,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type URLContextCallDelta struct { + // The arguments to pass to the URL context. + Arguments URLContextCallArguments `json:"arguments" api:"required"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.URLContextCall `json:"type" default:"url_context_call"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// URL context call step. +type URLContextCallStep struct { + // Required. A unique ID for this specific tool call. + ID string `json:"id" api:"required"` + // Required. The arguments to pass to the URL context. + Arguments URLContextCallStepArguments `json:"arguments" api:"required"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.URLContextCall `json:"type" default:"url_context_call"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Required. The arguments to pass to the URL context. +type URLContextCallStepArguments struct { + // The URLs to fetch. + URLs []string `json:"urls,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The result of the URL context. +type URLContextResult struct { + // The status of the URL retrieval. + // + // Any of "success", "error", "paywall", "unsafe". + Status string `json:"status,omitzero"` + // The URL that was fetched. + URL string `json:"url,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type URLContextResultDelta struct { + Result []URLContextResult `json:"result" api:"required"` + IsError *bool `json:"is_error,omitzero"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.URLContextResult `json:"type" default:"url_context_result"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// URL context result step. +type URLContextResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id" api:"required"` + // Required. The results of the URL context. + Result []URLContextResultStepResult `json:"result" api:"required"` + // Whether the URL context resulted in an error. + IsError *bool `json:"is_error,omitzero"` + // A signature hash for backend validation. + Signature string `json:"signature,omitzero" format:"byte"` + // This field doesn't need to be set. + Type constant.URLContextResult `json:"type" default:"url_context_result"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The result of the URL context. +type URLContextResultStepResult struct { + // The status of the URL retrieval. + // + // Any of "success", "error", "paywall", "unsafe". + Status string `json:"status,omitzero"` + // The URL that was fetched. + URL string `json:"url,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Statistics on the interaction request's token usage. +type Usage struct { + // A breakdown of cached token usage by modality. + CachedTokensByModality []UsageCachedTokensByModality `json:"cached_tokens_by_modality,omitzero"` + // Grounding tool count. + GroundingToolCount []UsageGroundingToolCount `json:"grounding_tool_count,omitzero"` + // A breakdown of input token usage by modality. + InputTokensByModality []UsageInputTokensByModality `json:"input_tokens_by_modality,omitzero"` + // A breakdown of output token usage by modality. + OutputTokensByModality []UsageOutputTokensByModality `json:"output_tokens_by_modality,omitzero"` + // A breakdown of tool-use token usage by modality. + ToolUseTokensByModality []UsageToolUseTokensByModality `json:"tool_use_tokens_by_modality,omitzero"` + // Number of tokens in the cached part of the prompt (the cached content). + TotalCachedTokens *int `json:"total_cached_tokens,omitzero"` + // Number of tokens in the prompt (context). + TotalInputTokens *int `json:"total_input_tokens,omitzero"` + // Total number of tokens across all the generated responses. + TotalOutputTokens *int `json:"total_output_tokens,omitzero"` + // Number of tokens of thoughts for thinking models. + TotalThoughtTokens *int `json:"total_thought_tokens,omitzero"` + // Total token count for the interaction request (prompt + responses + other + // internal tokens). + TotalTokens *int `json:"total_tokens,omitzero"` + // Number of tokens present in tool-use prompt(s). + TotalToolUseTokens *int `json:"total_tool_use_tokens,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The token count for a single response modality. +type UsageCachedTokensByModality struct { + // The modality associated with the token count. + // + // Any of "text", "image", "audio", "video", "document". + Modality string `json:"modality,omitzero"` + // Number of tokens for the modality. + Tokens *int `json:"tokens,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The number of grounding tool counts. +type UsageGroundingToolCount struct { + // The number of grounding tool counts. + Count *int `json:"count,omitzero"` + // The grounding tool type associated with the count. + // + // Any of "google_search", "google_maps", "retrieval". + Type string `json:"type,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The token count for a single response modality. +type UsageInputTokensByModality struct { + // The modality associated with the token count. + // + // Any of "text", "image", "audio", "video", "document". + Modality string `json:"modality,omitzero"` + // Number of tokens for the modality. + Tokens *int `json:"tokens,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The token count for a single response modality. +type UsageOutputTokensByModality struct { + // The modality associated with the token count. + // + // Any of "text", "image", "audio", "video", "document". + Modality string `json:"modality,omitzero"` + // Number of tokens for the modality. + Tokens *int `json:"tokens,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// The token count for a single response modality. +type UsageToolUseTokensByModality struct { + // The modality associated with the token count. + // + // Any of "text", "image", "audio", "video", "document". + Modality string `json:"modality,omitzero"` + // Number of tokens for the modality. + Tokens *int `json:"tokens,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Input provided by the user. +type UserInputStep struct { + Content []Content `json:"content,omitzero"` + // This field doesn't need to be set. + Type constant.UserInput `json:"type" default:"user_input"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// A video content block. +type VideoContent struct { + // The video content. + Data string `json:"data,omitzero" format:"byte"` + // The mime type of the video. + // + // Any of "video/mp4", "video/mpeg", "video/mpg", "video/mov", "video/avi", + // "video/x-flv", "video/webm", "video/wmv", "video/3gpp". + MimeType string `json:"mime_type,omitzero"` + // The resolution of the media. + // + // Any of "low", "medium", "high", "ultra_high". + Resolution string `json:"resolution,omitzero"` + // The URI of the video. + Uri string `json:"uri,omitzero"` + // This field doesn't need to be set. + Type constant.Video `json:"type" default:"video"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type VideoDelta struct { + Data string `json:"data,omitzero" format:"byte"` + // Any of "video/mp4", "video/mpeg", "video/mpg", "video/mov", "video/avi", + // "video/x-flv", "video/webm", "video/wmv", "video/3gpp". + MimeType string `json:"mime_type,omitzero"` + // The resolution of the media. + // + // Any of "low", "medium", "high", "ultra_high". + Resolution string `json:"resolution,omitzero"` + Uri string `json:"uri,omitzero"` + // This field doesn't need to be set. + Type constant.Video `json:"type" default:"video"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Configuration for video output format. +type VideoResponseFormat struct { + // The aspect ratio for the video output. + // + // Any of "16:9", "9:16". + AspectRatio string `json:"aspectRatio,omitzero"` + // The delivery mode for the video output. + // + // Any of "inline", "uri". + Delivery string `json:"delivery,omitzero"` + // The duration for the video output. + Duration string `json:"duration,omitzero" format:"google-duration"` + // The GCS URI to store the video output. Required for Vertex if delivery mode is + // URI. + GcsUri string `json:"gcsUri,omitzero"` + // This field doesn't need to be set. + Type constant.Video `json:"type" default:"video"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +// Message for configuring webhook events for a request. +type WebhookConfig struct { + // Optional. If set, these webhook URIs will be used for webhook events instead of + // the registered webhooks. + Uris []string `json:"uris,omitzero"` + // Optional. The user metadata that will be returned on each event emission to the + // webhooks. + UserMetadata map[string]any `json:"user_metadata,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +type DeleteResponse = any + +type DeleteParams struct { + // Defaults to "v1beta" if not set. + APIVersion string `path:"api_version" api:"required" json:"-"` +} + +type CancelParams struct { + // Defaults to "v1beta" if not set. + APIVersion string `path:"api_version" api:"required" json:"-"` +} + +type NewAgentParams struct { + // Defaults to "v1beta" if not set. + APIVersion string `path:"api_version" api:"required" json:"-"` + // The name of the `Agent` used for generating the interaction. + Agent string `json:"agent" api:"required"` + // The input for the interaction. + Input Input `json:"input,omitzero" api:"required"` + // Configuration parameters for the agent interaction. + AgentConfig *NewAgentParamsAgentConfig `json:"agent_config,omitzero"` + // Input only. Whether to run the model interaction in the background. + Background *bool `json:"background,omitzero"` + // The ID of the previous interaction, if any. + PreviousInteractionID string `json:"previous_interaction_id,omitzero"` + // Enforces that the generated response is a JSON object that complies with the + // JSON schema specified in this field. + ResponseFormat *NewAgentParamsResponseFormat `json:"response_format,omitzero"` + // The mime type of the response. This is required if response_format is set. + ResponseMimeType string `json:"response_mime_type,omitzero"` + // The requested modalities of the response (TEXT, IMAGE, AUDIO). + // + // Any of "text", "image", "audio", "video", "document". + ResponseModalities []string `json:"response_modalities,omitzero"` + // The service tier for the interaction. + // + // Any of "flex", "standard", "priority". + ServiceTier string `json:"service_tier,omitzero"` + // Input only. Whether to store the response and request for later retrieval. + Store *bool `json:"store,omitzero"` + // System instruction for the interaction. + SystemInstruction string `json:"system_instruction,omitzero"` + // A list of tool declarations the model may call during interaction. + Tools []Tool `json:"tools,omitzero"` + // Optional. Webhook configuration for receiving notifications when the interaction + // completes. + WebhookConfig WebhookConfig `json:"webhook_config,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +func (r NewAgentParams) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} +func (r *NewAgentParams) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + for _, val := range r.ResponseModalities { + unmarshalinfo.ExpectEnum(&r.metadata, val, "text", "image", "audio", "video", "document") + } + unmarshalinfo.PreferEnum(&r.metadata, &r.ServiceTier, "flex", "standard", "priority") + return nil +} + +// Only one field in this union will be nonzero +type NewAgentParamsAgentConfig struct { + Dynamic *DynamicAgentConfig `json:",omitzero,inline" discriminator:"dynamic"` + DeepResearch *DeepResearchAgentConfig `json:",omitzero,inline" discriminator:"deep-research"` + + metadata `api:"union"` +} + +func (u NewAgentParamsAgentConfig) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *NewAgentParamsAgentConfig) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalDiscriminatedUnion(data, "type", u, &u.metadata) +} + +// Only one field in this union will be nonzero +type NewAgentParamsResponseFormat struct { + ResponseFormatListArray ResponseFormatList `json:",omitzero,inline"` + AudioResponseFormat *AudioResponseFormat `json:",omitzero,inline"` + TextResponseFormat *TextResponseFormat `json:",omitzero,inline"` + ImageResponseFormat *ImageResponseFormat `json:",omitzero,inline"` + VideoResponseFormat *VideoResponseFormat `json:",omitzero,inline"` + + metadata `api:"union"` +} + +func (u NewAgentParamsResponseFormat) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *NewAgentParamsResponseFormat) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalUnion(data, u, &u.metadata) +} + +type NewModelParams struct { + // Defaults to "v1beta" if not set. + APIVersion string `path:"api_version" api:"required" json:"-"` + // The input for the interaction. + Input Input `json:"input,omitzero" api:"required"` + // The name of the `Model` used for generating the interaction. + Model string `json:"model" api:"required"` + // Input only. Whether to run the model interaction in the background. + Background *bool `json:"background,omitzero"` + // Input only. Configuration parameters for the model interaction. + GenerationConfig GenerationConfig `json:"generation_config,omitzero"` + // The ID of the previous interaction, if any. + PreviousInteractionID string `json:"previous_interaction_id,omitzero"` + // Enforces that the generated response is a JSON object that complies with the + // JSON schema specified in this field. + ResponseFormat *NewModelParamsResponseFormat `json:"response_format,omitzero"` + // The mime type of the response. This is required if response_format is set. + ResponseMimeType string `json:"response_mime_type,omitzero"` + // The requested modalities of the response (TEXT, IMAGE, AUDIO). + // + // Any of "text", "image", "audio", "video", "document". + ResponseModalities []string `json:"response_modalities,omitzero"` + // The service tier for the interaction. + // + // Any of "flex", "standard", "priority". + ServiceTier string `json:"service_tier,omitzero"` + // Input only. Whether to store the response and request for later retrieval. + Store *bool `json:"store,omitzero"` + // System instruction for the interaction. + SystemInstruction string `json:"system_instruction,omitzero"` + // A list of tool declarations the model may call during interaction. + Tools []Tool `json:"tools,omitzero"` + // Optional. Webhook configuration for receiving notifications when the interaction + // completes. + WebhookConfig WebhookConfig `json:"webhook_config,omitzero"` + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +func (r NewModelParams) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} +func (r *NewModelParams) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + for _, val := range r.ResponseModalities { + unmarshalinfo.ExpectEnum(&r.metadata, val, "text", "image", "audio", "video", "document") + } + unmarshalinfo.PreferEnum(&r.metadata, &r.ServiceTier, "flex", "standard", "priority") + return nil +} + +// Only one field in this union will be nonzero +type NewModelParamsResponseFormat struct { + ResponseFormatListArray ResponseFormatList `json:",omitzero,inline"` + AudioResponseFormat *AudioResponseFormat `json:",omitzero,inline"` + TextResponseFormat *TextResponseFormat `json:",omitzero,inline"` + ImageResponseFormat *ImageResponseFormat `json:",omitzero,inline"` + VideoResponseFormat *VideoResponseFormat `json:",omitzero,inline"` + + metadata `api:"union"` +} + +func (u NewModelParamsResponseFormat) MarshalJSON() ([]byte, error) { + return apijson.MarshalUnionStruct(u) +} + +func (u *NewModelParamsResponseFormat) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalUnion(data, u, &u.metadata) +} + +type GetParams struct { + // Defaults to "v1beta" if not set. + APIVersion string `path:"api_version" api:"required" json:"-"` + // If set to true, includes the input in the response. + IncludeInput bool `query:"include_input,omitzero" json:"-"` + // Optional. If set, resumes the interaction stream from the next chunk after the + // event marked by the event id. Can only be used if `stream` is true. + LastEventID string `query:"last_event_id,omitzero" json:"-"` +} + +// URLQuery serializes [GetParams]'s query parameters as `url.Values`. +func (r GetParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +// Marshaling/unmarshaling boilerplate below + +func (r AllowedTools) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ArgumentsDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r AudioContent) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r AudioDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r AudioResponseFormat) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r CodeExecutionCallArguments) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r CodeExecutionCallDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r CodeExecutionCallStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r CodeExecutionCallStepArguments) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r CodeExecutionResultDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r CodeExecutionResultStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r DeepResearchAgentConfig) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r DocumentContent) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r DocumentDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r DynamicAgentConfig) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ErrorEvent) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ErrorEventError) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r FileCitation) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r FileSearchCallDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r FileSearchCallStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r FileSearchResultDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r FileSearchResultStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r Function) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r FunctionCallDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r FunctionCallStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r FunctionResultDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r FunctionResultStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GenerationConfig) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleMapsCallArguments) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleMapsCallDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleMapsCallStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleMapsCallStepArguments) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleMapsResult) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleMapsResultPlace) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleMapsResultPlaceReviewSnippet) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleMapsResultDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleMapsResultStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleMapsResultStepResult) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleMapsResultStepResultPlace) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleMapsResultStepResultPlaceReviewSnippet) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleSearchCallArguments) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleSearchCallDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleSearchCallStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleSearchCallStepArguments) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleSearchResult) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleSearchResultDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleSearchResultStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r GoogleSearchResultStepResult) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ImageConfig) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ImageContent) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ImageDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ImageResponseFormat) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r Interaction) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r InteractionCompletedEvent) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r InteractionCreatedEvent) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r InteractionStatusUpdate) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r MCPServerToolCallDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r MCPServerToolCallStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r MCPServerToolResultDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r MCPServerToolResultStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ModelOutputStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r PlaceCitation) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r PlaceCitationReviewSnippet) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r SpeechConfig) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r StepDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r StepStart) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r StepStop) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r TextAnnotationDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r TextContent) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r TextDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r TextResponseFormat) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ThoughtSignatureDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ThoughtStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ThoughtSummaryDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolCodeExecution) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolURLContext) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolComputerUse) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolMCPServer) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolGoogleSearch) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolFileSearch) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolGoogleMaps) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolRetrieval) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolRetrievalVertexAISearchConfig) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolChoiceConfig) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r URLCitation) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r URLContextCallArguments) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r URLContextCallDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r URLContextCallStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r URLContextCallStepArguments) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r URLContextResult) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r URLContextResultDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r URLContextResultStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r URLContextResultStepResult) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r Usage) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r UsageCachedTokensByModality) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r UsageGroundingToolCount) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r UsageInputTokensByModality) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r UsageOutputTokensByModality) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r UsageToolUseTokensByModality) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r UserInputStep) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r VideoContent) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r VideoDelta) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r VideoResponseFormat) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r WebhookConfig) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r *AllowedTools) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.PreferEnum(&r.metadata, &r.Mode, "auto", "any", "none", "validated") + return nil +} + +func (r *ArgumentsDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "arguments_delta") + return nil +} + +func (r *AudioContent) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "audio") + unmarshalinfo.PreferEnum( + &r.metadata, &r.MimeType, + "audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac", "audio/mpeg", "audio/m4a", "audio/l16", "audio/opus", "audio/alaw", "audio/mulaw", + ) + return nil +} + +func (r *AudioDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "audio") + unmarshalinfo.PreferEnum( + &r.metadata, &r.MimeType, + "audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac", "audio/mpeg", "audio/m4a", "audio/l16", "audio/opus", "audio/alaw", "audio/mulaw", + ) + return nil +} + +func (r *AudioResponseFormat) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "audio") + unmarshalinfo.PreferEnum(&r.metadata, &r.Delivery, "inline", "uri") + unmarshalinfo.PreferEnum( + &r.metadata, &r.MimeType, + "audio/mp3", "audio/ogg_opus", "audio/l16", "audio/wav", "audio/alaw", "audio/mulaw", + ) + return nil +} + +func (r *CodeExecutionCallArguments) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.PreferEnum(&r.metadata, &r.Language, "python") + return nil +} + +func (r *CodeExecutionCallDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "code_execution_call") + return nil +} + +func (r *CodeExecutionCallStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "code_execution_call") + return nil +} + +func (r *CodeExecutionCallStepArguments) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.PreferEnum(&r.metadata, &r.Language, "python") + return nil +} + +func (r *CodeExecutionResultDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "code_execution_result") + return nil +} + +func (r *CodeExecutionResultStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "code_execution_result") + return nil +} + +func (r *DeepResearchAgentConfig) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "deep-research") + unmarshalinfo.PreferEnum(&r.metadata, &r.ThinkingSummaries, "auto", "none") + unmarshalinfo.PreferEnum(&r.metadata, &r.Visualization, "off", "auto") + return nil +} + +func (r *DocumentContent) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "document") + unmarshalinfo.PreferEnum(&r.metadata, &r.MimeType, "application/pdf") + return nil +} + +func (r *DocumentDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "document") + unmarshalinfo.PreferEnum(&r.metadata, &r.MimeType, "application/pdf") + return nil +} + +func (r *DynamicAgentConfig) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "dynamic") + return nil +} + +func (r *ErrorEvent) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.EventType, "error") + return nil +} + +func (r *ErrorEventError) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *FileCitation) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "file_citation") + return nil +} + +func (r *FileSearchCallDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "file_search_call") + return nil +} + +func (r *FileSearchCallStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "file_search_call") + return nil +} + +func (r *FileSearchResultDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "file_search_result") + return nil +} + +func (r *FileSearchResultStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "file_search_result") + return nil +} + +func (r *Function) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "function") + return nil +} + +func (r *FunctionCallDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "function_call") + return nil +} + +func (r *FunctionCallStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "function_call") + return nil +} + +func (r *FunctionResultDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "function_result") + return nil +} + +func (r *FunctionResultStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "function_result") + return nil +} + +func (r *GenerationConfig) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.PreferEnum(&r.metadata, &r.ThinkingLevel, "minimal", "low", "medium", "high") + unmarshalinfo.PreferEnum(&r.metadata, &r.ThinkingSummaries, "auto", "none") + return nil +} + +func (r *GoogleMapsCallArguments) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *GoogleMapsCallDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "google_maps_call") + return nil +} + +func (r *GoogleMapsCallStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "google_maps_call") + return nil +} + +func (r *GoogleMapsCallStepArguments) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *GoogleMapsResult) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *GoogleMapsResultPlace) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *GoogleMapsResultPlaceReviewSnippet) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *GoogleMapsResultDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "google_maps_result") + return nil +} + +func (r *GoogleMapsResultStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "google_maps_result") + return nil +} + +func (r *GoogleMapsResultStepResult) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *GoogleMapsResultStepResultPlace) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *GoogleMapsResultStepResultPlaceReviewSnippet) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *GoogleSearchCallArguments) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *GoogleSearchCallDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "google_search_call") + return nil +} + +func (r *GoogleSearchCallStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "google_search_call") + unmarshalinfo.PreferEnum(&r.metadata, &r.SearchType, "web_search", "image_search", "enterprise_web_search") + return nil +} + +func (r *GoogleSearchCallStepArguments) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *GoogleSearchResult) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *GoogleSearchResultDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "google_search_result") + return nil +} + +func (r *GoogleSearchResultStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "google_search_result") + return nil +} + +func (r *GoogleSearchResultStepResult) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *ImageConfig) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.PreferEnum( + &r.metadata, &r.AspectRatio, + "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:8", "8:1", "1:4", "4:1", + ) + unmarshalinfo.PreferEnum(&r.metadata, &r.ImageSize, "1K", "2K", "4K", "512") + return nil +} + +func (r *ImageContent) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "image") + unmarshalinfo.PreferEnum( + &r.metadata, &r.MimeType, + "image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "image/gif", "image/bmp", "image/tiff", + ) + unmarshalinfo.PreferEnum(&r.metadata, &r.Resolution, "low", "medium", "high", "ultra_high") + return nil +} + +func (r *ImageDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "image") + unmarshalinfo.PreferEnum( + &r.metadata, &r.MimeType, + "image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "image/gif", "image/bmp", "image/tiff", + ) + unmarshalinfo.PreferEnum(&r.metadata, &r.Resolution, "low", "medium", "high", "ultra_high") + return nil +} + +func (r *ImageResponseFormat) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "image") + unmarshalinfo.PreferEnum( + &r.metadata, &r.AspectRatio, + "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:8", "8:1", "1:4", "4:1", + ) + unmarshalinfo.PreferEnum(&r.metadata, &r.Delivery, "inline", "uri") + unmarshalinfo.PreferEnum(&r.metadata, &r.ImageSize, "512", "1K", "2K", "4K") + unmarshalinfo.PreferEnum(&r.metadata, &r.MimeType, "image/jpeg") + return nil +} + +func (r *Interaction) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectEnum( + &r.metadata, r.Status, + "in_progress", "requires_action", "completed", "failed", "cancelled", "incomplete", + ) + for _, val := range r.ResponseModalities { + unmarshalinfo.ExpectEnum(&r.metadata, val, "text", "image", "audio", "video", "document") + } + unmarshalinfo.PreferEnum(&r.metadata, &r.ServiceTier, "flex", "standard", "priority") + return nil +} + +func (r *InteractionCompletedEvent) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.EventType, "interaction.completed") + return nil +} + +func (r *InteractionCreatedEvent) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.EventType, "interaction.created") + return nil +} + +func (r *InteractionStatusUpdate) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.EventType, "interaction.status_update") + unmarshalinfo.ExpectEnum( + &r.metadata, r.Status, + "in_progress", "requires_action", "completed", "failed", "cancelled", "incomplete", + ) + return nil +} + +func (r *MCPServerToolCallDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "mcp_server_tool_call") + return nil +} + +func (r *MCPServerToolCallStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "mcp_server_tool_call") + return nil +} + +func (r *MCPServerToolResultDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "mcp_server_tool_result") + return nil +} + +func (r *MCPServerToolResultStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "mcp_server_tool_result") + return nil +} + +func (r *ModelOutputStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "model_output") + return nil +} + +func (r *PlaceCitation) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "place_citation") + return nil +} + +func (r *PlaceCitationReviewSnippet) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *SpeechConfig) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *StepDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.EventType, "step.delta") + return nil +} + +func (r *StepStart) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.EventType, "step.start") + return nil +} + +func (r *StepStop) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.EventType, "step.stop") + return nil +} + +func (r *TextAnnotationDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "text_annotation_delta") + return nil +} + +func (r *TextContent) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "text") + return nil +} + +func (r *TextDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "text") + return nil +} + +func (r *TextResponseFormat) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "text") + unmarshalinfo.PreferEnum(&r.metadata, &r.MimeType, "application/json", "text/plain") + return nil +} + +func (r *ThoughtSignatureDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "thought_signature") + return nil +} + +func (r *ThoughtStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "thought") + return nil +} + +func (r *ThoughtSummaryDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "thought_summary") + return nil +} + +func (r *ToolCodeExecution) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "code_execution") + return nil +} + +func (r *ToolURLContext) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "url_context") + return nil +} + +func (r *ToolComputerUse) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "computer_use") + unmarshalinfo.PreferEnum(&r.metadata, &r.Environment, "browser") + return nil +} + +func (r *ToolMCPServer) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "mcp_server") + return nil +} + +func (r *ToolGoogleSearch) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "google_search") + for _, val := range r.SearchTypes { + unmarshalinfo.ExpectEnum(&r.metadata, val, "web_search", "image_search", "enterprise_web_search") + } + return nil +} + +func (r *ToolFileSearch) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "file_search") + return nil +} + +func (r *ToolGoogleMaps) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "google_maps") + return nil +} + +func (r *ToolRetrieval) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "retrieval") + for _, val := range r.RetrievalTypes { + unmarshalinfo.ExpectEnum(&r.metadata, val, "vertex_ai_search") + } + return nil +} + +func (r *ToolRetrievalVertexAISearchConfig) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *ToolChoiceConfig) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *URLCitation) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "url_citation") + return nil +} + +func (r *URLContextCallArguments) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *URLContextCallDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "url_context_call") + return nil +} + +func (r *URLContextCallStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "url_context_call") + return nil +} + +func (r *URLContextCallStepArguments) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *URLContextResult) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.PreferEnum(&r.metadata, &r.Status, "success", "error", "paywall", "unsafe") + return nil +} + +func (r *URLContextResultDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "url_context_result") + return nil +} + +func (r *URLContextResultStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "url_context_result") + return nil +} + +func (r *URLContextResultStepResult) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.PreferEnum(&r.metadata, &r.Status, "success", "error", "paywall", "unsafe") + return nil +} + +func (r *Usage) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} + +func (r *UsageCachedTokensByModality) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.PreferEnum(&r.metadata, &r.Modality, "text", "image", "audio", "video", "document") + return nil +} + +func (r *UsageGroundingToolCount) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.PreferEnum(&r.metadata, &r.Type, "google_search", "google_maps", "retrieval") + return nil +} + +func (r *UsageInputTokensByModality) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.PreferEnum(&r.metadata, &r.Modality, "text", "image", "audio", "video", "document") + return nil +} + +func (r *UsageOutputTokensByModality) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.PreferEnum(&r.metadata, &r.Modality, "text", "image", "audio", "video", "document") + return nil +} + +func (r *UsageToolUseTokensByModality) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.PreferEnum(&r.metadata, &r.Modality, "text", "image", "audio", "video", "document") + return nil +} + +func (r *UserInputStep) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "user_input") + return nil +} + +func (r *VideoContent) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "video") + unmarshalinfo.PreferEnum( + &r.metadata, &r.MimeType, + "video/mp4", "video/mpeg", "video/mpg", "video/mov", "video/avi", "video/x-flv", "video/webm", "video/wmv", "video/3gpp", + ) + unmarshalinfo.PreferEnum(&r.metadata, &r.Resolution, "low", "medium", "high", "ultra_high") + return nil +} + +func (r *VideoDelta) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "video") + unmarshalinfo.PreferEnum( + &r.metadata, &r.MimeType, + "video/mp4", "video/mpeg", "video/mpg", "video/mov", "video/avi", "video/x-flv", "video/webm", "video/wmv", "video/3gpp", + ) + unmarshalinfo.PreferEnum(&r.metadata, &r.Resolution, "low", "medium", "high", "ultra_high") + return nil +} + +func (r *VideoResponseFormat) UnmarshalJSON(data []byte) error { + if err := apijson.UnmarshalRoot(data, r, &r.metadata); err != nil { + return err + } + unmarshalinfo.ExpectConstant(&r.metadata, r.Type, "video") + unmarshalinfo.PreferEnum(&r.metadata, &r.AspectRatio, "16:9", "9:16") + unmarshalinfo.PreferEnum(&r.metadata, &r.Delivery, "inline", "uri") + return nil +} + +func (r *WebhookConfig) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r, &r.metadata) +} diff --git a/interactions/interaction_test.go b/interactions/interaction_test.go new file mode 100644 index 00000000..e1897bd6 --- /dev/null +++ b/interactions/interaction_test.go @@ -0,0 +1,279 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package interactions_test + +import ( + "context" + "errors" + "os" + "testing" + + "google.golang.org/genai/interactions" + "google.golang.org/genai/interactions/internal/testutil" + "google.golang.org/genai/interactions/option" +) + +func TestInteractionDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := interactions.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Interactions.Delete( + context.TODO(), + "id", + interactions.DeleteParams{ + APIVersion: "api_version", + }, + ) + if err != nil { + var apierr *interactions.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestInteractionCancel(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := interactions.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Interactions.Cancel( + context.TODO(), + "id", + interactions.CancelParams{ + APIVersion: "api_version", + }, + ) + if err != nil { + var apierr *interactions.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestInteractionNewAgentWithOptionalParams(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := interactions.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Interactions.NewAgent(context.TODO(), interactions.NewAgentParams{ + APIVersion: "api_version", + Agent: "deep-research-pro-preview-12-2025", + Input: interactions.Input{ + TextContent: &interactions.TextContent{ + Text: "text", + Annotations: []interactions.Annotation{{ + URLCitation: &interactions.URLCitation{ + EndIndex: new(0), + StartIndex: new(0), + Title: "title", + URL: "url", + }, + }}, + }, + }, + AgentConfig: &interactions.NewAgentParamsAgentConfig{ + Dynamic: &interactions.DynamicAgentConfig{}, + }, + Background: new(true), + PreviousInteractionID: "previous_interaction_id", + ResponseFormat: &interactions.NewAgentParamsResponseFormat{ + ResponseFormatListArray: interactions.ResponseFormatList{interactions.ResponseFormatListItem{ + AudioResponseFormat: &interactions.AudioResponseFormat{ + BitRate: new(0), + Delivery: "inline", + MimeType: "audio/mp3", + SampleRate: new(0), + }, + }}, + }, + ResponseMimeType: "response_mime_type", + ResponseModalities: []string{"text"}, + ServiceTier: "flex", + Store: new(true), + SystemInstruction: "system_instruction", + Tools: []interactions.Tool{{ + Function: &interactions.Function{ + Description: "description", + Name: "name", + Parameters: map[string]any{}, + }, + }}, + WebhookConfig: interactions.WebhookConfig{ + Uris: []string{"string"}, + UserMetadata: map[string]any{ + "foo": "bar", + }, + }, + }) + if err != nil { + var apierr *interactions.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestInteractionNewModelWithOptionalParams(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := interactions.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Interactions.NewModel(context.TODO(), interactions.NewModelParams{ + APIVersion: "api_version", + Input: interactions.Input{ + TextContent: &interactions.TextContent{ + Text: "text", + Annotations: []interactions.Annotation{{ + URLCitation: &interactions.URLCitation{ + EndIndex: new(0), + StartIndex: new(0), + Title: "title", + URL: "url", + }, + }}, + }, + }, + Model: "gemini-2.5-computer-use-preview-10-2025", + Background: new(true), + GenerationConfig: interactions.GenerationConfig{ + ImageConfig: interactions.ImageConfig{ + AspectRatio: "1:1", + ImageSize: "1K", + }, + MaxOutputTokens: new(0), + Seed: new(0), + SpeechConfig: []interactions.SpeechConfig{{ + Language: "language", + Speaker: "speaker", + Voice: "voice", + }}, + StopSequences: []string{"string"}, + Temperature: new(float64(0)), + ThinkingLevel: interactions.ThinkingLevelMinimal, + ThinkingSummaries: "auto", + ToolChoice: &interactions.GenerationConfigToolChoice{ + ToolChoiceType: interactions.ToolChoiceTypeAuto, + }, + TopP: new(float64(0)), + }, + PreviousInteractionID: "previous_interaction_id", + ResponseFormat: &interactions.NewModelParamsResponseFormat{ + ResponseFormatListArray: interactions.ResponseFormatList{interactions.ResponseFormatListItem{ + AudioResponseFormat: &interactions.AudioResponseFormat{ + BitRate: new(0), + Delivery: "inline", + MimeType: "audio/mp3", + SampleRate: new(0), + }, + }}, + }, + ResponseMimeType: "response_mime_type", + ResponseModalities: []string{"text"}, + ServiceTier: "flex", + Store: new(true), + SystemInstruction: "system_instruction", + Tools: []interactions.Tool{{ + Function: &interactions.Function{ + Description: "description", + Name: "name", + Parameters: map[string]any{}, + }, + }}, + WebhookConfig: interactions.WebhookConfig{ + Uris: []string{"string"}, + UserMetadata: map[string]any{ + "foo": "bar", + }, + }, + }) + if err != nil { + var apierr *interactions.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestInteractionGetWithOptionalParams(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := interactions.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Interactions.Get( + context.TODO(), + "id", + interactions.GetParams{ + APIVersion: "api_version", + IncludeInput: true, + LastEventID: "last_event_id", + }, + ) + if err != nil { + var apierr *interactions.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/interactions/internal/apierror/apierror.go b/interactions/internal/apierror/apierror.go new file mode 100644 index 00000000..f4f3b7f0 --- /dev/null +++ b/interactions/internal/apierror/apierror.go @@ -0,0 +1,69 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package apierror + +import ( + "fmt" + "net/http" + "net/http/httputil" + + "google.golang.org/genai/interactions/internal/apijson" + "google.golang.org/genai/interactions/internal/apijson/unmarshalinfo" + "google.golang.org/genai/interactions/packages/apidata" +) + +// aliased to make [unmarshalinfo.Metadata] private when embedding +type metadata = unmarshalinfo.Metadata + +// Error represents an error that originates from the API, i.e. when a request is +// made and the API returns a response with a HTTP status code. Other errors are +// not wrapped by this SDK. +type Error struct { + StatusCode int + Request *http.Request + Response *http.Response + + // DynamicFields can be used to add, omit, or overwrite fields + apidata.DynamicFields `json:"-" api:"extras"` + metadata +} + +func (e Error) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(e) +} + +func (e *Error) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, e, &e.metadata) +} + +func (r *Error) Error() string { + // Attempt to re-populate the response body + return fmt.Sprintf("%s %q: %d %s %s", r.Request.Method, r.Request.URL, r.Response.StatusCode, http.StatusText(r.Response.StatusCode), r.RawJSON()) +} + +func (r *Error) DumpRequest(body bool) []byte { + if r.Request.GetBody != nil { + r.Request.Body, _ = r.Request.GetBody() + } + out, _ := httputil.DumpRequestOut(r.Request, body) + return out +} + +func (r *Error) DumpResponse(body bool) []byte { + out, _ := httputil.DumpResponse(r.Response, body) + return out +} diff --git a/interactions/internal/apiform/apidata.go b/interactions/internal/apiform/apidata.go new file mode 100644 index 00000000..d767d113 --- /dev/null +++ b/interactions/internal/apiform/apidata.go @@ -0,0 +1,35 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiform + +import ( + "bytes" + "mime/multipart" +) + +func MarshalMultipart(obj any) (data []byte, contentType string, err error) { + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + err = MarshalRoot(obj, writer) + if err != nil { + writer.Close() + return nil, "", err + } + err = writer.Close() + if err != nil { + return nil, "", err + } + return buf.Bytes(), writer.FormDataContentType(), nil +} diff --git a/interactions/internal/apiform/dynamicfield_test.go b/interactions/internal/apiform/dynamicfield_test.go new file mode 100644 index 00000000..472097ba --- /dev/null +++ b/interactions/internal/apiform/dynamicfield_test.go @@ -0,0 +1,297 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiform_test + +import ( + "bytes" + "google.golang.org/genai/interactions/internal/apiform" + "google.golang.org/genai/interactions/internal/apijson/unmarshalinfo" + "google.golang.org/genai/interactions/packages/apidata" + "mime/multipart" + "strings" + "testing" +) + +// ── Types ──────────────────────────────────────────────────────────── + +type simple struct { + Name string `form:"name"` + Value int `form:"value"` + Tag string `form:"tag"` + + apidata.DynamicFields `json:"-" form:"-"` +} + +type nested struct { + Parent string `form:"parent"` + Child simple `form:"child"` + + apidata.DynamicFields `json:"-" form:"-"` +} + +type withInlineMap struct { + Name string `form:"name"` + Extra map[string]any `form:",inline"` + + apidata.DynamicFields `json:"-" form:"-"` +} + +type unionWrapper struct { + Union union `form:"union"` +} + +type union struct { + OfString *string `form:",omitzero,inline"` + OfInt *int `form:",omitzero,inline"` + meta unmarshalinfo.Metadata `api:"union"` +} + +func ptr[T any](v T) *T { return &v } + +// ── Test helpers ───────────────────────────────────────────────────── + +// tForm wraps testing.T with multipart form assertion helpers. +type tForm struct{ *testing.T } + +func (tf tForm) Marshal(val any) formResult { + tf.Helper() + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + if err := writer.SetBoundary("xxx"); err != nil { + tf.Fatalf("SetBoundary: %v", err) + } + if err := apiform.MarshalWithSettings(val, writer, "indices:dots"); err != nil { + tf.Fatalf("MarshalWithSettings: %v", err) + } + if err := writer.Close(); err != nil { + tf.Fatalf("Close: %v", err) + } + return formResult{tf, buf.String()} +} + +func (tf tForm) MarshalErr(val any) error { + tf.Helper() + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + if err := writer.SetBoundary("xxx"); err != nil { + tf.Fatalf("SetBoundary: %v", err) + } + return apiform.MarshalWithSettings(val, writer, "indices:dots") +} + +// formResult holds parsed multipart output for assertions. +type formResult struct { + tForm + raw string +} + +// fields parses Content-Disposition headers into name→value pairs. +func (r formResult) fields() map[string]string { + result := make(map[string]string) + parts := strings.Split(r.raw, "\r\n") + for i, p := range parts { + if strings.HasPrefix(p, "Content-Disposition:") { + nameStart := strings.Index(p, `name="`) + 6 + nameEnd := strings.Index(p[nameStart:], `"`) + nameStart + if i+2 < len(parts) { + result[p[nameStart:nameEnd]] = parts[i+2] + } + } + } + return result +} + +func (r formResult) Has(field, value string) { + r.Helper() + got, ok := r.fields()[field] + if !ok { + r.Errorf("expected field %q present, got:\n%s", field, r.raw) + } else if got != value { + r.Errorf("field %q: expected %q, got %q", field, value, got) + } +} + +func (r formResult) Lacks(field string) { + r.Helper() + if _, ok := r.fields()[field]; ok { + r.Errorf("expected field %q absent, got:\n%s", field, r.raw) + } +} + +func (r formResult) Count(field string) int { + return strings.Count(r.raw, `name="`+field+`"`) +} + +// ── Tests ──────────────────────────────────────────────────────────── + +func TestDynamicFieldsMarshal(t *testing.T) { + tests := []struct { + name string + val any + has map[string]string + lacks []string + }{ + { + name: "extra field added", + val: simple{Name: "a", Value: 1, DynamicFields: apidata.DynamicFields{"extra": "v"}}, + has: map[string]string{"name": "a", "value": "1", "extra": "v"}, + }, + { + name: "Omit removes native field", + val: simple{Name: "a", Value: 1, DynamicFields: apidata.DynamicFields{"name": apidata.Omit}}, + has: map[string]string{"value": "1"}, + lacks: []string{"name"}, + }, + { + name: "Unknown replaces native field with raw value", + val: simple{Name: "a", Value: 1, DynamicFields: apidata.DynamicFields{"name": apidata.Unknown(`42`)}}, + has: map[string]string{"name": "42", "value": "1"}, + }, + { + name: "Unknown replaces with empty string", + val: simple{Name: "will_disappear", Value: 1, DynamicFields: apidata.DynamicFields{"name": apidata.Unknown(``)}}, + has: map[string]string{"name": ""}, + }, + { + name: "Omit on nonexistent field is no-op", + val: simple{Name: "a", DynamicFields: apidata.DynamicFields{"ghost": apidata.Omit}}, + has: map[string]string{"name": "a"}, + lacks: []string{"ghost"}, + }, + { + name: "plain value adds a new field", + val: simple{Name: "a", Value: 1, DynamicFields: apidata.DynamicFields{"new_field": 99}}, + has: map[string]string{"name": "a", "value": "1", "new_field": "99"}, + }, + { + name: "all operations at once", + val: simple{ + Name: "alice", Value: 42, Tag: "original", + DynamicFields: apidata.DynamicFields{ + "name": apidata.Omit, + "value": apidata.Unknown(`9999`), + "added": "new_value", + "ghost": apidata.Omit, + }, + }, + has: map[string]string{"value": "9999", "tag": "original", "added": "new_value"}, + lacks: []string{"name", "ghost"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tf := tForm{t} + r := tf.Marshal(tt.val) + for field, value := range tt.has { + r.Has(field, value) + } + for _, field := range tt.lacks { + r.Lacks(field) + } + }) + } +} + +func TestDynamicFieldsNilVsEmpty(t *testing.T) { + tf := tForm{t} + base := simple{Name: "a", Value: 1, Tag: "t"} + + nilR := tf.Marshal(base) + base.DynamicFields = apidata.DynamicFields{} + emptyR := tf.Marshal(base) + + if nilR.raw != emptyR.raw { + t.Fatalf("nil vs empty differ:\nnil: %q\nempty: %q", nilR.raw, emptyR.raw) + } +} + +func TestDynamicFieldsNestedIsolation(t *testing.T) { + tf := tForm{t} + r := tf.Marshal(nested{ + Parent: "p", + Child: simple{ + Name: "child_name", + Value: 10, + DynamicFields: apidata.DynamicFields{"name": apidata.Omit}, + }, + DynamicFields: apidata.DynamicFields{"injected": "top_level"}, + }) + + r.Has("parent", "p") + r.Has("injected", "top_level") + r.Has("child.value", "10") + r.Lacks("child.name") +} + +func TestDynamicFieldsWithInlineMap(t *testing.T) { + t.Run("both contribute fields", func(t *testing.T) { + tf := tForm{t} + r := tf.Marshal(withInlineMap{ + Name: "native", + Extra: map[string]any{"from_map": "m"}, + DynamicFields: apidata.DynamicFields{"from_raw": "r"}, + }) + r.Has("name", "native") + r.Has("from_map", "m") + r.Has("from_raw", "r") + }) + + t.Run("same key in both produces duplicate", func(t *testing.T) { + tf := tForm{t} + r := tf.Marshal(withInlineMap{ + Name: "native", + Extra: map[string]any{"overlap": "from_map"}, + DynamicFields: apidata.DynamicFields{ + "overlap": "from_raw", + "name": apidata.Omit, + }, + }) + r.Lacks("name") + if n := r.Count("overlap"); n < 2 { + t.Errorf("expected both inline-map and DynamicFields 'overlap', got %d", n) + } + }) +} + +func TestDynamicFieldsUnionMarshal(t *testing.T) { + tests := []struct { + name string + val any + has map[string]string + wantErr bool + }{ + {name: "string member", val: unionWrapper{Union: union{OfString: ptr("hello")}}, has: map[string]string{"union": "hello"}}, + {name: "int member", val: unionWrapper{Union: union{OfInt: ptr(42)}}, has: map[string]string{"union": "42"}}, + {name: "first non-zero wins", val: unionWrapper{Union: union{OfString: ptr("first"), OfInt: ptr(999)}}, has: map[string]string{"union": "first"}}, + {name: "no member set errors", val: unionWrapper{Union: union{}}, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tf := tForm{t} + if tt.wantErr { + if err := tf.MarshalErr(tt.val); err == nil { + t.Fatal("expected error, got nil") + } + return + } + r := tf.Marshal(tt.val) + for field, value := range tt.has { + r.Has(field, value) + } + }) + } +} diff --git a/interactions/internal/apiform/encoder.go b/interactions/internal/apiform/encoder.go new file mode 100644 index 00000000..cefe5fea --- /dev/null +++ b/interactions/internal/apiform/encoder.go @@ -0,0 +1,590 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiform + +import ( + "fmt" + "io" + "maps" + "mime/multipart" + "net/textproto" + "path" + "reflect" + "sort" + "strconv" + "strings" + "sync" + "time" + + "google.golang.org/genai/interactions/internal/apijson/unmarshalinfo" + "google.golang.org/genai/interactions/packages/apidata" +) + +var encoders sync.Map // map[encoderEntry]encoderFunc + +func Marshal(value any, writer *multipart.Writer) error { + e := &encoder{ + dateFormat: time.RFC3339, + arrayFmt: "comma", + } + return e.marshal(value, writer) +} + +// MarshalRoot marshals the multipart form +// MarshalRoot respects the [apidata.DynamicFields] pattern. +func MarshalRoot(value any, writer *multipart.Writer) error { + e := &encoder{ + root: true, + dateFormat: time.RFC3339, + arrayFmt: "comma", + } + return e.marshal(value, writer) +} + +func MarshalWithSettings(value any, writer *multipart.Writer, arrayFormat string) error { + e := &encoder{ + arrayFmt: arrayFormat, + dateFormat: time.RFC3339, + } + return e.marshal(value, writer) +} + +type encoder struct { + arrayFmt string + dateFormat string + root bool +} + +type encoderFunc func(key string, value reflect.Value, writer *multipart.Writer) error + +type encoderField struct { + tag parsedStructTag + fn encoderFunc + idx []int +} + +type encoderEntry struct { + typ reflect.Type + dateFormat string + arrayFmt string + root bool +} + +func (e *encoder) marshal(value any, writer *multipart.Writer) error { + val := reflect.ValueOf(value) + if !val.IsValid() { + return nil + } + typ := val.Type() + enc := e.typeEncoder(typ) + return enc("", val, writer) +} + +func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { + entry := encoderEntry{ + typ: t, + dateFormat: e.dateFormat, + arrayFmt: e.arrayFmt, + root: e.root, + } + + if fi, ok := encoders.Load(entry); ok { + return fi.(encoderFunc) + } + + // To deal with recursive types, populate the map with an + // indirect func before we build it. This type waits on the + // real func (f) to be ready and then calls it. This indirect + // func is only used for recursive types. + var ( + wg sync.WaitGroup + f encoderFunc + ) + wg.Add(1) + fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(key string, v reflect.Value, writer *multipart.Writer) error { + wg.Wait() + return f(key, v, writer) + })) + if loaded { + return fi.(encoderFunc) + } + + // Compute the real encoder and replace the indirect func with it. + f = e.newTypeEncoder(t) + wg.Done() + encoders.Store(entry, f) + return f +} + +func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc { + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return e.newTimeTypeEncoder() + } + if t.Implements(reflect.TypeOf((*io.Reader)(nil)).Elem()) { + return e.newReaderTypeEncoder() + } + e.root = false + switch t.Kind() { + case reflect.Pointer: + inner := t.Elem() + + innerEncoder := e.typeEncoder(inner) + return func(key string, v reflect.Value, writer *multipart.Writer) error { + if !v.IsValid() || v.IsNil() { + return nil + } + return innerEncoder(key, v.Elem(), writer) + } + case reflect.Struct: + return e.newStructTypeEncoder(t) + case reflect.Slice, reflect.Array: + return e.newArrayTypeEncoder(t) + case reflect.Map: + return e.newMapEncoder(t) + case reflect.Interface: + return e.newInterfaceEncoder() + default: + return e.newPrimitiveTypeEncoder(t) + } +} + +func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { + switch t.Kind() { + // Note that we could use `gjson` to encode these types but it would complicate our + // code more and this current code shouldn't cause any issues + case reflect.String: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, v.String()) + } + case reflect.Bool: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + if v.Bool() { + return writer.WriteField(key, "true") + } + return writer.WriteField(key, "false") + } + case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, strconv.FormatInt(v.Int(), 10)) + } + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, strconv.FormatUint(v.Uint(), 10)) + } + case reflect.Float32: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 32)) + } + case reflect.Float64: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 64)) + } + default: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return fmt.Errorf("unknown type received at primitive encoder: %s", t.String()) + } + } +} + +func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { + itemEncoder := e.typeEncoder(t.Elem()) + keyFn := e.arrayKeyEncoder() + if e.arrayFmt == "comma" { + return func(key string, v reflect.Value, writer *multipart.Writer) error { + if v.Len() == 0 { + return nil + } + elements := make([]string, v.Len()) + for i := 0; i < v.Len(); i++ { + elements[i] = fmt.Sprint(v.Index(i).Interface()) + } + return writer.WriteField(key, strings.Join(elements, ",")) + } + } + return func(key string, v reflect.Value, writer *multipart.Writer) error { + if keyFn == nil { + return fmt.Errorf("apiform: unsupported array format") + } + for i := 0; i < v.Len(); i++ { + err := itemEncoder(keyFn(key, i), v.Index(i), writer) + if err != nil { + return err + } + } + return nil + } +} + +var extraFieldsType = reflect.TypeOf(apidata.DynamicFields(nil)) + +func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { + if unmarshalinfo.IsUnion(t) { + return e.newStructUnionTypeEncoder(t) + } + + encoderFields := []encoderField{} + extraEncoder := (*encoderField)(nil) + + // This helper allows us to recursively collect field encoders into a flat + // array. The parameter `index` keeps track of the access patterns necessary + // to get to some field. + var collectEncoderFields func(r reflect.Type, index []int) + collectEncoderFields = func(r reflect.Type, index []int) { + for i := 0; i < r.NumField(); i++ { + idx := append(index, i) + field := t.FieldByIndex(idx) + if !field.IsExported() { + continue + } + // If this is an embedded struct, traverse one level deeper to extract + // the field and get their encoders as well. + if field.Anonymous && field.Type.Kind() == reflect.Struct { + collectEncoderFields(field.Type, idx) + continue + } + // If json tag is not present, then we skip, which is intentionally + // different behavior from the stdlib. + ptag, ok := parseFormStructTag(field) + if !ok { + continue + } + // Inline fields encode their contents at the parent's key level + // rather than nesting under a child key. There are two cases: + // + // 1. Root-level inline maps (additional properties): + // + // type Params struct { + // Name string `form:"name"` + // Extras map[string]any `form:",inline"` + // } + // + // The map entries are written as sibling form fields via + // encodeMapEntries after all struct fields. This uses the + // separate extraEncoder path and only applies at the root + // struct level (len(index) == 0) — an inline map inherited + // from an embedded struct falls through to case 2. + // + // 2. Inline structs, pointers, and non-root inline maps: + // + // type Params struct { + // Name string `form:"name"` + // Inner Inner `form:",inline"` + // } + // + // The field's type encoder is called with the parent's key, + // so its contents are encoded at the same level. Supports + // omitzero to skip the field entirely when zero-valued. + if ptag.inline { + ft := field.Type + for ft.Kind() == reflect.Pointer { + ft = ft.Elem() + } + // len(index) == 0: only the root struct's inline map gets the + // dedicated extraEncoder path; an inline map inherited from an + // embedded struct (depth > 0) falls through to the general case. + if ft.Kind() == reflect.Map && len(index) == 0 { + extraEncoder = &encoderField{ptag, e.typeEncoder(field.Type.Elem()), idx} + } else { + encoderFn := e.typeEncoder(field.Type) + if ptag.omitzero { + base := encoderFn + encoderFn = func(key string, value reflect.Value, writer *multipart.Writer) error { + if value.IsZero() { + return nil + } + return base(key, value, writer) + } + } + encoderFields = append(encoderFields, encoderField{ptag, encoderFn, idx}) + } + continue + } + if ptag.name == "-" || ptag.name == "" { + continue + } + + dateFormat, ok := parseFormatStructTag(field) + oldFormat := e.dateFormat + if ok { + switch dateFormat { + case "date-time": + e.dateFormat = time.RFC3339 + case "date": + e.dateFormat = "2006-01-02" + } + } + + var encoderFn encoderFunc + if ptag.omitzero { + typeEncoderFn := e.typeEncoder(field.Type) + encoderFn = func(key string, value reflect.Value, writer *multipart.Writer) error { + if value.IsZero() { + return nil + } + return typeEncoderFn(key, value, writer) + } + } else if ptag.defaultValue != nil { + typeEncoderFn := e.typeEncoder(field.Type) + encoderFn = func(key string, value reflect.Value, writer *multipart.Writer) error { + if value.IsZero() { + return typeEncoderFn(key, reflect.ValueOf(ptag.defaultValue), writer) + } + return typeEncoderFn(key, value, writer) + } + } else { + encoderFn = e.typeEncoder(field.Type) + } + encoderFields = append(encoderFields, encoderField{ptag, encoderFn, idx}) + e.dateFormat = oldFormat + } + } + collectEncoderFields(t, []int{}) + + // Ensure deterministic output by sorting by lexicographic order + sort.Slice(encoderFields, func(i, j int) bool { + return encoderFields[i].tag.name < encoderFields[j].tag.name + }) + + extraFieldsIdx := unmarshalinfo.DynamicFieldsIndex(t) + + // Build a set of native field names for quick lookup when classifying extras. + nativeNames := make(map[string]bool, len(encoderFields)) + for _, ef := range encoderFields { + nativeNames[ef.tag.name] = true + } + + return func(key string, value reflect.Value, writer *multipart.Writer) error { + keyFn := e.objKeyEncoder(key) + + // Clone this so we can remove replacement fields and be left with only new fields at the end + extrasFieldValue, _ := value.FieldByIndex(extraFieldsIdx).Interface().(apidata.DynamicFields) + extras := maps.Clone(extrasFieldValue) + for _, ef := range encoderFields { + if extra, ok := extras[ef.tag.name]; ok { + // This field has already been accounted for and doesn't need to be encoded later on + delete(extras, ef.tag.name) + if extra == apidata.Omit { + continue + } + if err := writer.WriteField(keyFn(ef.tag.name), string(extra.(apidata.Unknown))); err != nil { + return err + } + } else { + field := value.FieldByIndex(ef.idx) + if err := ef.fn(keyFn(ef.tag.name), field, writer); err != nil { + return err + } + } + } + + if err := e.encodeExtraFields(extras, keyFn, writer); err != nil { + return err + } + + if extraEncoder != nil { + err := e.encodeMapEntries(key, value.FieldByIndex(extraEncoder.idx), writer) + if err != nil { + return err + } + } + + return nil + } +} + +// encodeExtraFields writes extra fields (from DynamicFields) to the multipart +// writer in sorted order for deterministic output. +func (e *encoder) encodeExtraFields(extras map[string]any, keyFn func(string) string, writer *multipart.Writer) error { + if len(extras) == 0 { + return nil + } + keys := make([]string, 0, len(extras)) + for k := range extras { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + val := extras[k] + var err error + if raw, ok := val.(apidata.Unknown); ok { + err = writer.WriteField(keyFn(k), string(raw)) + } else if val != apidata.Omit { + v := reflect.ValueOf(val) + err = e.typeEncoder(v.Type())(keyFn(k), v, writer) + } + if err != nil { + return err + } + } + return nil +} + +var metadataType = reflect.TypeOf(unmarshalinfo.Metadata{}) + +func (e *encoder) newStructUnionTypeEncoder(t reflect.Type) encoderFunc { + var fieldEncoders []encoderFunc + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if field.Type == metadataType { + fieldEncoders = append(fieldEncoders, nil) + } else { + fieldEncoders = append(fieldEncoders, e.typeEncoder(field.Type)) + } + } + + return func(key string, value reflect.Value, writer *multipart.Writer) error { + for i := 0; i < t.NumField(); i++ { + if t.Field(i).Type == metadataType { + continue + } + if !value.Field(i).IsZero() { + return fieldEncoders[i](key, value.Field(i), writer) + } + } + return fmt.Errorf("apiform: union %s has no field set", t.String()) + } +} + +func (e *encoder) newTimeTypeEncoder() encoderFunc { + format := e.dateFormat + return func(key string, value reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format)) + } +} + +func (e encoder) newInterfaceEncoder() encoderFunc { + return func(key string, value reflect.Value, writer *multipart.Writer) error { + value = value.Elem() + if !value.IsValid() { + return nil + } + return e.typeEncoder(value.Type())(key, value, writer) + } +} + +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + +func escapeQuotes(s string) string { + return quoteEscaper.Replace(s) +} + +func (e *encoder) newReaderTypeEncoder() encoderFunc { + return func(key string, value reflect.Value, writer *multipart.Writer) error { + reader, ok := value.Convert(reflect.TypeOf((*io.Reader)(nil)).Elem()).Interface().(io.Reader) + if !ok { + return nil + } + filename := "anonymous_file" + contentType := "application/octet-stream" + if named, ok := reader.(interface{ Filename() string }); ok { + filename = named.Filename() + } else if named, ok := reader.(interface{ Name() string }); ok { + filename = path.Base(named.Name()) + } + if typed, ok := reader.(interface{ ContentType() string }); ok { + contentType = typed.ContentType() + } + + // Below is taken almost 1-for-1 from [multipart.CreateFormFile] + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(key), escapeQuotes(filename))) + h.Set("Content-Type", contentType) + filewriter, err := writer.CreatePart(h) + if err != nil { + return err + } + _, err = io.Copy(filewriter, reader) + return err + } +} + +func (e encoder) arrayKeyEncoder() func(string, int) string { + var keyFn func(string, int) string + switch e.arrayFmt { + case "comma", "repeat": + keyFn = func(k string, _ int) string { return k } + case "brackets": + keyFn = func(key string, _ int) string { return key + "[]" } + case "indices:dots": + keyFn = func(k string, i int) string { + if k == "" { + return strconv.Itoa(i) + } + return k + "." + strconv.Itoa(i) + } + case "indices:brackets": + keyFn = func(k string, i int) string { + if k == "" { + return strconv.Itoa(i) + } + return k + "[" + strconv.Itoa(i) + "]" + } + } + return keyFn +} + +func (e encoder) objKeyEncoder(parent string) func(string) string { + if parent == "" { + return func(child string) string { return child } + } + switch e.arrayFmt { + case "brackets": + return func(child string) string { return parent + "[" + child + "]" } + default: + return func(child string) string { return parent + "." + child } + } +} + +// Given a []byte of json (may either be an empty object or an object that already contains entries) +// encode all of the entries in the map to the json byte array. +func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipart.Writer) error { + type mapPair struct { + key string + value reflect.Value + } + + pairs := []mapPair{} + + iter := v.MapRange() + for iter.Next() { + if iter.Key().Type().Kind() == reflect.String { + pairs = append(pairs, mapPair{key: iter.Key().String(), value: iter.Value()}) + } else { + return fmt.Errorf("cannot encode a map with a non string key") + } + } + + // Ensure deterministic output + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].key < pairs[j].key + }) + + elementEncoder := e.typeEncoder(v.Type().Elem()) + keyFn := e.objKeyEncoder(key) + for _, p := range pairs { + err := elementEncoder(keyFn(p.key), p.value, writer) + if err != nil { + return err + } + } + + return nil +} + +func (e *encoder) newMapEncoder(_ reflect.Type) encoderFunc { + return func(key string, value reflect.Value, writer *multipart.Writer) error { + return e.encodeMapEntries(key, value, writer) + } +} diff --git a/interactions/internal/apiform/form.go b/interactions/internal/apiform/form.go new file mode 100644 index 00000000..962e686b --- /dev/null +++ b/interactions/internal/apiform/form.go @@ -0,0 +1,19 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiform + +type Marshaler interface { + MarshalMultipart() ([]byte, string, error) +} diff --git a/interactions/internal/apiform/form_test.go b/interactions/internal/apiform/form_test.go new file mode 100644 index 00000000..ac7ed7bf --- /dev/null +++ b/interactions/internal/apiform/form_test.go @@ -0,0 +1,662 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiform + +import ( + "bytes" + "google.golang.org/genai/interactions/internal/apijson/unmarshalinfo" + "io" + "mime/multipart" + "strings" + "testing" + "time" +) + +func P[T any](v T) *T { return &v } + +type Primitives struct { + A bool `form:"a"` + B int `form:"b"` + C uint `form:"c"` + D float64 `form:"d"` + E float32 `form:"e"` + F []int `form:"f"` +} + +// These aliases are necessary to bypass the cache. +// This only relevant during testing. +type int_ int +type PrimitivesBrackets struct { + F []int_ `form:"f"` +} + +type PrimitivePointers struct { + A *bool `form:"a"` + B *int `form:"b"` + C *uint `form:"c"` + D *float64 `form:"d"` + E *float32 `form:"e"` + F *[]int `form:"f"` +} + +type Slices struct { + Slice []Primitives `form:"slices"` +} + +type DateTime struct { + Date time.Time `form:"date" format:"date"` + DateTime time.Time `form:"date-time" format:"date-time"` +} + +type AdditionalProperties struct { + A bool `form:"a"` + Extras map[string]any `form:",inline"` +} + +type TypedAdditionalProperties struct { + A bool `form:"a"` + Extras map[string]int `form:",inline"` +} + +type EmbeddedStructs struct { + AdditionalProperties + A *int `form:"number2"` + Extras map[string]any `form:",inline"` +} + +type Recursive struct { + Name string `form:"name"` + Child *Recursive `form:"child"` +} + +type UnknownStruct struct { + Unknown any `form:"unknown"` +} + +type UnionStruct struct { + Union Union `form:"union" format:"date"` +} + +type Union interface { + union() +} + +type UnionInteger int64 + +func (UnionInteger) union() {} + +type UnionStructA struct { + Type string `form:"type"` + A string `form:"a"` + B string `form:"b"` +} + +func (UnionStructA) union() {} + +type UnionStructB struct { + Type string `form:"type"` + A string `form:"a"` +} + +func (UnionStructB) union() {} + +type UnionTime time.Time + +func (UnionTime) union() {} + +type ReaderStruct struct { + File io.Reader `form:"file"` +} + +type NamedEnum string + +const NamedEnumFoo NamedEnum = "foo" + +type StructUnionWrapper struct { + Union StructUnion `form:"union"` +} + +type StructUnion struct { + OfInt *int64 `form:",omitzero,inline"` + OfString *string `form:",omitzero,inline"` + OfEnum *NamedEnum `form:",omitzero,inline"` + OfA UnionStructA `form:",omitzero,inline"` + OfB UnionStructB `form:",omitzero,inline"` + meta unmarshalinfo.Metadata `api:"union"` +} + +type ConstantStruct struct { + Anchor string `form:"anchor" default:"created_at"` + Seconds int `form:"seconds"` +} + +type MultipartMarshalerParent struct { + Middle MultipartMarshalerMiddleNext `form:"middle"` +} + +type MultipartMarshalerMiddleNext struct { + MiddleNext MultipartMarshalerMiddle `form:"middleNext"` +} + +type MultipartMarshalerMiddle struct { + Child int `form:"child"` +} + +var tests = map[string]struct { + buf string + val any +}{ + "file": { + buf: `--xxx +Content-Disposition: form-data; name="file"; filename="anonymous_file" +Content-Type: application/octet-stream + +some file contents... +--xxx-- +`, + val: ReaderStruct{ + File: io.Reader(bytes.NewBuffer([]byte("some file contents..."))), + }, + }, + "map_string": { + `--xxx +Content-Disposition: form-data; name="foo" + +bar +--xxx-- +`, + map[string]string{"foo": "bar"}, + }, + + "map_interface": { + `--xxx +Content-Disposition: form-data; name="a" + +1 +--xxx +Content-Disposition: form-data; name="b" + +str +--xxx +Content-Disposition: form-data; name="c" + +false +--xxx-- +`, + map[string]any{"a": float64(1), "b": "str", "c": false}, + }, + + "primitive_struct": { + `--xxx +Content-Disposition: form-data; name="a" + +false +--xxx +Content-Disposition: form-data; name="b" + +237628372683 +--xxx +Content-Disposition: form-data; name="c" + +654 +--xxx +Content-Disposition: form-data; name="d" + +9999.43 +--xxx +Content-Disposition: form-data; name="e" + +43.76 +--xxx +Content-Disposition: form-data; name="f.0" + +1 +--xxx +Content-Disposition: form-data; name="f.1" + +2 +--xxx +Content-Disposition: form-data; name="f.2" + +3 +--xxx +Content-Disposition: form-data; name="f.3" + +4 +--xxx-- +`, + Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + }, + "primitive_struct,brackets": { + `--xxx +Content-Disposition: form-data; name="f[]" + +1 +--xxx +Content-Disposition: form-data; name="f[]" + +2 +--xxx +Content-Disposition: form-data; name="f[]" + +3 +--xxx +Content-Disposition: form-data; name="f[]" + +4 +--xxx-- +`, + PrimitivesBrackets{F: []int_{1, 2, 3, 4}}, + }, + + "slices": { + `--xxx +Content-Disposition: form-data; name="slices.0.a" + +false +--xxx +Content-Disposition: form-data; name="slices.0.b" + +237628372683 +--xxx +Content-Disposition: form-data; name="slices.0.c" + +654 +--xxx +Content-Disposition: form-data; name="slices.0.d" + +9999.43 +--xxx +Content-Disposition: form-data; name="slices.0.e" + +43.76 +--xxx +Content-Disposition: form-data; name="slices.0.f.0" + +1 +--xxx +Content-Disposition: form-data; name="slices.0.f.1" + +2 +--xxx +Content-Disposition: form-data; name="slices.0.f.2" + +3 +--xxx +Content-Disposition: form-data; name="slices.0.f.3" + +4 +--xxx-- +`, + Slices{ + Slice: []Primitives{{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}}, + }, + }, + "primitive_pointer_struct": { + `--xxx +Content-Disposition: form-data; name="a" + +false +--xxx +Content-Disposition: form-data; name="b" + +237628372683 +--xxx +Content-Disposition: form-data; name="c" + +654 +--xxx +Content-Disposition: form-data; name="d" + +9999.43 +--xxx +Content-Disposition: form-data; name="e" + +43.76 +--xxx +Content-Disposition: form-data; name="f.0" + +1 +--xxx +Content-Disposition: form-data; name="f.1" + +2 +--xxx +Content-Disposition: form-data; name="f.2" + +3 +--xxx +Content-Disposition: form-data; name="f.3" + +4 +--xxx +Content-Disposition: form-data; name="f.4" + +5 +--xxx-- +`, + PrimitivePointers{ + A: P(false), + B: P(237628372683), + C: P(uint(654)), + D: P(9999.43), + E: P(float32(43.76)), + F: &[]int{1, 2, 3, 4, 5}, + }, + }, + + "datetime_struct": { + `--xxx +Content-Disposition: form-data; name="date" + +2006-01-02 +--xxx +Content-Disposition: form-data; name="date-time" + +2006-01-02T15:04:05Z +--xxx-- +`, + DateTime{ + Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC), + DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC), + }, + }, + + "additional_properties": { + `--xxx +Content-Disposition: form-data; name="a" + +true +--xxx +Content-Disposition: form-data; name="bar" + +value +--xxx +Content-Disposition: form-data; name="foo" + +true +--xxx-- +`, + AdditionalProperties{ + A: true, + Extras: map[string]any{ + "bar": "value", + "foo": true, + }, + }, + }, + "recursive_struct,brackets": { + `--xxx +Content-Disposition: form-data; name="child[name]" + +Alex +--xxx +Content-Disposition: form-data; name="name" + +Robert +--xxx-- +`, + Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}}, + }, + + "recursive_struct": { + `--xxx +Content-Disposition: form-data; name="child.name" + +Alex +--xxx +Content-Disposition: form-data; name="name" + +Robert +--xxx-- +`, + Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}}, + }, + + "unknown_struct_number": { + `--xxx +Content-Disposition: form-data; name="unknown" + +12 +--xxx-- +`, + UnknownStruct{ + Unknown: 12., + }, + }, + + "unknown_struct_map": { + `--xxx +Content-Disposition: form-data; name="unknown.foo" + +bar +--xxx-- +`, + UnknownStruct{ + Unknown: map[string]any{ + "foo": "bar", + }, + }, + }, + + "struct_union_integer": { + `--xxx +Content-Disposition: form-data; name="union" + +12 +--xxx-- +`, + StructUnionWrapper{ + Union: StructUnion{OfInt: P(int64(12))}, + }, + }, + + "union_integer": { + `--xxx +Content-Disposition: form-data; name="union" + +12 +--xxx-- +`, + UnionStruct{ + Union: UnionInteger(12), + }, + }, + + "struct_union_struct_discriminated_a": { + `--xxx +Content-Disposition: form-data; name="union.a" + +foo +--xxx +Content-Disposition: form-data; name="union.b" + +bar +--xxx +Content-Disposition: form-data; name="union.type" + +typeA +--xxx-- +`, + StructUnionWrapper{ + Union: StructUnion{OfA: UnionStructA{ + Type: "typeA", + A: "foo", + B: "bar", + }}, + }, + }, + + "union_struct_discriminated_a": { + `--xxx +Content-Disposition: form-data; name="union.a" + +foo +--xxx +Content-Disposition: form-data; name="union.b" + +bar +--xxx +Content-Disposition: form-data; name="union.type" + +typeA +--xxx-- +`, + + UnionStruct{ + Union: UnionStructA{ + Type: "typeA", + A: "foo", + B: "bar", + }, + }, + }, + + "struct_union_struct_discriminated_b": { + `--xxx +Content-Disposition: form-data; name="union.a" + +foo +--xxx +Content-Disposition: form-data; name="union.type" + +typeB +--xxx-- +`, + StructUnionWrapper{ + Union: StructUnion{OfB: UnionStructB{ + Type: "typeB", + A: "foo", + }}, + }, + }, + + "union_struct_discriminated_b": { + `--xxx +Content-Disposition: form-data; name="union.a" + +foo +--xxx +Content-Disposition: form-data; name="union.type" + +typeB +--xxx-- +`, + UnionStruct{ + Union: UnionStructB{ + Type: "typeB", + A: "foo", + }, + }, + }, + + "union_struct_time": { + `--xxx +Content-Disposition: form-data; name="union" + +2010-05-23 +--xxx-- +`, + UnionStruct{ + Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)), + }, + }, + "constant_zero_value": { + `--xxx +Content-Disposition: form-data; name="anchor" + +created_at +--xxx +Content-Disposition: form-data; name="seconds" + +3600 +--xxx-- +`, + ConstantStruct{ + Seconds: 3600, + }, + }, + "constant_explicit_value": { + `--xxx +Content-Disposition: form-data; name="anchor" + +created_at_override +--xxx +Content-Disposition: form-data; name="seconds" + +3600 +--xxx-- +`, + ConstantStruct{ + Anchor: "created_at_override", + Seconds: 3600, + }, + }, + "deeply-nested-struct,brackets": { + `--xxx +Content-Disposition: form-data; name="middle[middleNext][child]" + +10 +--xxx-- +`, + MultipartMarshalerParent{ + Middle: MultipartMarshalerMiddleNext{ + MiddleNext: MultipartMarshalerMiddle{ + Child: 10, + }, + }, + }, + }, + "deeply-nested-map,brackets": { + `--xxx +Content-Disposition: form-data; name="middle[middleNext][child]" + +10 +--xxx-- +`, + map[string]any{"middle": map[string]any{"middleNext": map[string]any{"child": 10}}}, + }, +} + +func TestEncode(t *testing.T) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + err := writer.SetBoundary("xxx") + if err != nil { + t.Errorf("setting boundary for %v failed with error %v", test.val, err) + } + + arrayFmt := "indices:dots" + if tags := strings.Split(name, ","); len(tags) > 1 { + arrayFmt = tags[1] + } + + err = MarshalWithSettings(test.val, writer, arrayFmt) + if err != nil { + t.Errorf("serialization of %v failed with error %v", test.val, err) + } + err = writer.Close() + if err != nil { + t.Errorf("serialization of %v failed with error %v", test.val, err) + } + raw := buf.Bytes() + if string(raw) != strings.ReplaceAll(test.buf, "\n", "\r\n") { + t.Errorf("expected %+#v to serialize to '%s' but got '%s' (with format %s)", test.val, test.buf, string(raw), arrayFmt) + } + }) + } +} diff --git a/interactions/internal/apiform/inline_test.go b/interactions/internal/apiform/inline_test.go new file mode 100644 index 00000000..c5768ddb --- /dev/null +++ b/interactions/internal/apiform/inline_test.go @@ -0,0 +1,188 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiform_test + +import ( + "testing" + + "google.golang.org/genai/interactions/packages/apidata" +) + +func TestInline(t *testing.T) { + type Inner struct { + X string `form:"x"` + Y int `form:"y"` + } + type InlineStruct struct { + A string `form:"a"` + Inner Inner `form:",inline"` + } + type InlinePtr struct { + A string `form:"a"` + Inner *Inner `form:",inline"` + } + type InlineOmitzero struct { + A string `form:"a"` + Inner Inner `form:",inline,omitzero"` + } + type InlineMapAndRaw struct { + Name string `form:"name"` + Extras map[string]any `form:",inline"` + apidata.DynamicFields + } + type EmbedInner struct { + A bool `form:"a"` + Extras map[string]any `form:",inline"` + } + type EmbedOuter struct { + B string `form:"b"` + EmbedInner + } + + tests := []struct { + name string + val any + has map[string]string + lacks []string + sameAs string // if set, output must match the test with this name + }{ + // ── Inline map ────────────────────────────────────────────── + { + name: "inline map spreads entries as sibling fields", + val: withInlineMap{Name: "alice", Extra: map[string]any{"foo": true, "bar": "value"}}, + has: map[string]string{"name": "alice", "foo": "true", "bar": "value"}, + }, + { + name: "typed inline map", + val: struct { + A bool `form:"a"` + Extras map[string]int `form:",inline"` + }{A: true, Extras: map[string]int{"count": 42}}, + has: map[string]string{"a": "true", "count": "42"}, + }, + { + name: "empty inline map — baseline", + val: withInlineMap{Name: "a"}, + }, + { + name: "nil inline map matches baseline", + val: withInlineMap{Name: "a", Extra: nil}, + sameAs: "empty inline map — baseline", + }, + { + name: "empty inline map matches baseline", + val: withInlineMap{Name: "a", Extra: map[string]any{}}, + sameAs: "empty inline map — baseline", + }, + + // ── Inline struct ─────────────────────────────────────────── + { + name: "inline struct spreads fields into parent", + val: InlineStruct{A: "hello", Inner: Inner{X: "val", Y: 42}}, + has: map[string]string{"a": "hello", "x": "val", "y": "42"}, + lacks: []string{"inner"}, + }, + { + name: "inline struct spreads fields into parent", + val: InlineStruct{A: "hello", Inner: Inner{X: "", Y: 0}}, + has: map[string]string{"a": "hello", "x": "", "y": "0"}, + lacks: []string{"inner"}, + }, + { + name: "inline pointer to struct", + val: InlinePtr{A: "hello", Inner: &Inner{X: "val", Y: 42}}, + has: map[string]string{"a": "hello", "x": "val", "y": "42"}, + lacks: []string{"inner"}, + }, + { + name: "nil inline pointer produces no inner fields", + val: InlinePtr{A: "hello", Inner: nil}, + has: map[string]string{"a": "hello"}, + lacks: []string{"x", "y"}, + }, + { + name: "inline struct with omitzero skips when zero", + val: InlineOmitzero{A: "hello"}, + has: map[string]string{"a": "hello"}, + lacks: []string{"x", "y"}, + }, + + // ── Inline map + embedded struct ──────────────────────────── + { + name: "inner embedded inline map entries appear at top level", + val: EmbedOuter{ + EmbedInner: EmbedInner{A: true, Extras: map[string]any{"inner_extra": "works"}}, + B: "hello", + }, + has: map[string]string{"a": "true", "b": "hello", "inner_extra": "works"}, + }, + + // ── Inline map + DynamicFields coexistence ────────────────────── + { + name: "inline map and DynamicFields both contribute", + val: InlineMapAndRaw{ + Name: "alice", + Extras: map[string]any{"from_map": "yes"}, + DynamicFields: apidata.DynamicFields{"from_raw": "also_yes"}, + }, + has: map[string]string{"name": "alice", "from_map": "yes", "from_raw": "also_yes"}, + }, + { + name: "DynamicFields Omit removes native field alongside inline map", + val: InlineMapAndRaw{ + Name: "alice", + Extras: map[string]any{"x": "1"}, + DynamicFields: apidata.DynamicFields{"name": apidata.Omit}, + }, + has: map[string]string{"x": "1"}, + lacks: []string{"name"}, + }, + { + name: "DynamicFields Unknown replaces native field alongside inline map", + val: InlineMapAndRaw{ + Name: "alice", + Extras: map[string]any{"x": "1"}, + DynamicFields: apidata.DynamicFields{"name": apidata.Unknown(`bob`)}, + }, + has: map[string]string{"name": "bob", "x": "1"}, + }, + } + + tf := tForm{t} + results := map[string]formResult{} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tf.Marshal(tt.val) + results[tt.name] = r + + for field, value := range tt.has { + r.Has(field, value) + } + for _, field := range tt.lacks { + r.Lacks(field) + } + if tt.sameAs != "" { + baseline, ok := results[tt.sameAs] + if !ok { + t.Fatalf("sameAs %q not found (must appear earlier in table)", tt.sameAs) + } + if r.raw != baseline.raw { + t.Errorf("expected output identical to %q\ngot: %q\nwant: %q", tt.sameAs, r.raw, baseline.raw) + } + } + }) + } +} diff --git a/interactions/internal/apiform/tag.go b/interactions/internal/apiform/tag.go new file mode 100644 index 00000000..e116d621 --- /dev/null +++ b/interactions/internal/apiform/tag.go @@ -0,0 +1,93 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiform + +import ( + "reflect" + "strings" +) + +const apiStructTag = "api" +const jsonStructTag = "json" +const formStructTag = "form" +const formatStructTag = "format" +const defaultStructTag = "default" + +type parsedStructTag struct { + name string + required bool + inline bool + omitzero bool + defaultValue any +} + +func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { + raw, ok := field.Tag.Lookup(formStructTag) + if !ok { + raw, ok = field.Tag.Lookup(jsonStructTag) + } + if !ok { + return tag, ok + } + parts := strings.Split(raw, ",") + if len(parts) == 0 { + return tag, false + } + tag.name = parts[0] + for _, part := range parts[1:] { + switch part { + case "inline": + tag.inline = true + case "omitzero": + tag.omitzero = true + } + } + + parseApiStructTag(field, &tag) + parseDefaultStructTag(field, &tag) + return tag, ok +} + +func parseDefaultStructTag(field reflect.StructField, tag *parsedStructTag) { + if field.Type.Kind() != reflect.String { + // Only strings are currently supported + return + } + + raw, ok := field.Tag.Lookup(defaultStructTag) + if !ok { + return + } + tag.defaultValue = raw +} + +func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { + raw, ok := field.Tag.Lookup(apiStructTag) + if !ok { + return + } + parts := strings.Split(raw, ",") + for _, part := range parts { + switch part { + case "required": + tag.required = true + } + } +} + +func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { + format, ok = field.Tag.Lookup(formatStructTag) + return format, ok +} diff --git a/interactions/internal/apijson/apidata.go b/interactions/internal/apijson/apidata.go new file mode 100644 index 00000000..f979c723 --- /dev/null +++ b/interactions/internal/apijson/apidata.go @@ -0,0 +1,143 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// EDIT(begin): marshal APIData +package apijson + +import ( + "fmt" + "reflect" + "sort" + "strings" + + "google.golang.org/genai/interactions/internal/apijson/unmarshalinfo" + "google.golang.org/genai/interactions/packages/apidata" + + "github.com/tidwall/sjson" +) + +// sjsonPathEscaper escapes the metacharacters that sjson/gjson paths +// interpret: `.` (nested path), `*` / `?` (wildcards), `#` (array +// op), `|` (modifier), and `\` (escape). A DynamicFields key that +// contains any of these would otherwise be silently reshaped or +// dropped when spliced via sjson.Set*/DeleteBytes. +var sjsonPathEscaper = strings.NewReplacer( + `\`, `\\`, + `.`, `\.`, + `*`, `\*`, + `?`, `\?`, + `#`, `\#`, + `|`, `\|`, +) + +func escapeSjsonKey(k string) string { return sjsonPathEscaper.Replace(k) } + +// MarshalRoot marshals an object to JSON, using the [apidata.DynamicFields] pattern. +// +// Unlike [Marshal], this skips the [Marshaler] interface check at the root level, +// so it can be called from within a type's MarshalJSON without infinite recursion. +func MarshalRoot(obj any) ([]byte, error) { + result, err := marshalRoot(obj) + if err != nil { + return nil, err + } + + DynamicFieldsIdx := unmarshalinfo.DynamicFieldsIndex(reflect.TypeOf(obj)) + if DynamicFieldsIdx != nil { + if reflect.ValueOf(obj).FieldByIndex(DynamicFieldsIdx).IsZero() { + return result, nil + } + + extras, ok := reflect.ValueOf(obj).FieldByIndex(DynamicFieldsIdx).Interface().(apidata.DynamicFields) + if !ok { + return result, nil + } + + // Handle [apidata.ExtraFields] overrides + if len(extras) == 0 { + return result, nil + } + + keys := make([]string, 0, len(extras)) + for k := range extras { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + v := extras[k] + + // [apidata.Mismatch] shouldn't be marshaled for security reasons. + if _, mismatch := v.(apidata.Mismatch); mismatch { + continue + } + + // sjson rejects empty paths ("path cannot be empty"). + // Just skip empty keys for now + if k == "" { + continue + } + + path := escapeSjsonKey(k) + if v == apidata.Omit { + result, err = sjson.DeleteBytes(result, path) + } else if raw, ok := v.(apidata.Unknown); ok { + result, err = sjson.SetRawBytes(result, path, []byte(raw)) + } else { + result, err = sjson.SetBytes(result, path, v) + } + if err != nil { + return nil, err + } + } + } + + return result, nil +} + +// MarshalUnionStruct marshals the first non-zero member of the struct +func MarshalUnionStruct(union any) ([]byte, error) { + v := reflect.ValueOf(union) + // De-ref pointers + for v.Kind() == reflect.Pointer { + v = v.Elem() + } + if v.Kind() != reflect.Struct { + return nil, fmt.Errorf("union must be a struct") + } + t := v.Type() + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + fieldType := t.Field(i) + if !fieldType.IsExported() || !field.IsValid() || field.IsZero() { + continue + } + return Marshal(field.Interface()) + } + return nil, fmt.Errorf("no union members set") +} + +// UnmarshalRoot unmarshals raw JSON bytes into the target, skipping the +// UnmarshalJSON interface check at the root level so it can be called from +// within a type's UnmarshalJSON without infinite recursion. +// +// UnmarshalRoot respects the [apidata.DynamicFields] pattern and the [unmarshalinfo.Metadata] pattern. +func UnmarshalRoot(raw []byte, target any, data *unmarshalinfo.Metadata) error { + score, err := unmarshalRootWithScore(raw, target) + if err == nil { + unmarshalinfo.SetUnmarshalState(raw, score, data) + } + return err +} + +// EDIT(end): marshal APIData diff --git a/interactions/internal/apijson/decode.go b/interactions/internal/apijson/decode.go new file mode 100644 index 00000000..f2490d05 --- /dev/null +++ b/interactions/internal/apijson/decode.go @@ -0,0 +1,1521 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Vendored from Go 1.24.0-pre-release +// To find alterations, check package shims, and comments beginning in SHIM(). +// +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Represents JSON data structure using native Go types: booleans, floats, +// strings, arrays, and maps. + +package apijson + +import ( + "google.golang.org/genai/interactions/internal/apijson/shims" + + // EDIT(begin): decoding scores + "google.golang.org/genai/interactions/internal/apijson/unmarshalinfo" + "google.golang.org/genai/interactions/internal/apijson/unmarshalscore" + + // EDIT(end) + "encoding" + "encoding/base64" + "fmt" + "reflect" + "strconv" + "strings" + "unicode" + "unicode/utf16" + "unicode/utf8" + _ "unsafe" // for linkname +) + +// Unmarshal parses the JSON-encoded data and stores the result +// in the value pointed to by v. If v is nil or not a pointer, +// Unmarshal returns an [InvalidUnmarshalError]. +// +// Unmarshal uses the inverse of the encodings that +// [Marshal] uses, allocating maps, slices, and pointers as necessary, +// with the following additional rules: +// +// To unmarshal JSON into a pointer, Unmarshal first handles the case of +// the JSON being the JSON literal null. In that case, Unmarshal sets +// the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into +// the value pointed at by the pointer. If the pointer is nil, Unmarshal +// allocates a new value for it to point to. +// +// To unmarshal JSON into a value implementing [Unmarshaler], +// Unmarshal calls that value's [Unmarshaler.UnmarshalJSON] method, including +// when the input is a JSON null. +// Otherwise, if the value implements [encoding.TextUnmarshaler] +// and the input is a JSON quoted string, Unmarshal calls +// [encoding.TextUnmarshaler.UnmarshalText] with the unquoted form of the string. +// +// To unmarshal JSON into a struct, Unmarshal matches incoming object +// keys to the keys used by [Marshal] (either the struct field name or its tag), +// preferring an exact match but also accepting a case-insensitive match. By +// default, object keys which don't have a corresponding struct field are +// ignored (see [Decoder.DisallowUnknownFields] for an alternative). +// +// To unmarshal JSON into an interface value, +// Unmarshal stores one of these in the interface value: +// +// - bool, for JSON booleans +// - float64, for JSON numbers +// - string, for JSON strings +// - []any, for JSON arrays +// - map[string]any, for JSON objects +// - nil for JSON null +// +// To unmarshal a JSON array into a slice, Unmarshal resets the slice length +// to zero and then appends each element to the slice. +// As a special case, to unmarshal an empty JSON array into a slice, +// Unmarshal replaces the slice with a new empty slice. +// +// To unmarshal a JSON array into a Go array, Unmarshal decodes +// JSON array elements into corresponding Go array elements. +// If the Go array is smaller than the JSON array, +// the additional JSON array elements are discarded. +// If the JSON array is smaller than the Go array, +// the additional Go array elements are set to zero values. +// +// To unmarshal a JSON object into a map, Unmarshal first establishes a map to +// use. If the map is nil, Unmarshal allocates a new map. Otherwise Unmarshal +// reuses the existing map, keeping existing entries. Unmarshal then stores +// key-value pairs from the JSON object into the map. The map's key type must +// either be any string type, an integer, or implement [encoding.TextUnmarshaler]. +// +// If the JSON-encoded data contain a syntax error, Unmarshal returns a [SyntaxError]. +// +// If a JSON value is not appropriate for a given target type, +// or if a JSON number overflows the target type, Unmarshal +// skips that field and completes the unmarshaling as best it can. +// If no more serious errors are encountered, Unmarshal returns +// an [UnmarshalTypeError] describing the earliest such error. In any +// case, it's not guaranteed that all the remaining fields following +// the problematic one will be unmarshaled into the target object. +// +// The JSON null value unmarshals into an interface, map, pointer, or slice +// by setting that Go value to nil. Because null is often used in JSON to mean +// “not present,” unmarshaling a JSON null into any other Go type has no effect +// on the value and produces no error. +// +// When unmarshaling quoted strings, invalid UTF-8 or +// invalid UTF-16 surrogate pairs are not treated as an error. +// Instead, they are replaced by the Unicode replacement +// character U+FFFD. +func Unmarshal(data []byte, v any) error { + // Check for well-formedness. + // Avoids filling out half a data structure + // before discovering a JSON syntax error. + var d decodeState + err := checkValid(data, &d.scan) + if err != nil { + return err + } + + d.init(data) + return d.unmarshal(v) +} + +// Unmarshaler is the interface implemented by types +// that can unmarshal a JSON description of themselves. +// The input can be assumed to be a valid encoding of +// a JSON value. UnmarshalJSON must copy the JSON data +// if it wishes to retain the data after returning. +// +// By convention, to approximate the behavior of [Unmarshal] itself, +// Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op. +type Unmarshaler interface { + UnmarshalJSON([]byte) error +} + +// An UnmarshalTypeError describes a JSON value that was +// not appropriate for a value of a specific Go type. +type UnmarshalTypeError struct { + Value string // description of JSON value - "bool", "array", "number -5" + Type reflect.Type // type of Go value it could not be assigned to + Offset int64 // error occurred after reading Offset bytes + Struct string // name of the struct type containing the field + Field string // the full path from root node to the field, include embedded struct +} + +func (e *UnmarshalTypeError) Error() string { + if e.Struct != "" || e.Field != "" { + return "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String() + } + return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() +} + +// An UnmarshalFieldError describes a JSON object key that +// led to an unexported (and therefore unwritable) struct field. +// +// Deprecated: No longer used; kept for compatibility. +type UnmarshalFieldError struct { + Key string + Type reflect.Type + Field reflect.StructField +} + +func (e *UnmarshalFieldError) Error() string { + return "json: cannot unmarshal object key " + strconv.Quote(e.Key) + " into unexported field " + e.Field.Name + " of type " + e.Type.String() +} + +// An InvalidUnmarshalError describes an invalid argument passed to [Unmarshal]. +// (The argument to [Unmarshal] must be a non-nil pointer.) +type InvalidUnmarshalError struct { + Type reflect.Type +} + +func (e *InvalidUnmarshalError) Error() string { + if e.Type == nil { + return "json: Unmarshal(nil)" + } + + if e.Type.Kind() != reflect.Pointer { + return "json: Unmarshal(non-pointer " + e.Type.String() + ")" + } + return "json: Unmarshal(nil " + e.Type.String() + ")" +} + +func (d *decodeState) unmarshal(v any) error { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Pointer || rv.IsNil() { + return &InvalidUnmarshalError{reflect.TypeOf(v)} + } + + d.scan.reset() + d.scanWhile(scanSkipSpace) + // We decode rv not rv.Elem because the Unmarshaler interface + // test must be applied at the top level of the value. + err := d.value(rv) + if err != nil { + return d.addErrorContext(err) + } + return d.savedError +} + +// EDIT(begin): unmarshalRoot bypasses the UnmarshalJSON interface at the root level. +// This allows UnmarshalRoot to be called from within a type's UnmarshalJSON without recursion. +func (d *decodeState) unmarshalRoot(v any) error { + d.skipRootUnmarshaler = true + return d.unmarshal(v) +} + +// EDIT(end) + +// A Number represents a JSON number literal. +type Number string + +// String returns the literal text of the number. +func (n Number) String() string { return string(n) } + +// Float64 returns the number as a float64. +func (n Number) Float64() (float64, error) { + return strconv.ParseFloat(string(n), 64) +} + +// Int64 returns the number as an int64. +func (n Number) Int64() (int64, error) { + return strconv.ParseInt(string(n), 10, 64) +} + +// An errorContext provides context for type errors during decoding. +type errorContext struct { + Struct reflect.Type + FieldStack []string +} + +// decodeState represents the state while decoding a JSON value. +type decodeState struct { + data []byte + off int // next read offset in data + opcode int // last read result + scan scanner + errorContext *errorContext + savedError error + useNumber bool + disallowUnknownFields bool + // EDIT(begin): decoding scores + score *unmarshalscore.Score // score tracking for decode quality + // EDIT(end) + // EDIT(begin): skip root unmarshaler + skipRootUnmarshaler bool + // EDIT(end) +} + +// readIndex returns the position of the last byte read. +func (d *decodeState) readIndex() int { + return d.off - 1 +} + +// phasePanicMsg is used as a panic message when we end up with something that +// shouldn't happen. It can indicate a bug in the JSON decoder, or that +// something is editing the data slice while the decoder executes. +const phasePanicMsg = "JSON decoder out of sync - data changing underfoot?" + +func (d *decodeState) init(data []byte) *decodeState { + d.data = data + d.off = 0 + d.savedError = nil + if d.errorContext != nil { + d.errorContext.Struct = nil + // Reuse the allocated space for the FieldStack slice. + d.errorContext.FieldStack = d.errorContext.FieldStack[:0] + } + return d +} + +// saveError saves the first err it is called with, +// for reporting at the end of the unmarshal. +func (d *decodeState) saveError(err error) { + if d.savedError == nil { + d.savedError = d.addErrorContext(err) + } +} + +// addErrorContext returns a new error enhanced with information from d.errorContext +func (d *decodeState) addErrorContext(err error) error { + if d.errorContext != nil && (d.errorContext.Struct != nil || len(d.errorContext.FieldStack) > 0) { + switch err := err.(type) { + case *UnmarshalTypeError: + err.Struct = d.errorContext.Struct.Name() + fieldStack := d.errorContext.FieldStack + if err.Field != "" { + fieldStack = append(fieldStack, err.Field) + } + err.Field = strings.Join(fieldStack, ".") + } + } + return err +} + +// skip scans to the end of what was started. +func (d *decodeState) skip() { + s, data, i := &d.scan, d.data, d.off + depth := len(s.parseState) + for { + op := s.step(s, data[i]) + i++ + if len(s.parseState) < depth { + d.off = i + d.opcode = op + return + } + } +} + +// scanNext processes the byte at d.data[d.off]. +func (d *decodeState) scanNext() { + if d.off < len(d.data) { + d.opcode = d.scan.step(&d.scan, d.data[d.off]) + d.off++ + } else { + d.opcode = d.scan.eof() + d.off = len(d.data) + 1 // mark processed EOF with len+1 + } +} + +// scanWhile processes bytes in d.data[d.off:] until it +// receives a scan code not equal to op. +func (d *decodeState) scanWhile(op int) { + s, data, i := &d.scan, d.data, d.off + for i < len(data) { + newOp := s.step(s, data[i]) + i++ + if newOp != op { + d.opcode = newOp + d.off = i + return + } + } + + d.off = len(data) + 1 // mark processed EOF with len+1 + d.opcode = d.scan.eof() +} + +// rescanLiteral is similar to scanWhile(scanContinue), but it specialises the +// common case where we're decoding a literal. The decoder scans the input +// twice, once for syntax errors and to check the length of the value, and the +// second to perform the decoding. +// +// Only in the second step do we use decodeState to tokenize literals, so we +// know there aren't any syntax errors. We can take advantage of that knowledge, +// and scan a literal's bytes much more quickly. +func (d *decodeState) rescanLiteral() { + data, i := d.data, d.off +Switch: + switch data[i-1] { + case '"': // string + for ; i < len(data); i++ { + switch data[i] { + case '\\': + i++ // escaped char + case '"': + i++ // tokenize the closing quote too + break Switch + } + } + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-': // number + for ; i < len(data); i++ { + switch data[i] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '.', 'e', 'E', '+', '-': + default: + break Switch + } + } + case 't': // true + i += len("rue") + case 'f': // false + i += len("alse") + case 'n': // null + i += len("ull") + } + if i < len(data) { + d.opcode = stateEndValue(&d.scan, data[i]) + } else { + d.opcode = scanEnd + } + d.off = i + 1 +} + +// value consumes a JSON value from d.data[d.off-1:], decoding into v, and +// reads the following byte ahead. If v is invalid, the value is discarded. +// The first byte of the value has been read already. +func (d *decodeState) value(v reflect.Value) error { + switch d.opcode { + default: + panic(phasePanicMsg) + + case scanBeginArray: + if v.IsValid() { + if err := d.array(v); err != nil { + return err + } + } else { + d.skip() + } + d.scanNext() + + case scanBeginObject: + if v.IsValid() { + if err := d.object(v); err != nil { + return err + } + } else { + d.skip() + } + d.scanNext() + + case scanBeginLiteral: + // All bytes inside literal return scanContinue op code. + start := d.readIndex() + d.rescanLiteral() + + if v.IsValid() { + if err := d.literalStore(d.data[start:d.readIndex()], v, false); err != nil { + return err + } + } + } + return nil +} + +type unquotedValue struct{} + +// valueQuoted is like value but decodes a +// quoted string literal or literal null into an interface value. +// If it finds anything other than a quoted string literal or null, +// valueQuoted returns unquotedValue{}. +func (d *decodeState) valueQuoted() any { + switch d.opcode { + default: + panic(phasePanicMsg) + + case scanBeginArray, scanBeginObject: + d.skip() + d.scanNext() + + case scanBeginLiteral: + v := d.literalInterface() + switch v.(type) { + case nil, string: + return v + } + } + return unquotedValue{} +} + +// indirect walks down v allocating pointers as needed, +// until it gets to a non-pointer. +// If it encounters an Unmarshaler, indirect stops and returns that. +// If decodingNull is true, indirect stops at the first settable pointer so it +// can be set to nil. +func indirect(v reflect.Value, decodingNull bool) (Unmarshaler, encoding.TextUnmarshaler, reflect.Value) { + // Issue #24153 indicates that it is generally not a guaranteed property + // that you may round-trip a reflect.Value by calling Value.Addr().Elem() + // and expect the value to still be settable for values derived from + // unexported embedded struct fields. + // + // The logic below effectively does this when it first addresses the value + // (to satisfy possible pointer methods) and continues to dereference + // subsequent pointers as necessary. + // + // After the first round-trip, we set v back to the original value to + // preserve the original RW flags contained in reflect.Value. + v0 := v + haveAddr := false + + // If v is a named type and is addressable, + // start with its address, so that if the type has pointer methods, + // we find them. + if v.Kind() != reflect.Pointer && v.Type().Name() != "" && v.CanAddr() { + haveAddr = true + v = v.Addr() + } + for { + // Load value from interface, but only if the result will be + // usefully addressable. + if v.Kind() == reflect.Interface && !v.IsNil() { + e := v.Elem() + if e.Kind() == reflect.Pointer && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Pointer) { + haveAddr = false + v = e + continue + } + } + + if v.Kind() != reflect.Pointer { + break + } + + if decodingNull && v.CanSet() { + break + } + + // Prevent infinite loop if v is an interface pointing to its own address: + // var v any + // v = &v + if v.Elem().Kind() == reflect.Interface && v.Elem().Elem().Equal(v) { + v = v.Elem() + break + } + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Unmarshaler); ok { + return u, nil, reflect.Value{} + } + if !decodingNull { + if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { + return nil, u, reflect.Value{} + } + } + } + + if haveAddr { + v = v0 // restore original value after round-trip Value.Addr().Elem() + haveAddr = false + } else { + v = v.Elem() + } + } + return nil, nil, v +} + +// array consumes an array from d.data[d.off-1:], decoding into v. +// The first byte of the array ('[') has been read already. +func (d *decodeState) array(v reflect.Value) error { + // Check for unmarshaler. + u, ut, pv := indirect(v, false) + // EDIT(begin): skip root unmarshaler + if d.skipRootUnmarshaler { + d.skipRootUnmarshaler = false + u = nil + ut = nil + pv = v + for pv.Kind() == reflect.Pointer { + pv = pv.Elem() + } + } + // EDIT(end) + if u != nil { + start := d.readIndex() + d.skip() + // EDIT(begin): track decode scores + err := u.UnmarshalJSON(d.data[start:d.off]) + if d.score != nil { + if s, ok := u.(unmarshalinfo.UnmarshalTracker); ok { + d.score.Add(*s.UnmarshalState()) + } + } + return err + // EDIT(end) + } + if ut != nil { + d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + } + v = pv + + // Check type of target. + switch v.Kind() { + case reflect.Interface: + if v.NumMethod() == 0 { + // Decoding into nil interface? Switch to non-reflect code. + ai := d.arrayInterface() + v.Set(reflect.ValueOf(ai)) + return nil + } + // Otherwise it's invalid. + fallthrough + default: + d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + case reflect.Array, reflect.Slice: + break + } + + i := 0 + for { + // Look ahead for ] - can only happen on first iteration. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndArray { + break + } + + // Expand slice length, growing the slice if necessary. + if v.Kind() == reflect.Slice { + if i >= v.Cap() { + v.Grow(1) + } + if i >= v.Len() { + v.SetLen(i + 1) + } + } + + if i < v.Len() { + // Decode into element. + if err := d.value(v.Index(i)); err != nil { + return err + } + } else { + // Ran out of fixed array: skip. + if err := d.value(reflect.Value{}); err != nil { + return err + } + } + i++ + + // Next token must be , or ]. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndArray { + break + } + if d.opcode != scanArrayValue { + panic(phasePanicMsg) + } + } + + if i < v.Len() { + if v.Kind() == reflect.Array { + for ; i < v.Len(); i++ { + v.Index(i).SetZero() // zero remainder of array + } + } else { + v.SetLen(i) // truncate the slice + } + } + if i == 0 && v.Kind() == reflect.Slice { + v.Set(reflect.MakeSlice(v.Type(), 0, 0)) + } + return nil +} + +var nullLiteral = []byte("null") + +// SHIM(reflect): reflect.TypeFor[T]() reflect.T +var textUnmarshalerType = shims.TypeFor[encoding.TextUnmarshaler]() + +// object consumes an object from d.data[d.off-1:], decoding into v. +// The first byte ('{') of the object has been read already. +func (d *decodeState) object(v reflect.Value) error { + // Check for unmarshaler. + u, ut, pv := indirect(v, false) + // EDIT(begin): skip root unmarshaler + if d.skipRootUnmarshaler { + d.skipRootUnmarshaler = false + u = nil + ut = nil + // pv is zero when indirect found an Unmarshaler; deref v manually + pv = v + for pv.Kind() == reflect.Pointer { + pv = pv.Elem() + } + } + // EDIT(end) + if u != nil { + start := d.readIndex() + d.skip() + // EDIT(begin): track decode scores + err := u.UnmarshalJSON(d.data[start:d.off]) + if d.score != nil { + if s, ok := u.(unmarshalinfo.UnmarshalTracker); ok { + d.score.Add(*s.UnmarshalState()) + } + } + return err + // EDIT(end) + } + if ut != nil { + d.saveError(&UnmarshalTypeError{Value: "object", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + } + v = pv + t := v.Type() + + // Decoding into nil interface? Switch to non-reflect code. + if v.Kind() == reflect.Interface && v.NumMethod() == 0 { + oi := d.objectInterface() + v.Set(reflect.ValueOf(oi)) + return nil + } + + var fields structFields + + // Check type of target: + // struct or + // map[T1]T2 where T1 is string, an integer type, + // or an encoding.TextUnmarshaler + switch v.Kind() { + case reflect.Map: + // Map key must either have string kind, have an integer kind, + // or be an encoding.TextUnmarshaler. + switch t.Key().Kind() { + case reflect.String, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + default: + if !reflect.PointerTo(t.Key()).Implements(textUnmarshalerType) { + d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)}) + d.skip() + return nil + } + } + if v.IsNil() { + v.Set(reflect.MakeMap(t)) + } + case reflect.Struct: + fields = cachedTypeFields(t) + // ok + default: + d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)}) + d.skip() + return nil + } + + var mapElem reflect.Value + var origErrorContext errorContext + if d.errorContext != nil { + origErrorContext = *d.errorContext + } + + // EDIT(begin): track decoding scores + // Track which required fields are seen for scoring + var seenRequiredFields map[string]bool + var seenFields map[string]bool + if d.score != nil && v.Kind() == reflect.Struct { + seenRequiredFields = make(map[string]bool) + seenFields = make(map[string]bool) + } + // EDIT(end) + + // EDIT(begin): extra field routing — see extraFieldRouter in inline.go. + extraFieldRouter := newExtraFieldRouter(fields, v, t) + // EDIT(end) + + for { + // Read opening " of string key or closing }. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if d.opcode != scanBeginLiteral { + panic(phasePanicMsg) + } + + // Read key. + start := d.readIndex() + d.rescanLiteral() + item := d.data[start:d.readIndex()] + key, ok := unquoteBytes(item) + if !ok { + panic(phasePanicMsg) + } + + // EDIT(begin): track decoding scores + // Track total fields for scoring + if d.score != nil && v.Kind() != reflect.Map { + d.score.FieldsTotal++ + } + // EDIT(end) + + // Figure out field corresponding to key. + var subv reflect.Value + destring := false // whether the value is wrapped in a string to be decoded first + + // EDIT(begin) + var isExtraField bool // no struct field matched; router will handle after scan + // EDIT(end) + + if v.Kind() == reflect.Map { + elemType := t.Elem() + if !mapElem.IsValid() { + mapElem = reflect.New(elemType).Elem() + } else { + mapElem.SetZero() + } + subv = mapElem + // EDIT(begin): track decoding scores + // Maps always match fields + if d.score != nil { + d.score.MapValuesMatched++ + } + // EDIT(end) + } else { + f := fields.byExactName[string(key)] + if f == nil { + f = fields.byFoldedName[string(foldName(key))] + } + if f != nil { + subv = v + destring = f.quoted + if d.errorContext == nil { + d.errorContext = new(errorContext) + } + for i, ind := range f.index { + if subv.Kind() == reflect.Pointer { + if subv.IsNil() { + // If a struct embeds a pointer to an unexported type, + // it is not possible to set a newly allocated value + // since the field is unexported. + // + // See https://golang.org/issue/21357 + if !subv.CanSet() { + d.saveError(fmt.Errorf("json: cannot set embedded pointer to unexported struct: %v", subv.Type().Elem())) + // Invalidate subv to ensure d.value(subv) skips over + // the JSON value without assigning it to subv. + subv = reflect.Value{} + destring = false + break + } + subv.Set(reflect.New(subv.Type().Elem())) + } + subv = subv.Elem() + } + if i < len(f.index)-1 { + d.errorContext.FieldStack = append( + d.errorContext.FieldStack, + subv.Type().Field(ind).Name, + ) + } + subv = subv.Field(ind) + } + d.errorContext.Struct = t + d.errorContext.FieldStack = append(d.errorContext.FieldStack, f.name) + // EDIT(begin): track decoding scores + if d.score != nil { + d.score.FieldsMatched++ + // Mark field as seen + if seenFields != nil { + seenFields[f.name] = true + } + // Mark required field as seen + if seenRequiredFields != nil && f.required { + seenRequiredFields[f.name] = true + } + } + // EDIT(end) + } else if d.disallowUnknownFields { + // EDIT(begin): track decoding scores + if d.score != nil { + d.score.UnknownFields++ + } + // EDIT(end) + d.saveError(fmt.Errorf("json: unknown field %q", key)) + } else { + // EDIT(begin): track decoding scores + if d.score != nil { + // To store the extra field in a raw fields container, + // we'll pass the raw bytes to extraFieldRouter.Route() at write-back time. + if extraFieldRouter.Active() { + isExtraField = true + } + d.score.UnknownFields++ + } + // EDIT(end) + } + } + + // Read : before value. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode != scanObjectKey { + panic(phasePanicMsg) + } + d.scanWhile(scanSkipSpace) + + // EDIT(begin): capture value position so we can extract raw bytes + // for extra fields that bypass decoding. + valStart := d.readIndex() + // EDIT(end) + + if destring { + switch qv := d.valueQuoted().(type) { + case nil: + if err := d.literalStore(nullLiteral, subv, false); err != nil { + return err + } + case string: + if err := d.literalStore([]byte(qv), subv, true); err != nil { + return err + } + default: + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into %v", subv.Type())) + } + } else { + if err := d.value(subv); err != nil { + return err + } + } + + // Write value back to map; + // if using struct, subv points into struct already. + if v.Kind() == reflect.Map { + kt := t.Key() + var kv reflect.Value + if reflect.PointerTo(kt).Implements(textUnmarshalerType) { + kv = reflect.New(kt) + if err := d.literalStore(item, kv, true); err != nil { + return err + } + kv = kv.Elem() + } else { + switch kt.Kind() { + case reflect.String: + kv = reflect.New(kt).Elem() + kv.SetString(string(key)) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + s := string(key) + n, err := strconv.ParseInt(s, 10, 64) + // SHIM(reflect): reflect.Type.OverflowInt(int64) bool + okt := shims.OverflowableType{Type: kt} + if err != nil || okt.OverflowInt(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: kt, Offset: int64(start + 1)}) + break + } + kv = reflect.New(kt).Elem() + kv.SetInt(n) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + s := string(key) + n, err := strconv.ParseUint(s, 10, 64) + // SHIM(reflect): reflect.Type.OverflowUint(uint64) bool + okt := shims.OverflowableType{Type: kt} + if err != nil || okt.OverflowUint(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: kt, Offset: int64(start + 1)}) + break + } + kv = reflect.New(kt).Elem() + kv.SetUint(n) + default: + panic("json: Unexpected key type") // should never occur + } + } + if kv.IsValid() { + v.SetMapIndex(kv, subv) + } + // EDIT(begin): extra field — the scanner skipped the value without + // decoding. Hand the raw JSON bytes to the router, which decides + // whether they go into the inline map or DynamicFields. + } else if isExtraField { + result := extraFieldRouter.Route(string(key), d.data[valStart:d.readIndex()]) + if d.score != nil { + switch result { + case extraFieldMatched: + d.score.FieldsMatched++ + case extraFieldUnknown: + d.score.UnknownFields++ + } + } + } else if subv.IsValid() && IsNullForNonNullable(d.data[valStart:d.readIndex()], subv.Type()) { + // Known field, but the JSON value is null and the field can't + // hold null. Stdlib Unmarshal silently leaves the target at + // its zero value; record the raw bytes as apidata.Mismatch in + // DynamicFields so callers can detect what was actually received. + extraFieldRouter.RecordMismatch(string(key), d.data[valStart:d.readIndex()]) + } + // EDIT(end) + + // Next token must be , or }. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.errorContext != nil { + // Reset errorContext to its original state. + // Keep the same underlying array for FieldStack, to reuse the + // space and avoid unnecessary allocs. + d.errorContext.FieldStack = d.errorContext.FieldStack[:len(origErrorContext.FieldStack)] + d.errorContext.Struct = origErrorContext.Struct + } + if d.opcode == scanEndObject { + break + } + if d.opcode != scanObjectValue { + panic(phasePanicMsg) + } + } + + // EDIT(begin): track decoding scores + // Count missing required fields and unmatched target fields for scoring + if d.score != nil && v.Kind() == reflect.Struct { + for _, f := range fields.list { + if f.required { + if seenRequiredFields[f.name] { + d.score.MatchedRequiredFields++ + } else { + d.score.MissingRequiredFields++ + } + } + if !seenFields[f.name] { + d.score.UnmatchedTargetFields++ + } + } + } + // EDIT(end) + + return nil +} + +// convertNumber converts the number literal s to a float64 or a Number +// depending on the setting of d.useNumber. +func (d *decodeState) convertNumber(s string) (any, error) { + if d.useNumber { + return Number(s), nil + } + f, err := strconv.ParseFloat(s, 64) + if err != nil { + // SHIM(reflect): reflect.TypeFor[T]() reflect.Type + return nil, &UnmarshalTypeError{Value: "number " + s, Type: shims.TypeFor[float64](), Offset: int64(d.off)} + } + return f, nil +} + +// SHIM(reflect): TypeFor[T]() reflect.Type +var numberType = shims.TypeFor[Number]() + +// literalStore decodes a literal stored in item into v. +// +// fromQuoted indicates whether this literal came from unwrapping a +// string from the ",string" struct tag option. this is used only to +// produce more helpful error messages. +func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) error { + // Check for unmarshaler. + if len(item) == 0 { + // Empty string given. + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + return nil + } + isNull := item[0] == 'n' // null + u, ut, pv := indirect(v, isNull) + // EDIT(begin): skip root unmarshaler + if d.skipRootUnmarshaler { + d.skipRootUnmarshaler = false + u = nil + ut = nil + pv = v + for pv.Kind() == reflect.Pointer { + pv = pv.Elem() + } + } + // EDIT(end) + if u != nil { + // EDIT(begin): track decode scores + err := u.UnmarshalJSON(item) + if d.score != nil { + if s, ok := u.(unmarshalinfo.UnmarshalTracker); ok { + d.score.Add(*s.UnmarshalState()) + } + } + return err + // EDIT(end) + } + if ut != nil { + if item[0] != '"' { + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + return nil + } + val := "number" + switch item[0] { + case 'n': + val = "null" + case 't', 'f': + val = "bool" + } + d.saveError(&UnmarshalTypeError{Value: val, Type: v.Type(), Offset: int64(d.readIndex())}) + return nil + } + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + return ut.UnmarshalText(s) + } + + v = pv + + switch c := item[0]; c { + case 'n': // null + // The main parser checks that only true and false can reach here, + // but if this was a quoted string input, it could be anything. + if fromQuoted && string(item) != "null" { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + break + } + switch v.Kind() { + case reflect.Interface, reflect.Pointer, reflect.Map, reflect.Slice: + v.SetZero() + // otherwise, ignore null for primitives/string + } + case 't', 'f': // true, false + value := item[0] == 't' + // The main parser checks that only true and false can reach here, + // but if this was a quoted string input, it could be anything. + if fromQuoted && string(item) != "true" && string(item) != "false" { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + break + } + switch v.Kind() { + default: + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.saveError(&UnmarshalTypeError{Value: "bool", Type: v.Type(), Offset: int64(d.readIndex())}) + } + case reflect.Bool: + v.SetBool(value) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(value)) + } else { + d.saveError(&UnmarshalTypeError{Value: "bool", Type: v.Type(), Offset: int64(d.readIndex())}) + } + } + + case '"': // string + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + switch v.Kind() { + default: + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + case reflect.Slice: + if v.Type().Elem().Kind() != reflect.Uint8 { + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) + n, err := base64.StdEncoding.Decode(b, s) + if err != nil { + d.saveError(err) + break + } + v.SetBytes(b[:n]) + case reflect.String: + t := string(s) + if v.Type() == numberType && !isValidNumber(t) { + return fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", item) + } + v.SetString(t) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(string(s))) + } else { + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + } + } + + default: // number + if c != '-' && (c < '0' || c > '9') { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + switch v.Kind() { + default: + if v.Kind() == reflect.String && v.Type() == numberType { + // s must be a valid number, because it's + // already been tokenized. + v.SetString(string(item)) + break + } + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + d.saveError(&UnmarshalTypeError{Value: "number", Type: v.Type(), Offset: int64(d.readIndex())}) + case reflect.Interface: + n, err := d.convertNumber(string(item)) + if err != nil { + d.saveError(err) + break + } + if v.NumMethod() != 0 { + d.saveError(&UnmarshalTypeError{Value: "number", Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.Set(reflect.ValueOf(n)) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n, err := strconv.ParseInt(string(item), 10, 64) + if err != nil || v.OverflowInt(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + string(item), Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetInt(n) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + n, err := strconv.ParseUint(string(item), 10, 64) + if err != nil || v.OverflowUint(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + string(item), Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetUint(n) + + case reflect.Float32, reflect.Float64: + n, err := strconv.ParseFloat(string(item), v.Type().Bits()) + if err != nil || v.OverflowFloat(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + string(item), Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetFloat(n) + } + } + return nil +} + +// The xxxInterface routines build up a value to be stored +// in an empty interface. They are not strictly necessary, +// but they avoid the weight of reflection in this common case. + +// valueInterface is like value but returns any. +func (d *decodeState) valueInterface() (val any) { + switch d.opcode { + default: + panic(phasePanicMsg) + case scanBeginArray: + val = d.arrayInterface() + d.scanNext() + case scanBeginObject: + val = d.objectInterface() + d.scanNext() + case scanBeginLiteral: + val = d.literalInterface() + } + return +} + +// arrayInterface is like array but returns []any. +func (d *decodeState) arrayInterface() []any { + var v = make([]any, 0) + for { + // Look ahead for ] - can only happen on first iteration. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndArray { + break + } + + v = append(v, d.valueInterface()) + + // Next token must be , or ]. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndArray { + break + } + if d.opcode != scanArrayValue { + panic(phasePanicMsg) + } + } + return v +} + +// objectInterface is like object but returns map[string]any. +func (d *decodeState) objectInterface() map[string]any { + m := make(map[string]any) + for { + // Read opening " of string key or closing }. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if d.opcode != scanBeginLiteral { + panic(phasePanicMsg) + } + + // Read string key. + start := d.readIndex() + d.rescanLiteral() + item := d.data[start:d.readIndex()] + key, ok := unquote(item) + if !ok { + panic(phasePanicMsg) + } + + // Read : before value. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode != scanObjectKey { + panic(phasePanicMsg) + } + d.scanWhile(scanSkipSpace) + + // Read value. + m[key] = d.valueInterface() + + // Next token must be , or }. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndObject { + break + } + if d.opcode != scanObjectValue { + panic(phasePanicMsg) + } + } + return m +} + +// literalInterface consumes and returns a literal from d.data[d.off-1:] and +// it reads the following byte ahead. The first byte of the literal has been +// read already (that's how the caller knows it's a literal). +func (d *decodeState) literalInterface() any { + // All bytes inside literal return scanContinue op code. + start := d.readIndex() + d.rescanLiteral() + + item := d.data[start:d.readIndex()] + + switch c := item[0]; c { + case 'n': // null + return nil + + case 't', 'f': // true, false + return c == 't' + + case '"': // string + s, ok := unquote(item) + if !ok { + panic(phasePanicMsg) + } + return s + + default: // number + if c != '-' && (c < '0' || c > '9') { + panic(phasePanicMsg) + } + n, err := d.convertNumber(string(item)) + if err != nil { + d.saveError(err) + } + return n + } +} + +// getu4 decodes \uXXXX from the beginning of s, returning the hex value, +// or it returns -1. +func getu4(s []byte) rune { + if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { + return -1 + } + var r rune + for _, c := range s[2:6] { + switch { + case '0' <= c && c <= '9': + c = c - '0' + case 'a' <= c && c <= 'f': + c = c - 'a' + 10 + case 'A' <= c && c <= 'F': + c = c - 'A' + 10 + default: + return -1 + } + r = r*16 + rune(c) + } + return r +} + +// unquote converts a quoted JSON string literal s into an actual string t. +// The rules are different than for Go, so cannot use strconv.Unquote. +func unquote(s []byte) (t string, ok bool) { + s, ok = unquoteBytes(s) + t = string(s) + return +} + +// unquoteBytes should be an internal detail, +// but widely used packages access it using linkname. +// Notable members of the hall of shame include: +// - github.com/bytedance/sonic +// +// Do not remove or change the type signature. +// See go.dev/issue/67401. +// +//go:linkname unquoteBytes +func unquoteBytes(s []byte) (t []byte, ok bool) { + if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { + return + } + s = s[1 : len(s)-1] + + // Check for unusual characters. If there are none, + // then no unquoting is needed, so return a slice of the + // original bytes. + r := 0 + for r < len(s) { + c := s[r] + if c == '\\' || c == '"' || c < ' ' { + break + } + if c < utf8.RuneSelf { + r++ + continue + } + rr, size := utf8.DecodeRune(s[r:]) + if rr == utf8.RuneError && size == 1 { + break + } + r += size + } + if r == len(s) { + return s, true + } + + b := make([]byte, len(s)+2*utf8.UTFMax) + w := copy(b, s[0:r]) + for r < len(s) { + // Out of room? Can only happen if s is full of + // malformed UTF-8 and we're replacing each + // byte with RuneError. + if w >= len(b)-2*utf8.UTFMax { + nb := make([]byte, (len(b)+utf8.UTFMax)*2) + copy(nb, b[0:w]) + b = nb + } + switch c := s[r]; { + case c == '\\': + r++ + if r >= len(s) { + return + } + switch s[r] { + default: + return + case '"', '\\', '/', '\'': + b[w] = s[r] + r++ + w++ + case 'b': + b[w] = '\b' + r++ + w++ + case 'f': + b[w] = '\f' + r++ + w++ + case 'n': + b[w] = '\n' + r++ + w++ + case 'r': + b[w] = '\r' + r++ + w++ + case 't': + b[w] = '\t' + r++ + w++ + case 'u': + r-- + rr := getu4(s[r:]) + if rr < 0 { + return + } + r += 6 + if utf16.IsSurrogate(rr) { + rr1 := getu4(s[r:]) + if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar { + // A valid pair; consume. + r += 6 + w += utf8.EncodeRune(b[w:], dec) + break + } + // Invalid surrogate; fall back to replacement rune. + rr = unicode.ReplacementChar + } + w += utf8.EncodeRune(b[w:], rr) + } + + // Quote, control characters are invalid. + case c == '"', c < ' ': + return + + // ASCII + case c < utf8.RuneSelf: + b[w] = c + r++ + w++ + + // Coerce to well-formed UTF-8. + default: + rr, size := utf8.DecodeRune(s[r:]) + r += size + w += utf8.EncodeRune(b[w:], rr) + } + } + return b[0:w], true +} diff --git a/interactions/internal/apijson/decodescore.go b/interactions/internal/apijson/decodescore.go new file mode 100644 index 00000000..4e9ad7a9 --- /dev/null +++ b/interactions/internal/apijson/decodescore.go @@ -0,0 +1,67 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// EDIT(begin): Logic for scoring decodings +package apijson + +// This file defines some logic for evaluating the score of different +// unmarshalings, to let you judge between which unmarshalings are better than +// others. This is an addition on top of the Go standard decoding logic. + +import ( + "google.golang.org/genai/interactions/internal/apijson/unmarshalscore" +) + +// UnmarshalWithScore parses the JSON-encoded data and stores the result +// in the value pointed to by v, returning a DecodeScore that indicates +// how well the JSON matched the target type. +func UnmarshalWithScore(data []byte, v any) (unmarshalscore.Score, error) { + var d decodeState + err := checkValid(data, &d.scan) + if err != nil { + return unmarshalscore.Score{}, err + } + + d.init(data) + score := unmarshalscore.Score{} + d.score = &score + err = d.unmarshal(v) + if err != nil { + return unmarshalscore.Score{Succeeded: false}, err + } + score.Succeeded = true + return score, err +} + +// unmarshalRootWithScore is like UnmarshalWithScore but skips the +// UnmarshalJSON interface check at the root level. +func unmarshalRootWithScore(data []byte, v any) (unmarshalscore.Score, error) { + var d decodeState + err := checkValid(data, &d.scan) + if err != nil { + return unmarshalscore.Score{}, err + } + + d.init(data) + score := unmarshalscore.Score{} + d.score = &score + err = d.unmarshalRoot(v) + if err != nil { + return unmarshalscore.Score{Succeeded: false}, err + } + score.Succeeded = true + return score, err +} + +// EDIT(end) diff --git a/interactions/internal/apijson/decodescore_test.go b/interactions/internal/apijson/decodescore_test.go new file mode 100644 index 00000000..51143008 --- /dev/null +++ b/interactions/internal/apijson/decodescore_test.go @@ -0,0 +1,400 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apijson + +import ( + "fmt" + "google.golang.org/genai/interactions/internal/apijson/unmarshalscore" + "reflect" + "testing" +) + +func TestUnmarshalPrecedence(t *testing.T) { + type WithRequired struct { + Name string `json:"name" api:"required"` + Value string `json:"value"` + } + type WithoutRequired struct { + Name string `json:"name"` + Other string `json:"other"` + } + type TwoFields struct { + A string `json:"a"` + B string `json:"b"` + } + type OneField struct { + A string `json:"a"` + } + type OneFieldAlias OneField + type WithRequiredExtra struct { + Name string `json:"name" api:"required"` + Extra string `json:"extra" api:"required"` + } + type WithManyFields struct { + ID string `json:"id"` + Name string `json:"name"` + Value string `json:"value"` + } + type WithRequiredID struct { + ID string `json:"id" api:"required"` + } + type InnerStruct struct { + A string `json:"a"` + B string `json:"b"` + } + type NestedStruct struct { + Outer string `json:"outer"` + Inner InnerStruct `json:"inner"` + } + type WithPointer struct { + Name *string `json:"name"` + Value *int `json:"value"` + } + type WithSlice struct { + Items []string `json:"items"` + } + type WithMap struct { + Data map[string]string `json:"data"` + } + type WithBool struct { + Flag bool `json:"flag"` + } + type WithFloat struct { + Value float64 `json:"value"` + } + type WithInt struct { + Count int `json:"count"` + } + type EmptyStruct struct{} + type ThreeFields struct { + A string `json:"a"` + B string `json:"b"` + C string `json:"c"` + } + type AllRequired struct { + A string `json:"a" api:"required"` + B string `json:"b" api:"required"` + } + type InnerA struct { + X string `json:"x"` + Y string `json:"y"` + } + type InnerB struct { + X string `json:"x"` + } + type OuterA struct { + Name string `json:"name"` + Inner InnerA `json:"inner"` + } + type OuterB struct { + Name string `json:"name"` + Inner InnerB `json:"inner"` + } + + tests := []struct { + name string + json string + variants []any + expected any + }{ + // Basic type matching + { + name: "matches_int_over_string", + json: `42`, + variants: []any{ + ptrTo[int](), + ptrTo[string](), + }, + expected: 42, + }, + { + name: "matches_string_over_int", + json: `"hello"`, + variants: []any{ + ptrTo[int](), + ptrTo[string](), + }, + expected: "hello", + }, + { + name: "matches_bool", + json: `true`, + variants: []any{ + ptrTo[int](), + ptrTo[bool](), + }, + expected: true, + }, + // Struct matching + { + name: "prefers_more_fields", + json: `{"a":"x","b":"y"}`, + variants: []any{ + ptrTo[OneField](), + ptrTo[TwoFields](), + }, + expected: TwoFields{A: "x", B: "y"}, + }, + { + name: "prefers_not_missing_fields", + json: `{"a":"x"}`, + variants: []any{ + ptrTo[OneField](), + ptrTo[TwoFields](), + }, + expected: OneField{A: "x"}, + }, + { + name: "prefers_exact_match_over_superset", + json: `{"a":"1","b":"2"}`, + variants: []any{ + ptrTo[ThreeFields](), + ptrTo[TwoFields](), + }, + expected: TwoFields{A: "1", B: "2"}, + }, + // Required field handling + { + name: "prefers_required_field_match", + json: `{"name":"test"}`, + variants: []any{ + ptrTo[WithoutRequired](), + ptrTo[WithRequired](), + }, + expected: WithRequired{Name: "test"}, + }, + { + name: "required_field_missing_loses", + json: `{"name":"test"}`, + variants: []any{ + ptrTo[WithRequiredExtra](), + ptrTo[WithoutRequired](), + }, + expected: WithoutRequired{Name: "test"}, + }, + { + name: "more_matches_beats_required_fields", + json: `{"id":"123","name":"n","value":"v"}`, + variants: []any{ + ptrTo[WithRequiredID](), + ptrTo[WithManyFields](), + }, + expected: WithManyFields{ID: "123", Name: "n", Value: "v"}, + }, + // Arrays and slices + { + name: "matches_array_of_floats_when_needed", + json: `[1, 2.5, 3]`, + variants: []any{ + ptrTo[[]int](), + ptrTo[[]float64](), + }, + expected: []float64{1, 2.5, 3}, + }, + // Maps + { + name: "prefers_struct_to_map", + json: `{"a":"x"}`, + variants: []any{ + ptrTo[map[string]string](), + ptrTo[OneField](), + }, + expected: OneField{A: "x"}, + }, + { + name: "prefers_map_to_struct_with_missing_fields", + json: `{"a":"x","b":"y"}`, + variants: []any{ + ptrTo[OneField](), + ptrTo[map[string]string](), + }, + expected: map[string]string{"a": "x", "b": "y"}, + }, + // Nested structs + { + name: "matches_nested_struct", + json: `{"outer":"o","inner":{"a":"x","b":"y"}}`, + variants: []any{ + ptrTo[OneField](), + ptrTo[NestedStruct](), + }, + expected: NestedStruct{Outer: "o", Inner: InnerStruct{A: "x", B: "y"}}, + }, + { + name: "nested_struct_prefers_better_inner_match_innerA", + json: `{"name":"test","inner":{"x":"1","y":"2"}}`, + variants: []any{ + ptrTo[OuterB](), + ptrTo[OuterA](), + }, + expected: OuterA{Name: "test", Inner: InnerA{X: "1", Y: "2"}}, + }, + { + name: "nested_struct_prefers_better_inner_match_innerB", + json: `{"name":"test","inner":{"x":"1"}}`, + variants: []any{ + ptrTo[OuterA](), + ptrTo[OuterB](), + }, + expected: OuterB{Name: "test", Inner: InnerB{X: "1"}}, + }, + // Multiple valid matches - should pick best + { + name: "multiple_structs_picks_best_match", + json: `{"a":"1","b":"2","c":"3"}`, + variants: []any{ + ptrTo[OneField](), + ptrTo[TwoFields](), + ptrTo[ThreeFields](), + }, + expected: ThreeFields{A: "1", B: "2", C: "3"}, + }, + // Variant order should not matter + { + name: "variant_order_does_not_matter", + json: `{"a":"x","b":"y"}`, + variants: []any{ + ptrTo[TwoFields](), + ptrTo[OneField](), + }, + expected: TwoFields{A: "x", B: "y"}, + }, + // any type + { + name: "prefers_specific_type_over_any", + json: `"hello"`, + variants: []any{ + ptrTo[any](), + ptrTo[string](), + }, + expected: "hello", + }, + { + name: "prefers_struct_over_any", + json: `{"a":"x","b":"y"}`, + variants: []any{ + ptrTo[any](), + ptrTo[TwoFields](), + }, + expected: TwoFields{A: "x", B: "y"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scores := make([]unmarshalscore.Score, 0, len(tt.variants)) + for _, v := range tt.variants { + score, _ := UnmarshalWithScore([]byte(tt.json), v) + scores = append(scores, score) + } + valuePtr, err := getBestScoring(tt.variants, scores) + if err != nil { + t.Logf("Failed to match with any variant: %s", tt.json) + t.Fatal(err) + } + value := reflect.ValueOf(valuePtr).Elem().Interface() + if !reflect.DeepEqual(tt.expected, value) { + t.Errorf("expected %#v but got %#v", tt.expected, value) + for i, variant := range tt.variants { + score := scores[i] + t.Errorf("%v: %+v", reflect.TypeOf(variant).Elem(), score) + } + t.FailNow() + } + }) + } +} + +type HasAdditionalProperties struct { + Name string `json:"name"` + Extras map[string]any `json:",inline"` +} + +func TestUnmarshalAdditionalProperties(t *testing.T) { + json := `{"name":"test","extra1":"value1","extra2":"value2"}` + var result HasAdditionalProperties + score, err := UnmarshalWithScore([]byte(json), &result) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result.Name != "test" { + t.Errorf("expected Name to be 'test', got %q", result.Name) + } + if result.Extras == nil { + t.Fatal("expected Extras to be populated") + } + if result.Extras["extra1"] != "value1" { + t.Errorf("expected Extras['extra1'] to be 'value1', got %v", result.Extras["extra1"]) + } + if result.Extras["extra2"] != "value2" { + t.Errorf("expected Extras['extra2'] to be 'value2', got %v", result.Extras["extra2"]) + } + if !score.Succeeded { + t.Error("expected Succeeded to be true") + } + if score.FieldsMatched != 3 { + t.Errorf("expected FieldsMatched to be 3, got %d", score.FieldsMatched) + } +} + +type WithCustomUnmarshaler struct { + Name string `json:"name"` + metadata +} + +func (w *WithCustomUnmarshaler) UnmarshalJSON(raw []byte) error { + type shadow WithCustomUnmarshaler + return UnmarshalRoot(raw, (*shadow)(w), &w.metadata) +} + +type HoldsCustomUnmarshaler struct { + WithCustom WithCustomUnmarshaler `json:"with_custom"` + OtherField string `json:"other"` +} + +func TestCustomUnmarshalScore(t *testing.T) { + var h HoldsCustomUnmarshaler + score, err := UnmarshalWithScore([]byte(`{"with_custom":{"name":"x"},"other":"y"}`), &h) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // The inner struct matches 1 field (name), outer matches 2 fields (with_custom + other). + // Inner score is added to outer via the custom unmarshaler path. + if score.FieldsMatched != 3 { + t.Errorf("expected FieldsMatched to be 3, got %d", score.FieldsMatched) + } + if h.WithCustom.Name != "x" { + t.Errorf("expected Name to be 'x', got %q", h.WithCustom.Name) + } +} + +func ptrTo[T any]() *T { + return new(T) +} + +func getBestScoring(variants []any, scores []unmarshalscore.Score) (any, error) { + bestScore := unmarshalscore.Score{} + bestIndex := -1 + for i, score := range scores { + if score.IsBetterThan(bestScore) { + bestScore = score + bestIndex = i + } + } + if bestIndex == -1 { + return nil, fmt.Errorf("no valid value found") + } + return variants[bestIndex], nil +} diff --git a/interactions/internal/apijson/default.go b/interactions/internal/apijson/default.go new file mode 100644 index 00000000..9509c62e --- /dev/null +++ b/interactions/internal/apijson/default.go @@ -0,0 +1,69 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apijson + +import ( + "fmt" + "reflect" + "strconv" +) + +// continueWithDefault wraps an encoder so that zero values are replaced with +// the default before encoding. The inner encoder handles quoting and escaping. +func continueWithDefault(field *field, k encoderFunc) encoderFunc { + dv, err := parseDefaultTag(field) + return func(e *encodeState, v reflect.Value, opts encOpts) { + if v.IsZero() { + if err != nil { + e.error(err) + return + } + k(e, dv, opts) + return + } + k(e, v, opts) + } +} + +// parseDefaultTag gets called during construction of the encoders +func parseDefaultTag(field *field) (reflect.Value, error) { + typ, defaultValue := field.typ, field.defaultValue + dv := reflect.New(typ).Elem() + switch typ.Kind() { + case reflect.String: + dv.SetString(defaultValue) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n, err := strconv.ParseInt(defaultValue, 10, 64) + if err != nil { + return reflect.Value{}, fmt.Errorf("json: invalid default %q for int field: %w", defaultValue, err) + } + dv.SetInt(n) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + n, err := strconv.ParseUint(defaultValue, 10, 64) + if err != nil { + return reflect.Value{}, fmt.Errorf("json: invalid default %q for uint field: %w", defaultValue, err) + } + dv.SetUint(n) + case reflect.Float32, reflect.Float64: + n, err := strconv.ParseFloat(defaultValue, 64) + if err != nil { + return reflect.Value{}, fmt.Errorf("json: invalid default %q for float field: %w", defaultValue, err) + } + dv.SetFloat(n) + case reflect.Bool: + dv.SetBool(defaultValue == "true") + } + return dv, nil +} diff --git a/interactions/internal/apijson/default_test.go b/interactions/internal/apijson/default_test.go new file mode 100644 index 00000000..3ff726bc --- /dev/null +++ b/interactions/internal/apijson/default_test.go @@ -0,0 +1,99 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apijson_test + +import ( + "fmt" + "google.golang.org/genai/interactions/internal/apijson" + "google.golang.org/genai/interactions/packages/apidata" + "testing" +) + +type defaultSimple struct { + Name string `json:"name" default:"HOUR"` + Value int `json:"value" default:"42"` + + apidata.DynamicFields `json:"-"` +} + +func (r defaultSimple) MarshalJSON() ([]byte, error) { + return apijson.MarshalRoot(r) +} + +// customString implements MarshalJSON to wrap in angle brackets. +type customString struct { + Val string +} + +func (c customString) MarshalJSON() ([]byte, error) { + return []byte(`"(` + c.Val + `)"`), nil +} + +func (c customString) IsZero() bool { + return c.Val == "" +} + +type defaultCustom struct { + Tag customString `json:"tag" default:"fallback"` + + apidata.DynamicFields `json:"-"` +} + +func (r defaultCustom) MarshalJSON() ([]byte, error) { + return apijson.MarshalRoot(r) +} + +func TestDefaultStringZeroUsesDefault(t *testing.T) { + got, err := apijson.Marshal(defaultSimple{}) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + want := `{"name":"HOUR","value":42}` + if string(got) != want { + t.Fatalf("expected %s, got %s", want, string(got)) + } +} + +func TestDefaultNonZeroIgnoresDefault(t *testing.T) { + got, err := apijson.Marshal(defaultSimple{Name: "custom", Value: 99}) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + want := `{"name":"custom","value":99}` + if string(got) != want { + t.Fatalf("expected %s, got %s", want, string(got)) + } +} + +func TestDefaultWithCustomMarshalJSON(t *testing.T) { + // Zero value — should the default kick in or the custom marshaler? + got, err := apijson.Marshal(defaultCustom{}) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + fmt.Printf("=== custom MarshalJSON + default, zero value ===\n") + fmt.Printf("got: %s\n", got) + + // Non-zero — custom marshaler should be used, default ignored. + got2, err := apijson.Marshal(defaultCustom{Tag: customString{Val: "hello"}}) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + fmt.Printf("=== custom MarshalJSON + default, non-zero ===\n") + fmt.Printf("got: %s\n", got2) + if string(got2) != `{"tag":"(hello)"}` { + t.Fatalf("expected custom marshaler output, got %s", got2) + } +} diff --git a/interactions/internal/apijson/dynamicfield_test.go b/interactions/internal/apijson/dynamicfield_test.go new file mode 100644 index 00000000..793ba982 --- /dev/null +++ b/interactions/internal/apijson/dynamicfield_test.go @@ -0,0 +1,503 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apijson_test + +import ( + "encoding/json" + "strings" + "testing" + + "google.golang.org/genai/interactions/internal/apijson" + "google.golang.org/genai/interactions/internal/apijson/unmarshalinfo" + "google.golang.org/genai/interactions/packages/apidata" +) + +func ptr[T any](v T) *T { return &v } + +// ── Types ──────────────────────────────────────────────────────────── + +type diffSimple struct { + Name string `json:"name"` + Value int `json:"value"` + Tag string `json:"tag"` + + apidata.DynamicFields `json:"-"` +} + +func (r diffSimple) MarshalJSON() ([]byte, error) { + return apijson.MarshalRoot(r) +} + +type diffRoundtrip struct { + Name string `json:"name"` + Value int `json:"value"` + + apidata.DynamicFields `json:"-"` + meta unmarshalinfo.Metadata +} + +func (r diffRoundtrip) MarshalJSON() ([]byte, error) { + return apijson.MarshalRoot(r) +} + +func (r *diffRoundtrip) UnmarshalJSON(raw []byte) error { + return apijson.UnmarshalRoot(raw, r, &r.meta) +} + +type diffUnion struct { + OfString *string `json:",inline,omitzero"` + OfInt *int `json:",inline,omitzero"` + + meta unmarshalinfo.Metadata `api:"union"` +} + +// diffChild has a MarshalJSON that injects extra keys. +type diffChild struct { + Tag string `json:"tag"` +} + +func (c diffChild) MarshalJSON() ([]byte, error) { + return []byte(`{"tag":"` + c.Tag + `","injected":true}`), nil +} + +type diffParent struct { + Label string `json:"label"` + Child diffChild `json:"child"` + + apidata.DynamicFields `json:"-"` + meta unmarshalinfo.Metadata +} + +func (p diffParent) MarshalJSON() ([]byte, error) { + return apijson.MarshalRoot(p) +} + +func (p *diffParent) UnmarshalJSON(raw []byte) error { + return apijson.UnmarshalRoot(raw, p, &p.meta) +} + +type diffWithInlineMap struct { + Name string `json:"name"` + Extra map[string]any `json:",inline"` + + apidata.DynamicFields `json:"-"` +} + +func (r diffWithInlineMap) MarshalJSON() ([]byte, error) { + return apijson.MarshalRoot(r) +} + +// ── Test helpers ───────────────────────────────────────────────────── + +// tjson wraps testing.T with JSON assertion helpers. +type tjson struct{ *testing.T } + +// Marshal marshals v and returns a jsonResult for chained assertions. +func (tj tjson) Marshal(v any) jsonResult { + tj.Helper() + got, err := apijson.Marshal(v) + if err != nil { + tj.Fatalf("Marshal: %v", err) + } + return jsonResult{tj, string(got)} +} + +// unmarshal unmarshals input into a new T. +func unmarshal[T any](tj tjson, input string) T { + tj.Helper() + var v T + if err := apijson.Unmarshal([]byte(input), &v); err != nil { + tj.Fatalf("Unmarshal: %v", err) + } + return v +} + +// jsonResult holds marshaled JSON for assertions. +type jsonResult struct { + tjson + raw string +} + +// Has asserts the JSON output contains the substring. +func (r jsonResult) Has(substr string) { + r.Helper() + if !strings.Contains(r.raw, substr) { + r.Errorf("expected %q in:\n%s", substr, r.raw) + } +} + +// Lacks asserts the JSON output does not contain the substring. +func (r jsonResult) Lacks(substr string) { + r.Helper() + if strings.Contains(r.raw, substr) { + r.Errorf("expected %q absent from:\n%s", substr, r.raw) + } +} + +// Equals asserts exact JSON string equality. +func (r jsonResult) Equals(want string) { + r.Helper() + if r.raw != want { + r.Errorf("expected:\n %s\ngot:\n %s", want, r.raw) + } +} + +// Keys returns the top-level keys of the JSON object. +func (r jsonResult) Keys() []string { + r.Helper() + var m map[string]any + if err := json.Unmarshal([]byte(r.raw), &m); err != nil { + r.Fatalf("invalid JSON: %s", r.raw) + } + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + return keys +} + +// ── Tests ──────────────────────────────────────────────────────────── + +func TestMarshalDynamicFields(t *testing.T) { + tests := []struct { + name string + val any + want string + }{ + { + name: "extra field added", + val: diffSimple{Name: "a", Value: 1, DynamicFields: apidata.DynamicFields{"extra": "v"}}, + want: `{"name":"a","value":1,"tag":"","extra":"v"}`, + }, + { + name: "omit native field", + val: diffSimple{Name: "a", Value: 1, DynamicFields: apidata.DynamicFields{"name": apidata.Omit}}, + want: `{"value":1,"tag":""}`, + }, + { + name: "replace native with raw JSON", + val: diffSimple{Name: "a", Value: 1, DynamicFields: apidata.DynamicFields{"name": apidata.Unknown(`42`)}}, + want: `{"name":42,"value":1,"tag":""}`, + }, + { + name: "replace native with null", + val: diffSimple{Name: "a", Value: 1, DynamicFields: apidata.DynamicFields{"name": apidata.Unknown(`null`)}}, + want: `{"name":null,"value":1,"tag":""}`, + }, + { + name: "replace native with array", + val: diffSimple{Name: "a", Value: 1, DynamicFields: apidata.DynamicFields{"value": apidata.Unknown(`[1,2,3]`)}}, + want: `{"name":"a","value":[1,2,3],"tag":""}`, + }, + { + name: "add complex nested JSON", + val: diffSimple{Name: "a", DynamicFields: apidata.DynamicFields{"complex": apidata.Unknown(`{"inner":{"deep":[1,2]},"flag":true}`)}}, + want: `{"name":"a","value":0,"tag":"","complex":{"inner":{"deep":[1,2]},"flag":true}}`, + }, + { + name: "omit nonexistent field is no-op", + val: diffSimple{Name: "a", DynamicFields: apidata.DynamicFields{"ghost": apidata.Omit}}, + want: `{"name":"a","value":0,"tag":""}`, + }, + { + name: "add via Unknown on nonexistent field", + val: diffSimple{Name: "a", Value: 1, DynamicFields: apidata.DynamicFields{"new_field": apidata.Unknown(`99`)}}, + want: `{"name":"a","value":1,"tag":"","new_field":99}`, + }, + { + name: "nil DynamicFields — baseline", + val: diffSimple{Name: "a", Value: 1}, + want: `{"name":"a","value":1,"tag":""}`, + }, + { + name: "empty DynamicFields matches nil", + val: diffSimple{Name: "a", Value: 1, DynamicFields: apidata.DynamicFields{}}, + want: `{"name":"a","value":1,"tag":""}`, + }, + { + name: "inline map and DynamicFields both contribute", + val: diffWithInlineMap{ + Name: "a", + Extra: map[string]any{"from_map": "m"}, + DynamicFields: apidata.DynamicFields{"from_extras": "e"}, + }, + want: `{"name":"a","from_map":"m","from_extras":"e"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tj := tjson{t} + tj.Marshal(tt.val).Equals(tt.want) + }) + } +} + +func TestMarshalInlineUnion(t *testing.T) { + tests := []struct { + name string + val any + want string + wantErr bool + }{ + {name: "string member", val: diffUnion{OfString: ptr("hello")}, want: `"hello"`}, + {name: "int member", val: diffUnion{OfInt: ptr(42)}, want: `42`}, + {name: "first non-zero wins", val: diffUnion{OfString: ptr("first"), OfInt: ptr(999)}, want: `"first"`}, + {name: "no member set errors", val: diffUnion{}, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := apijson.MarshalUnionStruct(tt.val) + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("MarshalUnionStruct: %v", err) + } + if string(got) != tt.want { + t.Errorf("expected %s, got %s", tt.want, string(got)) + } + }) + } +} + +func TestRoundtrip(t *testing.T) { + tests := []struct { + name string + input string + mutate func(*diffRoundtrip) + want string + }{ + { + name: "preserves single unknown field", + input: `{"name":"alice","value":1,"extra":"surprise"}`, + want: `{"name":"alice","value":1,"extra":"surprise"}`, + }, + { + name: "preserves multiple unknown fields", + input: `{"name":"bob","value":2,"extra1":"a","extra2":42}`, + want: `{"name":"bob","value":2,"extra1":"a","extra2":42}`, + }, + { + name: "preserves unknown null", + input: `{"name":"a","value":1,"extra":null}`, + want: `{"name":"a","value":1,"extra":null}`, + }, + { + name: "preserves unknown nested object", + input: `{"name":"a","value":1,"nested":{"x":1,"y":"two"}}`, + want: `{"name":"a","value":1,"nested":{"x":1,"y":"two"}}`, + }, + { + name: "preserves unknown array", + input: `{"name":"a","value":1,"items":[1,"two",true]}`, + want: `{"name":"a","value":1,"items":[1,"two",true]}`, + }, + { + name: "preserves unknown boolean", + input: `{"name":"a","value":1,"flag":true}`, + want: `{"name":"a","value":1,"flag":true}`, + }, + { + name: "no unknowns round-trips cleanly", + input: `{"name":"a","value":1}`, + want: `{"name":"a","value":1}`, + }, + { + name: "empty object round-trips to zero values", + input: `{}`, + want: `{"name":"","value":0}`, + }, + { + name: "mutated known field uses new value", + input: `{"name":"original","value":1}`, + mutate: func(o *diffRoundtrip) { o.Name = "modified" }, + want: `{"name":"modified","value":1}`, + }, + { + name: "Omit overrides captured unknown", + input: `{"name":"a","value":1,"extra":"captured"}`, + mutate: func(o *diffRoundtrip) { + o.DynamicFields["extra"] = apidata.Omit + }, + want: `{"name":"a","value":1}`, + }, + { + name: "Unknown overrides captured unknown", + input: `{"name":"a","value":1,"extra":"original"}`, + mutate: func(o *diffRoundtrip) { + o.DynamicFields["extra"] = apidata.Unknown(`{"replaced":true}`) + }, + want: `{"name":"a","value":1,"extra":{"replaced":true}}`, + }, + { + name: "all JSON types survive round-trip with mutation", + input: `{"name":"alice","value":1,"str":"s","num":3.14,"obj":{"k":"v"},"arr":[1,true],"nil":null,"bool":true}`, + mutate: func(o *diffRoundtrip) { o.Name = "modified" }, + want: `{"name":"modified","value":1,"arr":[1,true],"bool":true,"nil":null,"num":3.14,"obj":{"k":"v"},"str":"s"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tj := tjson{t} + obj := unmarshal[diffRoundtrip](tj, tt.input) + if tt.mutate != nil { + tt.mutate(&obj) + } + tj.Marshal(obj).Equals(tt.want) + }) + } +} + +func TestRoundtripCustomMarshalJSON(t *testing.T) { + t.Run("child custom MarshalJSON keys appear in parent output", func(t *testing.T) { + tj := tjson{t} + r := tj.Marshal(diffParent{ + Label: "parent", + Child: diffChild{Tag: "hello"}, + DynamicFields: apidata.DynamicFields{ + "label": apidata.Unknown(`"overridden"`), + }, + }) + r.Has(`"label":"overridden"`) + r.Has(`"injected":true`) + r.Has(`"tag":"hello"`) + }) + + t.Run("child UnmarshalJSON respected and unknowns preserved", func(t *testing.T) { + tj := tjson{t} + obj := unmarshal[diffParent](tj, `{"label":"p","child":{"tag":"world"},"unknown":"kept"}`) + if obj.Label != "p" { + t.Fatalf("expected label=p, got %s", obj.Label) + } + if obj.Child.Tag != "world" { + t.Fatalf("expected child.tag=world, got %s", obj.Child.Tag) + } + raw, ok := obj.DynamicFields["unknown"].(apidata.Unknown) + if !ok { + t.Fatalf("expected apidata.Unknown, got %T", obj.DynamicFields["unknown"]) + } + if string(raw) != `"kept"` { + t.Fatalf("expected raw \"kept\", got %s", string(raw)) + } + }) +} + +// TestDynamicFieldsKeyWithSjsonMetacharacters pins the escaping behavior +// for keys that contain sjson path metacharacters (`.`, `*`, `?`, +// `#`, `|`, `\`). Without escaping, sjson would reshape/drop these +// keys silently — see MarshalRoot in apidata.go. +// +// Covers both directions: +// - Set via DynamicFields by the caller (marshal path) +// - Captured from inbound JSON (unmarshal → re-marshal round-trip) +func TestDynamicFieldsKeyWithSjsonMetacharacters(t *testing.T) { + metas := []string{"a.b", "a*b", "a?b", "a#b", "a|b", `a\b`} + + t.Run("marshal preserves the key verbatim", func(t *testing.T) { + for _, k := range metas { + obj := diffSimple{Name: "x", Value: 1, DynamicFields: apidata.DynamicFields{k: "v"}} + got, err := apijson.Marshal(obj) + if err != nil { + t.Errorf("%q: Marshal: %v", k, err) + continue + } + // Re-parse and verify the key lands at the top level. + var m map[string]any + if err := json.Unmarshal(got, &m); err != nil { + t.Errorf("%q: output isn't valid JSON: %v\n raw: %s", k, err, got) + continue + } + if _, ok := m[k]; !ok { + t.Errorf("%q: missing from top-level output; got: %s", k, got) + } + } + }) + + t.Run("round-trip through unmarshal preserves the key verbatim", func(t *testing.T) { + for _, k := range metas { + // Hand-roll the JSON so we control the exact key bytes. + kEsc, _ := json.Marshal(k) + input := `{"name":"x","value":1,"tag":"","` + string(kEsc[1:len(kEsc)-1]) + `":"v"}` + + var obj diffRoundtrip + if err := apijson.Unmarshal([]byte(input), &obj); err != nil { + t.Errorf("%q: Unmarshal: %v", k, err) + continue + } + if _, ok := obj.DynamicFields[k]; !ok { + t.Errorf("%q: decoder didn't capture into DynamicFields: %v", k, obj.DynamicFields) + continue + } + got, err := apijson.Marshal(obj) + if err != nil { + t.Errorf("%q: Marshal: %v", k, err) + continue + } + var m map[string]any + if err := json.Unmarshal(got, &m); err != nil { + t.Errorf("%q: re-marshaled output isn't valid JSON: %v\n raw: %s", k, err, got) + continue + } + if _, ok := m[k]; !ok { + t.Errorf("%q: round-trip dropped the key; got: %s", k, got) + } + } + }) + + t.Run("Omit with metacharacter key deletes only that key", func(t *testing.T) { + // Seed both `a.b` and a sibling `a` via DynamicFields, then Omit `a.b`. + // Without escaping, Omit("a.b") would path-navigate and corrupt the + // top-level `a` instead. + obj := diffSimple{ + Name: "n", + Value: 1, + DynamicFields: apidata.DynamicFields{ + "a": "keep", + "a.b": apidata.Omit, // no-op: key doesn't exist in the marshaled output + }, + } + got, err := apijson.Marshal(obj) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(got, &m); err != nil { + t.Fatalf("invalid JSON: %v\n raw: %s", err, got) + } + if m["a"] != "keep" { + t.Errorf("Omit on %q corrupted sibling %q; got: %s", "a.b", "a", got) + } + }) +} + +// TestDynamicFieldsEmptyKeyDropped — empty-string keys are silently +// dropped on marshal (sjson rejects empty paths; handling them +// specially isn't worth the complexity). +func TestDynamicFieldsEmptyKeyDropped(t *testing.T) { + obj := diffSimple{Name: "n", DynamicFields: apidata.DynamicFields{"": "ignored"}} + got, err := apijson.Marshal(obj) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + if strings.Contains(string(got), `"":`) { + t.Errorf("empty-key entry leaked into output: %s", got) + } +} diff --git a/interactions/internal/apijson/encode.go b/interactions/internal/apijson/encode.go new file mode 100644 index 00000000..17c61f02 --- /dev/null +++ b/interactions/internal/apijson/encode.go @@ -0,0 +1,1507 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Vendored from Go 1.24.0-pre-release +// To find alterations, check package shims, and comments beginning in SHIM(). +// +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package json implements encoding and decoding of JSON as defined in +// RFC 7159. The mapping between JSON and Go values is described +// in the documentation for the Marshal and Unmarshal functions. +// +// See "JSON and Go" for an introduction to this package: +// https://golang.org/doc/articles/json_and_go.html +package apijson + +import ( + "bytes" + "cmp" + "encoding" + "encoding/base64" + "fmt" + "google.golang.org/genai/interactions/internal/apijson/shims" + "math" + "reflect" + "slices" + "strconv" + "strings" + "sync" + "unicode" + "unicode/utf8" + _ "unsafe" // for linkname +) + +// Marshal returns the JSON encoding of v. +// +// Marshal traverses the value v recursively. +// If an encountered value implements [Marshaler] +// and is not a nil pointer, Marshal calls [Marshaler.MarshalJSON] +// to produce JSON. If no [Marshaler.MarshalJSON] method is present but the +// value implements [encoding.TextMarshaler] instead, Marshal calls +// [encoding.TextMarshaler.MarshalText] and encodes the result as a JSON string. +// The nil pointer exception is not strictly necessary +// but mimics a similar, necessary exception in the behavior of +// [Unmarshaler.UnmarshalJSON]. +// +// Otherwise, Marshal uses the following type-dependent default encodings: +// +// Boolean values encode as JSON booleans. +// +// Floating point, integer, and [Number] values encode as JSON numbers. +// NaN and +/-Inf values will return an [UnsupportedValueError]. +// +// String values encode as JSON strings coerced to valid UTF-8, +// replacing invalid bytes with the Unicode replacement rune. +// So that the JSON will be safe to embed inside HTML