feat: gitea client

This commit is contained in:
2026-02-12 20:58:55 +01:00
parent 8583ab48ce
commit 9bd7d363ba
1693 changed files with 653995 additions and 49 deletions

View File

@@ -0,0 +1,193 @@
// Copyright 2024 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"
"errors"
"fmt"
"time"
gl "cloud.google.com/go/ai/generativelanguage/apiv1beta"
pb "cloud.google.com/go/ai/generativelanguage/apiv1beta/generativelanguagepb"
"google.golang.org/api/iterator"
durationpb "google.golang.org/protobuf/types/known/durationpb"
fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
)
type cacheClient = gl.CacheClient
var (
newCacheClient = gl.NewCacheClient
newCacheRESTClient = gl.NewCacheRESTClient
)
// GenerativeModelFromCachedContent returns a [GenerativeModel] that uses the given [CachedContent].
// The argument should come from a call to [Client.CreateCachedContent] or [Client.GetCachedContent].
func (c *Client) GenerativeModelFromCachedContent(cc *CachedContent) *GenerativeModel {
return &GenerativeModel{
c: c,
fullName: cc.Model,
CachedContentName: cc.Name,
}
}
// CreateCachedContent creates a new CachedContent.
// The argument should contain a model name and some data to be cached, which can include
// contents, a system instruction, tools and/or tool configuration. It can also
// include an expiration time or TTL. But it should not include a name; the system
// will generate one.
//
// The return value will contain the name, which should be used to refer to the CachedContent
// in other API calls. It will also hold various metadata like expiration and creation time.
// It will not contain any of the actual content provided as input.
//
// You can use the return value to create a model with [Client.GenerativeModelFromCachedContent].
// Or you can set [GenerativeModel.CachedContentName] to the name of the CachedContent, in which
// case you must ensure that the model provided in this call matches the name in the [GenerativeModel].
func (c *Client) CreateCachedContent(ctx context.Context, cc *CachedContent) (*CachedContent, error) {
if cc.Name != "" {
return nil, errors.New("genai.CreateCachedContent: do not provide a name; one will be generated")
}
pcc := cc.toProto()
pcc.Model = Ptr(fullModelName(cc.Model))
req := &pb.CreateCachedContentRequest{
CachedContent: pcc,
}
debugPrint(req)
return c.cachedContentFromProto(c.cc.CreateCachedContent(ctx, req))
}
// GetCachedContent retrieves the CachedContent with the given name.
func (c *Client) GetCachedContent(ctx context.Context, name string) (*CachedContent, error) {
return c.cachedContentFromProto(c.cc.GetCachedContent(ctx, &pb.GetCachedContentRequest{Name: name}))
}
// DeleteCachedContent deletes the CachedContent with the given name.
func (c *Client) DeleteCachedContent(ctx context.Context, name string) error {
return c.cc.DeleteCachedContent(ctx, &pb.DeleteCachedContentRequest{Name: name})
}
// CachedContentToUpdate specifies which fields of a CachedContent to modify in a call to
// [Client.UpdateCachedContent].
type CachedContentToUpdate struct {
// If non-nil, update the expire time or TTL.
Expiration *ExpireTimeOrTTL
}
// UpdateCachedContent modifies the [CachedContent] according to the values
// of the [CachedContentToUpdate] struct.
// It returns the modified CachedContent.
//
// The argument CachedContent must have its Name field populated.
// If its UpdateTime field is non-zero, it will be compared with the update time
// of the stored CachedContent and the call will fail if they differ.
// This avoids a race condition when two updates are attempted concurrently.
// All other fields of the argument CachedContent are ignored.
func (c *Client) UpdateCachedContent(ctx context.Context, cc *CachedContent, ccu *CachedContentToUpdate) (*CachedContent, error) {
if ccu == nil || ccu.Expiration == nil {
return nil, errors.New("genai.UpdateCachedContent: no update specified")
}
cc2 := &CachedContent{
Name: cc.Name,
UpdateTime: cc.UpdateTime,
Expiration: *ccu.Expiration,
}
mask := "expire_time"
if ccu.Expiration.ExpireTime.IsZero() {
mask = "ttl"
}
req := &pb.UpdateCachedContentRequest{
CachedContent: cc2.toProto(),
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{mask}},
}
debugPrint(req)
return c.cachedContentFromProto(c.cc.UpdateCachedContent(ctx, req))
}
// ListCachedContents lists all the CachedContents associated with the project and location.
func (c *Client) ListCachedContents(ctx context.Context) *CachedContentIterator {
return &CachedContentIterator{
it: c.cc.ListCachedContents(ctx, &pb.ListCachedContentsRequest{}),
}
}
// A CachedContentIterator iterates over CachedContents.
type CachedContentIterator struct {
it *gl.CachedContentIterator
}
// Next returns the next result. Its second return value is iterator.Done if there are no more
// results. Once Next returns Done, all subsequent calls will return Done.
func (it *CachedContentIterator) Next() (*CachedContent, error) {
m, err := it.it.Next()
if err != nil {
return nil, err
}
return (CachedContent{}).fromProto(m), nil
}
// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
func (it *CachedContentIterator) PageInfo() *iterator.PageInfo {
return it.it.PageInfo()
}
func (c *Client) cachedContentFromProto(pcc *pb.CachedContent, err error) (*CachedContent, error) {
if err != nil {
return nil, err
}
cc := (CachedContent{}).fromProto(pcc)
return cc, nil
}
// ExpireTimeOrTTL describes the time when a resource expires.
// If ExpireTime is non-zero, it is the expiration time.
// Otherwise, the expiration time is the value of TTL ("time to live") added
// to the current time.
type ExpireTimeOrTTL struct {
ExpireTime time.Time
TTL time.Duration
}
// populateCachedContentTo populates some fields of p from v.
func populateCachedContentTo(p *pb.CachedContent, v *CachedContent) {
exp := v.Expiration
if !exp.ExpireTime.IsZero() {
p.Expiration = &pb.CachedContent_ExpireTime{
ExpireTime: timestamppb.New(exp.ExpireTime),
}
} else if exp.TTL != 0 {
p.Expiration = &pb.CachedContent_Ttl{
Ttl: durationpb.New(exp.TTL),
}
}
// If both fields of v.Expiration are zero, leave p.Expiration unset.
}
// populateCachedContentFrom populates some fields of v from p.
func populateCachedContentFrom(v *CachedContent, p *pb.CachedContent) {
if p.Expiration == nil {
return
}
switch e := p.Expiration.(type) {
case *pb.CachedContent_ExpireTime:
v.Expiration.ExpireTime = pvTimeFromProto(e.ExpireTime)
case *pb.CachedContent_Ttl:
v.Expiration.TTL = e.Ttl.AsDuration()
default:
panic(fmt.Sprintf("unknown type of CachedContent.Expiration: %T", p.Expiration))
}
}

View File

@@ -0,0 +1,89 @@
// Copyright 2023 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"
)
// A ChatSession provides interactive chat.
type ChatSession struct {
m *GenerativeModel
History []*Content
}
// StartChat starts a chat session.
func (m *GenerativeModel) StartChat() *ChatSession {
return &ChatSession{m: m}
}
// SendMessage sends a request to the model as part of a chat session.
func (cs *ChatSession) SendMessage(ctx context.Context, parts ...Part) (*GenerateContentResponse, error) {
// Call the underlying client with the entire history plus the argument Content.
cs.History = append(cs.History, NewUserContent(parts...))
req, err := cs.m.newGenerateContentRequest(cs.History...)
if err != nil {
return nil, err
}
req.GenerationConfig.CandidateCount = Ptr[int32](1)
resp, err := cs.m.generateContent(ctx, req)
if err != nil {
return nil, err
}
cs.addToHistory(resp.Candidates)
return resp, nil
}
// SendMessageStream is like SendMessage, but with a streaming request.
func (cs *ChatSession) SendMessageStream(ctx context.Context, parts ...Part) *GenerateContentResponseIterator {
cs.History = append(cs.History, NewUserContent(parts...))
req, err := cs.m.newGenerateContentRequest(cs.History...)
if err != nil {
return &GenerateContentResponseIterator{err: err}
}
req.GenerationConfig.CandidateCount = Ptr[int32](1)
streamClient, err := cs.m.c.gc.StreamGenerateContent(ctx, req)
return &GenerateContentResponseIterator{
sc: streamClient,
err: err,
cs: cs,
}
}
// By default, use the first candidate for history. The user can modify that if they want.
func (cs *ChatSession) addToHistory(cands []*Candidate) bool {
if len(cands) > 0 {
c := cands[0].Content
if c == nil {
return false
}
c.Role = roleModel
cs.History = append(cs.History, copySanitizedModelContent(c))
return true
}
return false
}
// copySanitizedModelContent creates a (shallow) copy of c with role set to
// model and empty text parts removed.
func copySanitizedModelContent(c *Content) *Content {
newc := &Content{Role: roleModel}
for _, part := range c.Parts {
if t, ok := part.(Text); !ok || len(string(t)) > 0 {
newc.Parts = append(newc.Parts, part)
}
}
return newc
}

View File

@@ -0,0 +1,476 @@
// Copyright 2023 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.
// For the following go:generate line to work, install the protoveener tool:
// git clone https://github.com/googleapis/google-cloud-go
// cd google-cloud-go
// go install ./internal/protoveneer/cmd/protoveneer
//
//go:generate ./generate.sh
package genai
import (
"context"
"errors"
"fmt"
"io"
"reflect"
"strings"
gl "cloud.google.com/go/ai/generativelanguage/apiv1beta"
pb "cloud.google.com/go/ai/generativelanguage/apiv1beta/generativelanguagepb"
"github.com/google/generative-ai-go/genai/internal"
gld "github.com/google/generative-ai-go/genai/internal/generativelanguage/v1beta" // discovery client
"google.golang.org/api/iterator"
"google.golang.org/api/option"
)
// A Client is a Google generative AI client.
type Client struct {
gc *gl.GenerativeClient
mc *gl.ModelClient
fc *gl.FileClient
cc *gl.CacheClient
ds *gld.Service
}
// NewClient creates a new Google generative AI client.
//
// Clients should be reused instead of created as needed. The methods of Client
// are safe for concurrent use by multiple goroutines.
//
// You may configure the client by passing in options from the [google.golang.org/api/option]
// package.
func NewClient(ctx context.Context, opts ...option.ClientOption) (*Client, error) {
if !hasAuthOption(opts) {
return nil, errors.New(`You need an auth option to use this client.
for an API Key: Visit https://ai.google.dev to get one, put it in an environment variable like GEMINI_API_KEY,
then pass it as an option:
genai.NewClient(ctx, option.WithAPIKey(os.Getenv("GEMINI_API_KEY")))
(If you're doing that already, then maybe the environment variable is empty or unset.)
Import the option package as "google.golang.org/api/option".`)
}
gc, err := gl.NewGenerativeRESTClient(ctx, opts...)
if err != nil {
return nil, fmt.Errorf("creating generative client: %w", err)
}
mc, err := gl.NewModelRESTClient(ctx, opts...)
if err != nil {
return nil, fmt.Errorf("creating model client: %w", err)
}
fc, err := gl.NewFileRESTClient(ctx, opts...)
if err != nil {
return nil, fmt.Errorf("creating file client: %w", err)
}
// Workaround for https://github.com/google/generative-ai-go/issues/151
optsForCache := removeHTTPClientOption(opts)
cc, err := gl.NewCacheClient(ctx, optsForCache...)
if err != nil {
return nil, fmt.Errorf("creating cache client: %w", err)
}
ds, err := gld.NewService(ctx, opts...)
if err != nil {
return nil, fmt.Errorf("creating discovery client: %w", err)
}
kvs := []string{"gccl", "v" + internal.Version, "genai-go", internal.Version}
if a, ok := optionOfType[*clientInfo](opts); ok {
kvs = append(kvs, a.key, a.value)
}
gc.SetGoogleClientInfo(kvs...)
mc.SetGoogleClientInfo(kvs...)
fc.SetGoogleClientInfo(kvs...)
return &Client{gc, mc, fc, cc, ds}, nil
}
// hasAuthOption reports whether an authentication-related option was provided.
//
// There is no good way to make these checks, because the types of the options
// are unexported, and the struct that they populates is in an internal package.
func hasAuthOption(opts []option.ClientOption) bool {
for _, opt := range opts {
v := reflect.ValueOf(opt)
ts := v.Type().String()
switch ts {
case "option.withAPIKey":
return v.String() != ""
case "option.withHTTPClient",
"option.withTokenSource",
"option.withCredFile",
"option.withCredentialsJSON":
return true
}
}
return false
}
// removeHTTPClientOption removes option.withHTTPClient from the given list
// of options, if it exists; it returns the new (filtered) list.
func removeHTTPClientOption(opts []option.ClientOption) []option.ClientOption {
var newOpts []option.ClientOption
for _, opt := range opts {
ts := reflect.ValueOf(opt).Type().String()
if ts != "option.withHTTPClient" {
newOpts = append(newOpts, opt)
}
}
return newOpts
}
// Close closes the client.
func (c *Client) Close() error {
return errors.Join(c.gc.Close(), c.mc.Close(), c.fc.Close())
}
// GenerativeModel is a model that can generate text.
// Create one with [Client.GenerativeModel], then configure
// it by setting the exported fields.
type GenerativeModel struct {
c *Client
fullName string
GenerationConfig
SafetySettings []*SafetySetting
Tools []*Tool
ToolConfig *ToolConfig // configuration for tools
// SystemInstruction (also known as "system prompt") is a more forceful prompt to the model.
// The model will adhere the instructions more strongly than if they appeared in a normal prompt.
SystemInstruction *Content
// The name of the CachedContent to use.
// Must have already been created with [Client.CreateCachedContent].
CachedContentName string
}
// GenerativeModel creates a new instance of the named generative model.
// For instance, "gemini-1.0-pro" or "models/gemini-1.0-pro".
//
// To access a tuned model named NAME, pass "tunedModels/NAME".
func (c *Client) GenerativeModel(name string) *GenerativeModel {
return &GenerativeModel{
c: c,
fullName: fullModelName(name),
}
}
func fullModelName(name string) string {
if strings.ContainsRune(name, '/') {
return name
}
return "models/" + name
}
// GenerateContent produces a single request and response.
func (m *GenerativeModel) GenerateContent(ctx context.Context, parts ...Part) (*GenerateContentResponse, error) {
content := NewUserContent(parts...)
req, err := m.newGenerateContentRequest(content)
if err != nil {
return nil, err
}
res, err := m.c.gc.GenerateContent(ctx, req)
if err != nil {
return nil, err
}
return protoToResponse(res)
}
// GenerateContentStream returns an iterator that enumerates responses.
func (m *GenerativeModel) GenerateContentStream(ctx context.Context, parts ...Part) *GenerateContentResponseIterator {
iter := &GenerateContentResponseIterator{}
req, err := m.newGenerateContentRequest(NewUserContent(parts...))
if err != nil {
iter.err = err
} else {
iter.sc, iter.err = m.c.gc.StreamGenerateContent(ctx, req)
}
return iter
}
func (m *GenerativeModel) generateContent(ctx context.Context, req *pb.GenerateContentRequest) (*GenerateContentResponse, error) {
streamClient, err := m.c.gc.StreamGenerateContent(ctx, req)
iter := &GenerateContentResponseIterator{
sc: streamClient,
err: err,
}
for {
_, err := iter.Next()
if err == iterator.Done {
return iter.MergedResponse(), nil
}
if err != nil {
return nil, err
}
}
}
func (m *GenerativeModel) newGenerateContentRequest(contents ...*Content) (*pb.GenerateContentRequest, error) {
return pvCatchPanic(func() *pb.GenerateContentRequest {
var cc *string
if m.CachedContentName != "" {
cc = &m.CachedContentName
}
req := &pb.GenerateContentRequest{
Model: m.fullName,
Contents: transformSlice(contents, (*Content).toProto),
SafetySettings: transformSlice(m.SafetySettings, (*SafetySetting).toProto),
Tools: transformSlice(m.Tools, (*Tool).toProto),
ToolConfig: m.ToolConfig.toProto(),
GenerationConfig: m.GenerationConfig.toProto(),
SystemInstruction: m.SystemInstruction.toProto(),
CachedContent: cc,
}
debugPrint(req)
return req
})
}
// GenerateContentResponseIterator is an iterator over GnerateContentResponse.
type GenerateContentResponseIterator struct {
sc pb.GenerativeService_StreamGenerateContentClient
err error
merged *GenerateContentResponse
cs *ChatSession
}
// Next returns the next response.
func (iter *GenerateContentResponseIterator) Next() (*GenerateContentResponse, error) {
if iter.err != nil {
return nil, iter.err
}
resp, err := iter.sc.Recv()
iter.err = err
if err == io.EOF {
if iter.cs != nil && iter.merged != nil {
iter.cs.addToHistory(iter.merged.Candidates)
}
return nil, iterator.Done
}
if err != nil {
return nil, err
}
gcp, err := protoToResponse(resp)
if err != nil {
iter.err = err
return nil, err
}
// Merge this response in with the ones we've already seen.
iter.merged = joinResponses(iter.merged, gcp)
// If this is part of a ChatSession, remember the response for the history.
return gcp, nil
}
func protoToResponse(resp *pb.GenerateContentResponse) (*GenerateContentResponse, error) {
gcp, err := fromProto[GenerateContentResponse](resp)
if err != nil {
return nil, err
}
if gcp == nil {
return nil, errors.New("empty response from model")
}
// Assume a non-nil PromptFeedback is an error.
// TODO: confirm.
if gcp.PromptFeedback != nil && gcp.PromptFeedback.BlockReason != BlockReasonUnspecified {
return nil, &BlockedError{PromptFeedback: gcp.PromptFeedback}
}
// If any candidate is blocked, error.
// TODO: is this too harsh?
for _, c := range gcp.Candidates {
if c.FinishReason == FinishReasonSafety || c.FinishReason == FinishReasonRecitation {
return nil, &BlockedError{Candidate: c}
}
}
return gcp, nil
}
// MergedResponse returns the result of combining all the streamed responses seen so far.
// After iteration completes, the merged response should match the response obtained without streaming
// (that is, if [GenerativeModel.GenerateContent] were called).
func (iter *GenerateContentResponseIterator) MergedResponse() *GenerateContentResponse {
return iter.merged
}
// CountTokens counts the number of tokens in the content.
func (m *GenerativeModel) CountTokens(ctx context.Context, parts ...Part) (*CountTokensResponse, error) {
req, err := m.newCountTokensRequest(NewUserContent(parts...))
if err != nil {
return nil, err
}
res, err := m.c.gc.CountTokens(ctx, req)
if err != nil {
return nil, err
}
return fromProto[CountTokensResponse](res)
}
func (m *GenerativeModel) newCountTokensRequest(contents ...*Content) (*pb.CountTokensRequest, error) {
gcr, err := m.newGenerateContentRequest(contents...)
if err != nil {
return nil, err
}
req := &pb.CountTokensRequest{
Model: m.fullName,
GenerateContentRequest: gcr,
}
debugPrint(req)
return req, nil
}
// Info returns information about the model.
func (m *GenerativeModel) Info(ctx context.Context) (*ModelInfo, error) {
return m.c.modelInfo(ctx, m.fullName)
}
func (c *Client) modelInfo(ctx context.Context, fullName string) (*ModelInfo, error) {
req := &pb.GetModelRequest{Name: fullName}
debugPrint(req)
res, err := c.mc.GetModel(ctx, req)
if err != nil {
return nil, err
}
return fromProto[ModelInfo](res)
}
// A BlockedError indicates that the model's response was blocked.
// There can be two underlying causes: the prompt or a candidate response.
type BlockedError struct {
// If non-nil, the model's response was blocked.
// Consult the FinishReason field for details.
Candidate *Candidate
// If non-nil, there was a problem with the prompt.
PromptFeedback *PromptFeedback
}
func (e *BlockedError) Error() string {
var b strings.Builder
fmt.Fprintf(&b, "blocked: ")
if e.Candidate != nil {
fmt.Fprintf(&b, "candidate: %s", e.Candidate.FinishReason)
}
if e.PromptFeedback != nil {
if e.Candidate != nil {
fmt.Fprintf(&b, ", ")
}
fmt.Fprintf(&b, "prompt: %v", e.PromptFeedback.BlockReason)
}
return b.String()
}
// joinResponses merges the two responses, which should be the result of a streaming call.
// The first argument is modified.
func joinResponses(dest, src *GenerateContentResponse) *GenerateContentResponse {
if dest == nil {
return src
}
dest.Candidates = joinCandidateLists(dest.Candidates, src.Candidates)
// Keep dest.PromptFeedback.
// TODO: Take the last UsageMetadata.
return dest
}
func joinCandidateLists(dest, src []*Candidate) []*Candidate {
indexToSrcCandidate := map[int32]*Candidate{}
for _, s := range src {
indexToSrcCandidate[s.Index] = s
}
for _, d := range dest {
s := indexToSrcCandidate[d.Index]
if s != nil {
d.Content = joinContent(d.Content, s.Content)
// Take the last of these.
d.FinishReason = s.FinishReason
// d.FinishMessage = s.FinishMessage
d.SafetyRatings = s.SafetyRatings
d.CitationMetadata = joinCitationMetadata(d.CitationMetadata, s.CitationMetadata)
}
}
return dest
}
func joinCitationMetadata(dest, src *CitationMetadata) *CitationMetadata {
if dest == nil {
return src
}
if src == nil {
return dest
}
dest.CitationSources = append(dest.CitationSources, src.CitationSources...)
return dest
}
func joinContent(dest, src *Content) *Content {
if dest == nil {
return src
}
if src == nil {
return dest
}
// Assume roles are the same.
dest.Parts = joinParts(dest.Parts, src.Parts)
return dest
}
func joinParts(dest, src []Part) []Part {
return mergeTexts(append(dest, src...))
}
func mergeTexts(in []Part) []Part {
var out []Part
i := 0
for i < len(in) {
if t, ok := in[i].(Text); ok {
texts := []string{string(t)}
var j int
for j = i + 1; j < len(in); j++ {
if t, ok := in[j].(Text); ok {
texts = append(texts, string(t))
} else {
break
}
}
// j is just after the last Text.
out = append(out, Text(strings.Join(texts, "")))
i = j
} else {
out = append(out, in[i])
i++
}
}
return out
}
// transformSlice applies f to each element of from and returns
// a new slice with the results.
func transformSlice[From, To any](from []From, f func(From) To) []To {
if from == nil {
return nil
}
to := make([]To, len(from))
for i, e := range from {
to[i] = f(e)
}
return to
}
func fromProto[V interface{ fromProto(P) *V }, P any](p P) (*V, error) {
var v V
return pvCatchPanic(func() *V { return v.fromProto(p) })
}

View File

@@ -0,0 +1,237 @@
# Copyright 2023 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.
# Configuration for the protoveneer tool.
package: genai
protoImportPath: cloud.google.com/go/ai/generativelanguage/apiv1beta/generativelanguagepb
types:
HarmCategory:
protoPrefix: HarmCategory_HARM_CATEGORY_
docVerb: specifies
SafetySetting_HarmBlockThreshold:
name: HarmBlockThreshold
protoPrefix: SafetySetting_BLOCK_
veneerPrefix: HarmBlock
docVerb: specifies
valueNames:
SafetySetting_HARM_BLOCK_THRESHOLD_UNSPECIFIED: HarmBlockUnspecified
SafetyRating_HarmProbability:
name: HarmProbability
protoPrefix: SafetyRating_
docVerb: specifies
valueNames:
SafetyRating_HARM_PROBABILITY_UNSPECIFIED: HarmProbabilityUnspecified
Candidate_FinishReason:
name: FinishReason
protoPrefix: Candidate_
GenerateContentResponse:
doc: 'is the response from a GenerateContent or GenerateContentStream call.'
GenerateContentResponse_PromptFeedback_BlockReason:
name: BlockReason
protoPrefix: GenerateContentResponse_PromptFeedback_
Content:
fields:
Parts:
type: '[]Part'
Blob:
fields:
MimeType:
name: MIMEType
docVerb: contains
FileData:
fields:
MimeType:
name: MIMEType
doc: |
The IANA standard MIME type of the source data.
If present, this overrides the MIME type specified or inferred
when the file was uploaded.
The supported MIME types are documented on [this page].
[this page]: https://ai.google.dev/gemini-api/docs/prompting_with_media?lang=go#supported_file_formats
FileUri:
name: URI
doc: 'The URI returned from UploadFile or GetFile.'
GenerationConfig:
fields:
ResponseMimeType:
name: ResponseMIMEType
SafetySetting:
SafetyRating:
docVerb: 'is the'
CitationMetadata:
CitationSource:
docVerb: contains
fields:
Uri:
name: URI
License:
type: string
Candidate:
fields:
Index:
type: int32
GroundingAttributions:
omit: true
GenerateContentResponse_PromptFeedback:
name: PromptFeedback
docVerb: contains
CountTokensResponse:
TaskType:
protoPrefix: TaskType
valueNames:
TaskType_TASK_TYPE_UNSPECIFIED: TaskTypeUnspecified
EmbedContentResponse:
BatchEmbedContentsResponse:
ContentEmbedding:
Model:
name: ModelInfo
doc: 'is information about a language model.'
fields:
BaseModelId:
name: BaseModelID
Temperature:
type: float32
TopP:
type: float32
TopK:
type: int32
# Types for function calling
Tool:
fields:
FunctionDeclarations:
doc: |
Optional. A list of FunctionDeclarations available to the model that
can be used for function calling. The model or system does not execute
the function. Instead the defined function may be returned as a [FunctionCall]
part with arguments to the client side for execution. The next conversation
turn may contain a [FunctionResponse] with the role "function" generation
context for the next model turn.
ToolConfig:
FunctionDeclaration:
FunctionCall:
FunctionResponse:
Schema:
Type:
protoPrefix: Type_
veneerPrefix: ''
FunctionCallingConfig:
doc: 'holds configuration for function calling.'
FunctionCallingConfig_Mode:
name: FunctionCallingMode
protoPrefix: FunctionCallingConfig
veneerPrefix: FunctionCalling
valueNames:
FunctionCallingConfig_MODE_UNSPECIFIED: FunctionCallingUnspecified
File:
populateToFrom: populateFileTo, populateFileFrom
fields:
Uri:
name: URI
MimeType:
name: MIMEType
Metadata:
type: '*FileMetadata'
noConvert: true
doc: 'Metadata for the File.'
VideoMetadata:
fields:
VideoDuration:
name: Duration
File_State:
name: FileState
docVerb: represents
protoPrefix: File
veneerPrefix: FileState
valueNames:
File_STATE_UNSPECIFIED: FileStateUnspecified
GenerateContentResponse_UsageMetadata:
name: UsageMetadata
fields:
PromptTokenCount:
type: int32
CandidatesTokenCount:
type: int32
TotalTokenCount:
type: int32
CachedContent:
populateToFrom: populateCachedContentTo, populateCachedContentFrom
fields:
Expiration:
type: ExpireTimeOrTTL
noConvert: true
Name:
type: string
Model:
type: string
DisplayName:
type: string
CachedContent_UsageMetadata:
name: CachedContentUsageMetadata
CodeExecution:
ExecutableCode:
CodeExecutionResult:
ExecutableCode_Language:
name: ExecutableCodeLanguage
protoPrefix: ExecutableCode
veneerPrefix: ExecutableCode
CodeExecutionResult_Outcome:
name: CodeExecutionResultOutcome
protoPrefix: CodeExecutionResult
veneerPrefix: CodeExecutionResult
valueNames:
CodeExecutionResult_OUTCOME_OK: CodeExecutionResultOutcomeOK
# Omit everything not explicitly configured.
omitTypes:
- '*'
converters:
Part: partToProto, partFromProto

View File

@@ -0,0 +1,182 @@
// Copyright 2023 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 (
"fmt"
pb "cloud.google.com/go/ai/generativelanguage/apiv1beta/generativelanguagepb"
)
const (
roleUser = "user"
roleModel = "model"
)
// A Part is a piece of model content.
// A Part can be one of the following types:
// - Text
// - Blob
// - FunctionCall
// - FunctionResponse
// - ExecutableCode
// - CodeExecutionResult
type Part interface {
toPart() *pb.Part
}
func partToProto(p Part) *pb.Part {
if p == nil {
return nil
}
return p.toPart()
}
func partFromProto(p *pb.Part) Part {
switch d := p.Data.(type) {
case *pb.Part_Text:
return Text(d.Text)
case *pb.Part_InlineData:
return Blob{
MIMEType: d.InlineData.MimeType,
Data: d.InlineData.Data,
}
case *pb.Part_FunctionCall:
return *(FunctionCall{}).fromProto(d.FunctionCall)
case *pb.Part_FunctionResponse:
panic("FunctionResponse unimplemented")
case *pb.Part_ExecutableCode:
return (ExecutableCode{}).fromProto(d.ExecutableCode)
case *pb.Part_CodeExecutionResult:
return (CodeExecutionResult{}).fromProto(d.CodeExecutionResult)
default:
panic(fmt.Errorf("unknown Part.Data type %T", p.Data))
}
}
// A Text is a piece of text, like a question or phrase.
type Text string
func (t Text) toPart() *pb.Part {
return &pb.Part{
Data: &pb.Part_Text{Text: string(t)},
}
}
func (b Blob) toPart() *pb.Part {
return &pb.Part{
Data: &pb.Part_InlineData{
InlineData: b.toProto(),
},
}
}
// ImageData is a convenience function for creating an image
// Blob for input to a model.
// The format should be the second part of the MIME type, after "image/".
// For example, for a PNG image, pass "png".
func ImageData(format string, data []byte) Blob {
return Blob{
MIMEType: "image/" + format,
Data: data,
}
}
func (f FunctionCall) toPart() *pb.Part {
return &pb.Part{
Data: &pb.Part_FunctionCall{
FunctionCall: f.toProto(),
},
}
}
func (f FunctionResponse) toPart() *pb.Part {
return &pb.Part{
Data: &pb.Part_FunctionResponse{
FunctionResponse: f.toProto(),
},
}
}
func (fd FileData) toPart() *pb.Part {
return &pb.Part{
Data: &pb.Part_FileData{
FileData: fd.toProto(),
},
}
}
func (ec ExecutableCode) toPart() *pb.Part {
return &pb.Part{
Data: &pb.Part_ExecutableCode{
ExecutableCode: ec.toProto(),
},
}
}
func (c CodeExecutionResult) toPart() *pb.Part {
return &pb.Part{
Data: &pb.Part_CodeExecutionResult{
CodeExecutionResult: c.toProto(),
},
}
}
// Ptr returns a pointer to its argument.
// It can be used to initialize pointer fields:
//
// model.Temperature = genai.Ptr[float32](0.1)
func Ptr[T any](t T) *T { return &t }
// SetCandidateCount sets the CandidateCount field.
func (c *GenerationConfig) SetCandidateCount(x int32) { c.CandidateCount = &x }
// SetMaxOutputTokens sets the MaxOutputTokens field.
func (c *GenerationConfig) SetMaxOutputTokens(x int32) { c.MaxOutputTokens = &x }
// SetTemperature sets the Temperature field.
func (c *GenerationConfig) SetTemperature(x float32) { c.Temperature = &x }
// SetTopP sets the TopP field.
func (c *GenerationConfig) SetTopP(x float32) { c.TopP = &x }
// SetTopK sets the TopK field.
func (c *GenerationConfig) SetTopK(x int32) { c.TopK = &x }
// FunctionCalls return all the FunctionCall parts in the candidate.
func (c *Candidate) FunctionCalls() []FunctionCall {
if c.Content == nil {
return nil
}
var fcs []FunctionCall
for _, p := range c.Content.Parts {
if fc, ok := p.(FunctionCall); ok {
fcs = append(fcs, fc)
}
}
return fcs
}
// NewUserContent returns a *Content with a "user" role set and one or more
// parts.
func NewUserContent(parts ...Part) *Content {
content := &Content{Role: roleUser, Parts: []Part{}}
for _, part := range parts {
content.Parts = append(content.Parts, part)
}
return content
}

View File

@@ -0,0 +1,38 @@
// Copyright 2023 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.
// This file contains debugging support functions.
package genai
import (
"fmt"
"os"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
)
// printRequests controls whether request protobufs are written to stderr.
var printRequests = false
func debugPrint(m proto.Message) {
if !printRequests {
return
}
fmt.Fprintln(os.Stderr, "--------")
fmt.Fprintf(os.Stderr, "%T\n", m)
fmt.Fprint(os.Stderr, prototext.Format(m))
fmt.Fprintln(os.Stderr, "^^^^^^^^")
}

65
vendor/github.com/google/generative-ai-go/genai/doc.go generated vendored Normal file
View File

@@ -0,0 +1,65 @@
// Copyright 2023 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.
// # [Deprecated] Google AI Go SDK for the Gemini API
//
// With Gemini 2.0, we took the chance to create a single unified SDK for all developers who want
// to use Google's GenAI models (Gemini, Veo, Imagen, etc). As part of that process, we took all of
// the feedback from this SDK and what developers like about other SDKs in the ecosystem to create
// the [Google Gen AI SDK].
//
// The Gemini API docs are fully updated to show examples of the new Google Gen AI SDK:
// [Get started].
//
// We know how disruptive an SDK change can be and don't take this change lightly, but our goal
// is to create an extremely simple and clear path for developers to build with our models so it
// felt necessary to make this change.
//
// Thank you for building with Gemini and [let us know] if you need any help!
//
// Please be advised that this repository is now considered legacy. For the latest features, performance
// improvements, and active development, we strongly recommend migrating to the official
// [Google Generative AI SDK for Go].
//
// Support Plan for this Repository:
//
// - Limited Maintenance: Development is now restricted to critical bug fixes only. No new features will be added.
// - Purpose: This limited support aims to provide stability for users while they transition to the new SDK.
// - End-of-Life Date: All support for this repository (including bug fixes) will permanently end on August 31st, 2025.
//
// We encourage all users to begin planning their migration to the [Google Generative AI SDK] to ensure
// continued access to the latest capabilities and support.
//
//
// # Getting started
//
// NOTE: This client uses the v1beta version of the API.
//
// Reading the [examples] is the best way to learn how to use this package.
//
// # Authorization
//
// You will need an API key to use the service.
// See the [setup tutorial] for details.
//
// # Errors
//
// [examples]: https://pkg.go.dev/github.com/google/generative-ai-go/genai#pkg-examples
// [setup tutorial]: https://ai.google.dev/tutorials/setup
// [Google Gen AI SDK]: https://github.com/googleapis/go-genai
// [Get started]: https://ai.google.dev/gemini-api/docs/quickstart?lang=go
// [let us know]: https://discuss.ai.google.dev/c/gemini-api/4
// [Google Generative AI SDK for Go]: https://github.com/googleapis/go-genai
// [Google Generative AI SDK]: https://github.com/googleapis/go-genai
package genai

View File

@@ -0,0 +1,126 @@
// Copyright 2023 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"
pb "cloud.google.com/go/ai/generativelanguage/apiv1beta/generativelanguagepb"
)
// EmbeddingModel creates a new instance of the named embedding model.
// Example name: "embedding-001" or "models/embedding-001".
func (c *Client) EmbeddingModel(name string) *EmbeddingModel {
return &EmbeddingModel{
c: c,
name: name,
fullName: fullModelName(name),
}
}
// EmbeddingModel is a model that computes embeddings.
// Create one with [Client.EmbeddingModel].
type EmbeddingModel struct {
c *Client
name string
fullName string
// TaskType describes how the embedding will be used.
TaskType TaskType
}
// Name returns the name of the EmbeddingModel.
func (m *EmbeddingModel) Name() string {
return m.name
}
// EmbedContent returns an embedding for the list of parts.
func (m *EmbeddingModel) EmbedContent(ctx context.Context, parts ...Part) (*EmbedContentResponse, error) {
return m.EmbedContentWithTitle(ctx, "", parts...)
}
// EmbedContentWithTitle returns an embedding for the list of parts.
// If the given title is non-empty, it is passed to the model and
// the task type is set to TaskTypeRetrievalDocument.
func (m *EmbeddingModel) EmbedContentWithTitle(ctx context.Context, title string, parts ...Part) (*EmbedContentResponse, error) {
req := newEmbedContentRequest(m.fullName, m.TaskType, title, parts)
res, err := m.c.gc.EmbedContent(ctx, req)
if err != nil {
return nil, err
}
return (EmbedContentResponse{}).fromProto(res), nil
}
func newEmbedContentRequest(model string, tt TaskType, title string, parts []Part) *pb.EmbedContentRequest {
req := &pb.EmbedContentRequest{
Model: model,
Content: NewUserContent(parts...).toProto(),
}
// A non-empty title overrides the task type.
if title != "" {
req.Title = &title
tt = TaskTypeRetrievalDocument
}
if tt != TaskTypeUnspecified {
taskType := pb.TaskType(tt)
req.TaskType = &taskType
}
debugPrint(req)
return req
}
// An EmbeddingBatch holds a collection of embedding requests.
type EmbeddingBatch struct {
tt TaskType
req *pb.BatchEmbedContentsRequest
}
// NewBatch returns a new, empty EmbeddingBatch with the same TaskType as the model.
// Make multiple calls to [EmbeddingBatch.AddContent] or [EmbeddingBatch.AddContentWithTitle].
// Then pass the EmbeddingBatch to [EmbeddingModel.BatchEmbedContents] to get
// all the embeddings in a single call to the model.
func (m *EmbeddingModel) NewBatch() *EmbeddingBatch {
return &EmbeddingBatch{
tt: m.TaskType,
req: &pb.BatchEmbedContentsRequest{
Model: m.fullName,
},
}
}
// AddContent adds a content to the batch.
func (b *EmbeddingBatch) AddContent(parts ...Part) *EmbeddingBatch {
b.AddContentWithTitle("", parts...)
return b
}
// AddContent adds a content to the batch with a title.
func (b *EmbeddingBatch) AddContentWithTitle(title string, parts ...Part) *EmbeddingBatch {
b.req.Requests = append(b.req.Requests, newEmbedContentRequest(b.req.Model, b.tt, title, parts))
return b
}
// BatchEmbedContents returns the embeddings for all the contents in the batch.
func (m *EmbeddingModel) BatchEmbedContents(ctx context.Context, b *EmbeddingBatch) (*BatchEmbedContentsResponse, error) {
res, err := m.c.gc.BatchEmbedContents(ctx, b.req)
if err != nil {
return nil, err
}
return (BatchEmbedContentsResponse{}).fromProto(res), nil
}
// Info returns information about the model.
func (m *EmbeddingModel) Info(ctx context.Context) (*ModelInfo, error) {
return m.c.modelInfo(ctx, m.fullName)
}

View File

@@ -0,0 +1,189 @@
// Copyright 2023 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
//go:generate ../devtools/generate_discovery_client.sh
import (
"context"
"io"
"os"
"strings"
gl "cloud.google.com/go/ai/generativelanguage/apiv1beta"
pb "cloud.google.com/go/ai/generativelanguage/apiv1beta/generativelanguagepb"
gld "github.com/google/generative-ai-go/genai/internal/generativelanguage/v1beta" // discovery client
"google.golang.org/api/googleapi"
"google.golang.org/api/iterator"
)
// UploadFileOptions are options for [Client.UploadFile].
type UploadFileOptions struct {
// A more readable name for the file.
DisplayName string
// The IANA standard MIME type of the file. It will be stored with the file as metadata.
// If omitted, the service will try to infer it. You may instead wish to use
// [http.DetectContentType].
// The supported MIME types are documented on [this page].
//
// [this page]: https://ai.google.dev/gemini-api/docs/document-processing?lang=go#technical-details
MIMEType string
}
// UploadFile copies the contents of the given io.Reader to file storage associated
// with the service, and returns information about the resulting file.
//
// The name is a relatively short, unique identifier for the file (rather than a typical
// filename).
// Typically it should be left empty, in which case a unique name will be generated.
// Otherwise, it can contain up to 40 characters that are lowercase
// alphanumeric or dashes (-), not starting or ending with a dash.
// To generate your own unique names, consider a cryptographic hash algorithm like SHA-1.
// The string "files/" is prepended to the name if it does not contain a '/'.
//
// Use the returned file's URI field with a [FileData] Part to use it for generation.
//
// It is an error to upload a file that already exists.
func (c *Client) UploadFile(ctx context.Context, name string, r io.Reader, opts *UploadFileOptions) (*File, error) {
if name != "" {
name = userNameToServiceName(name)
}
req := &gld.CreateFileRequest{
File: &gld.File{Name: name},
}
if opts != nil && opts.DisplayName != "" {
req.File.DisplayName = opts.DisplayName
}
call := c.ds.Media.Upload(req)
var mopts []googleapi.MediaOption
if opts != nil && opts.MIMEType != "" {
mopts = append(mopts, googleapi.ContentType(opts.MIMEType))
}
call.Media(r, mopts...)
res, err := call.Do()
if err != nil {
return nil, err
}
// Don't return the result, because it contains a file as represented by the
// discovery client and we'd have to write code to convert it to this package's
// File type.
// Instead, make a GetFile call to get the proto file, which our generated code can convert.
return c.GetFile(ctx, res.File.Name)
}
// UploadFileFromPath is a convenience method wrapping [UploadFile]. It takes
// a path to read the file from, and uses a default auto-generated ID for the
// uploaded file.
func (c *Client) UploadFileFromPath(ctx context.Context, path string, opts *UploadFileOptions) (*File, error) {
osf, err := os.Open(path)
if err != nil {
return nil, err
}
defer osf.Close()
return c.UploadFile(ctx, "", osf, opts)
}
// GetFile returns the named file.
func (c *Client) GetFile(ctx context.Context, name string) (*File, error) {
req := &pb.GetFileRequest{Name: userNameToServiceName(name)}
debugPrint(req)
pf, err := c.fc.GetFile(ctx, req)
if err != nil {
return nil, err
}
return (File{}).fromProto(pf), nil
}
// DeleteFile deletes the file with the given name.
// It is an error to delete a file that does not exist.
func (c *Client) DeleteFile(ctx context.Context, name string) error {
req := &pb.DeleteFileRequest{Name: userNameToServiceName(name)}
debugPrint(req)
return c.fc.DeleteFile(ctx, req)
}
// userNameToServiceName converts a name supplied by the user to a name required by the service.
func userNameToServiceName(name string) string {
if strings.ContainsRune(name, '/') {
return name
}
return "files/" + name
}
// ListFiles returns an iterator over the uploaded files.
func (c *Client) ListFiles(ctx context.Context) *FileIterator {
return &FileIterator{
it: c.fc.ListFiles(ctx, &pb.ListFilesRequest{}),
}
}
// A FileIterator iterates over Files.
type FileIterator struct {
it *gl.FileIterator
}
// Next returns the next result. Its second return value is iterator.Done if there are no more
// results. Once Next returns Done, all subsequent calls will return Done.
func (it *FileIterator) Next() (*File, error) {
m, err := it.it.Next()
if err != nil {
return nil, err
}
return (File{}).fromProto(m), nil
}
// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
func (it *FileIterator) PageInfo() *iterator.PageInfo {
return it.it.PageInfo()
}
// FileMetadata holds metadata about a file.
type FileMetadata struct {
// Set if the file contains video.
Video *VideoMetadata
}
func populateFileTo(p *pb.File, f *File) {
p.Metadata = nil
if f == nil || f.Metadata == nil {
return
}
if f.Metadata.Video != nil {
p.Metadata = &pb.File_VideoMetadata{
VideoMetadata: f.Metadata.Video.toProto(),
}
}
}
func populateFileFrom(f *File, p *pb.File) {
f.Metadata = nil
if p == nil || p.Metadata == nil {
return
}
if p.Metadata != nil {
switch m := p.Metadata.(type) {
case *pb.File_VideoMetadata:
f.Metadata = &FileMetadata{
Video: (VideoMetadata{}).fromProto(m.VideoMetadata),
}
default:
// ignore other types
// TODO: signal a problem
}
}
}

View File

@@ -0,0 +1,32 @@
#!/bin/sh
# Copyright 2023 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.
version=$(awk '$1 == "cloud.google.com/go/ai" {print $2}' ../go.mod)
if [[ $version = '' ]]; then
echo >&2 "could not get version of cloud.google.com/go/ai from ../go.mod"
exit 1
fi
dir=~/go/pkg/mod/cloud.google.com/go/ai@$version/generativelanguage/apiv1beta/generativelanguagepb
if [[ ! -d $dir ]]; then
echo >&2 "$dir does not exist or is not a directory"
exit 1
fi
echo "generating from $dir"
protoveneer -license license.txt config.yaml $dir

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
This directory was copied from github.com/googleapis/google-api-go-client/internal/gensupport.
It is needed for the discovery client in ../generativelanguage.
To update, first clone github.com/googleapis/google-api-go-client
into a directory we will call DIR below.
Then, from the repo root:
```
rm genai/internal/gensupport/*.go
cp $DIR/internal/gensupport/*.go genai/internal/gensupport
```
Then edit the params.go and resumable.go files to replace the reference to `internal.Version`
with the literal string from $DIR/internal/version.go, and remove the import of `internal`.

View File

@@ -0,0 +1,89 @@
// Copyright 2024 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 gensupport
import (
"bytes"
"io"
"google.golang.org/api/googleapi"
)
// MediaBuffer buffers data from an io.Reader to support uploading media in
// retryable chunks. It should be created with NewMediaBuffer.
type MediaBuffer struct {
media io.Reader
chunk []byte // The current chunk which is pending upload. The capacity is the chunk size.
err error // Any error generated when populating chunk by reading media.
// The absolute position of chunk in the underlying media.
off int64
}
// NewMediaBuffer initializes a MediaBuffer.
func NewMediaBuffer(media io.Reader, chunkSize int) *MediaBuffer {
return &MediaBuffer{media: media, chunk: make([]byte, 0, chunkSize)}
}
// Chunk returns the current buffered chunk, the offset in the underlying media
// from which the chunk is drawn, and the size of the chunk.
// Successive calls to Chunk return the same chunk between calls to Next.
func (mb *MediaBuffer) Chunk() (chunk io.Reader, off int64, size int, err error) {
// There may already be data in chunk if Next has not been called since the previous call to Chunk.
if mb.err == nil && len(mb.chunk) == 0 {
mb.err = mb.loadChunk()
}
return bytes.NewReader(mb.chunk), mb.off, len(mb.chunk), mb.err
}
// loadChunk will read from media into chunk, up to the capacity of chunk.
func (mb *MediaBuffer) loadChunk() error {
bufSize := cap(mb.chunk)
mb.chunk = mb.chunk[:bufSize]
read := 0
var err error
for err == nil && read < bufSize {
var n int
n, err = mb.media.Read(mb.chunk[read:])
read += n
}
mb.chunk = mb.chunk[:read]
return err
}
// Next advances to the next chunk, which will be returned by the next call to Chunk.
// Calls to Next without a corresponding prior call to Chunk will have no effect.
func (mb *MediaBuffer) Next() {
mb.off += int64(len(mb.chunk))
mb.chunk = mb.chunk[0:0]
}
type readerTyper struct {
io.Reader
googleapi.ContentTyper
}
// ReaderAtToReader adapts a ReaderAt to be used as a Reader.
// If ra implements googleapi.ContentTyper, then the returned reader
// will also implement googleapi.ContentTyper, delegating to ra.
func ReaderAtToReader(ra io.ReaderAt, size int64) io.Reader {
r := io.NewSectionReader(ra, 0, size)
if typer, ok := ra.(googleapi.ContentTyper); ok {
return readerTyper{r, typer}
}
return r
}

View File

@@ -0,0 +1,20 @@
// Copyright 2024 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 gensupport is an internal implementation detail used by code
// generated by the google-api-go-generator tool.
//
// This package may be modified at any time without regard for backwards
// compatibility. It should not be used directly by API users.
package gensupport

View File

@@ -0,0 +1,34 @@
// Copyright 2024 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 gensupport
import (
"errors"
"github.com/googleapis/gax-go/v2/apierror"
"google.golang.org/api/googleapi"
)
// WrapError creates an [apierror.APIError] from err, wraps it in err, and
// returns err. If err is not a [googleapi.Error] (or a
// [google.golang.org/grpc/status.Status]), it returns err without modification.
func WrapError(err error) error {
var herr *googleapi.Error
apiError, ok := apierror.ParseError(err, false)
if ok && errors.As(err, &herr) {
herr.Wrap(apiError)
}
return err
}

View File

@@ -0,0 +1,246 @@
// Copyright 2024 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 gensupport
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
// MarshalJSON returns a JSON encoding of schema containing only selected fields.
// A field is selected if any of the following is true:
// - it has a non-empty value
// - its field name is present in forceSendFields and it is not a nil pointer or nil interface
// - its field name is present in nullFields.
//
// The JSON key for each selected field is taken from the field's json: struct tag.
func MarshalJSON(schema interface{}, forceSendFields, nullFields []string) ([]byte, error) {
if len(forceSendFields) == 0 && len(nullFields) == 0 {
return json.Marshal(schema)
}
mustInclude := make(map[string]bool)
for _, f := range forceSendFields {
mustInclude[f] = true
}
useNull := make(map[string]bool)
useNullMaps := make(map[string]map[string]bool)
for _, nf := range nullFields {
parts := strings.SplitN(nf, ".", 2)
field := parts[0]
if len(parts) == 1 {
useNull[field] = true
} else {
if useNullMaps[field] == nil {
useNullMaps[field] = map[string]bool{}
}
useNullMaps[field][parts[1]] = true
}
}
dataMap, err := schemaToMap(schema, mustInclude, useNull, useNullMaps)
if err != nil {
return nil, err
}
return json.Marshal(dataMap)
}
func schemaToMap(schema interface{}, mustInclude, useNull map[string]bool, useNullMaps map[string]map[string]bool) (map[string]interface{}, error) {
m := make(map[string]interface{})
s := reflect.ValueOf(schema)
st := s.Type()
for i := 0; i < s.NumField(); i++ {
jsonTag := st.Field(i).Tag.Get("json")
if jsonTag == "" {
continue
}
tag, err := parseJSONTag(jsonTag)
if err != nil {
return nil, err
}
if tag.ignore {
continue
}
v := s.Field(i)
f := st.Field(i)
if useNull[f.Name] {
if !isEmptyValue(v) {
return nil, fmt.Errorf("field %q in NullFields has non-empty value", f.Name)
}
m[tag.apiName] = nil
continue
}
if !includeField(v, f, mustInclude) {
continue
}
// If map fields are explicitly set to null, use a map[string]interface{}.
if f.Type.Kind() == reflect.Map && useNullMaps[f.Name] != nil {
ms, ok := v.Interface().(map[string]string)
if !ok {
mi, err := initMapSlow(v, f.Name, useNullMaps)
if err != nil {
return nil, err
}
m[tag.apiName] = mi
continue
}
mi := map[string]interface{}{}
for k, v := range ms {
mi[k] = v
}
for k := range useNullMaps[f.Name] {
mi[k] = nil
}
m[tag.apiName] = mi
continue
}
// nil maps are treated as empty maps.
if f.Type.Kind() == reflect.Map && v.IsNil() {
m[tag.apiName] = map[string]string{}
continue
}
// nil slices are treated as empty slices.
if f.Type.Kind() == reflect.Slice && v.IsNil() {
m[tag.apiName] = []bool{}
continue
}
if tag.stringFormat {
m[tag.apiName] = formatAsString(v, f.Type.Kind())
} else {
m[tag.apiName] = v.Interface()
}
}
return m, nil
}
// initMapSlow uses reflection to build up a map object. This is slower than
// the default behavior so it should be used only as a fallback.
func initMapSlow(rv reflect.Value, fieldName string, useNullMaps map[string]map[string]bool) (map[string]interface{}, error) {
mi := map[string]interface{}{}
iter := rv.MapRange()
for iter.Next() {
k, ok := iter.Key().Interface().(string)
if !ok {
return nil, fmt.Errorf("field %q has keys in NullFields but is not a map[string]any", fieldName)
}
v := iter.Value().Interface()
mi[k] = v
}
for k := range useNullMaps[fieldName] {
mi[k] = nil
}
return mi, nil
}
// formatAsString returns a string representation of v, dereferencing it first if possible.
func formatAsString(v reflect.Value, kind reflect.Kind) string {
if kind == reflect.Ptr && !v.IsNil() {
v = v.Elem()
}
return fmt.Sprintf("%v", v.Interface())
}
// jsonTag represents a restricted version of the struct tag format used by encoding/json.
// It is used to describe the JSON encoding of fields in a Schema struct.
type jsonTag struct {
apiName string
stringFormat bool
ignore bool
}
// parseJSONTag parses a restricted version of the struct tag format used by encoding/json.
// The format of the tag must match that generated by the Schema.writeSchemaStruct method
// in the api generator.
func parseJSONTag(val string) (jsonTag, error) {
if val == "-" {
return jsonTag{ignore: true}, nil
}
var tag jsonTag
i := strings.Index(val, ",")
if i == -1 || val[:i] == "" {
return tag, fmt.Errorf("malformed json tag: %s", val)
}
tag = jsonTag{
apiName: val[:i],
}
switch val[i+1:] {
case "omitempty":
case "omitempty,string":
tag.stringFormat = true
default:
return tag, fmt.Errorf("malformed json tag: %s", val)
}
return tag, nil
}
// Reports whether the struct field "f" with value "v" should be included in JSON output.
func includeField(v reflect.Value, f reflect.StructField, mustInclude map[string]bool) bool {
// The regular JSON encoding of a nil pointer is "null", which means "delete this field".
// Therefore, we could enable field deletion by honoring pointer fields' presence in the mustInclude set.
// However, many fields are not pointers, so there would be no way to delete these fields.
// Rather than partially supporting field deletion, we ignore mustInclude for nil pointer fields.
// Deletion will be handled by a separate mechanism.
if f.Type.Kind() == reflect.Ptr && v.IsNil() {
return false
}
// The "any" type is represented as an interface{}. If this interface
// is nil, there is no reasonable representation to send. We ignore
// these fields, for the same reasons as given above for pointers.
if f.Type.Kind() == reflect.Interface && v.IsNil() {
return false
}
return mustInclude[f.Name] || !isEmptyValue(v)
}
// isEmptyValue reports whether v is the empty value for its type. This
// implementation is based on that of the encoding/json package, but its
// correctness does not depend on it being identical. What's important is that
// this function return false in situations where v should not be sent as part
// of a PATCH operation.
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return false
}

View File

@@ -0,0 +1,57 @@
// Copyright 2024 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 gensupport
import (
"encoding/json"
"errors"
"fmt"
"math"
)
// JSONFloat64 is a float64 that supports proper unmarshaling of special float
// values in JSON, according to
// https://developers.google.com/protocol-buffers/docs/proto3#json. Although
// that is a proto-to-JSON spec, it applies to all Google APIs.
//
// The jsonpb package
// (https://github.com/golang/protobuf/blob/master/jsonpb/jsonpb.go) has
// similar functionality, but only for direct translation from proto messages
// to JSON.
type JSONFloat64 float64
func (f *JSONFloat64) UnmarshalJSON(data []byte) error {
var ff float64
if err := json.Unmarshal(data, &ff); err == nil {
*f = JSONFloat64(ff)
return nil
}
var s string
if err := json.Unmarshal(data, &s); err == nil {
switch s {
case "NaN":
ff = math.NaN()
case "Infinity":
ff = math.Inf(1)
case "-Infinity":
ff = math.Inf(-1)
default:
return fmt.Errorf("google.golang.org/api/internal: bad float string %q", s)
}
*f = JSONFloat64(ff)
return nil
}
return errors.New("google.golang.org/api/internal: data not float or string")
}

View File

@@ -0,0 +1,320 @@
// Copyright 2024 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 gensupport
import (
"bytes"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"net/textproto"
"strings"
"sync"
"time"
gax "github.com/googleapis/gax-go/v2"
"google.golang.org/api/googleapi"
)
type typeReader struct {
io.Reader
typ string
}
// multipartReader combines the contents of multiple readers to create a multipart/related HTTP body.
// Close must be called if reads from the multipartReader are abandoned before reaching EOF.
type multipartReader struct {
pr *io.PipeReader
ctype string
mu sync.Mutex
pipeOpen bool
}
// boundary optionally specifies the MIME boundary
func newMultipartReader(parts []typeReader, boundary string) *multipartReader {
mp := &multipartReader{pipeOpen: true}
var pw *io.PipeWriter
mp.pr, pw = io.Pipe()
mpw := multipart.NewWriter(pw)
if boundary != "" {
mpw.SetBoundary(boundary)
}
mp.ctype = "multipart/related; boundary=" + mpw.Boundary()
go func() {
for _, part := range parts {
w, err := mpw.CreatePart(typeHeader(part.typ))
if err != nil {
mpw.Close()
pw.CloseWithError(fmt.Errorf("googleapi: CreatePart failed: %v", err))
return
}
_, err = io.Copy(w, part.Reader)
if err != nil {
mpw.Close()
pw.CloseWithError(fmt.Errorf("googleapi: Copy failed: %v", err))
return
}
}
mpw.Close()
pw.Close()
}()
return mp
}
func (mp *multipartReader) Read(data []byte) (n int, err error) {
return mp.pr.Read(data)
}
func (mp *multipartReader) Close() error {
mp.mu.Lock()
if !mp.pipeOpen {
mp.mu.Unlock()
return nil
}
mp.pipeOpen = false
mp.mu.Unlock()
return mp.pr.Close()
}
// CombineBodyMedia combines a json body with media content to create a multipart/related HTTP body.
// It returns a ReadCloser containing the combined body, and the overall "multipart/related" content type, with random boundary.
//
// The caller must call Close on the returned ReadCloser if reads are abandoned before reaching EOF.
func CombineBodyMedia(body io.Reader, bodyContentType string, media io.Reader, mediaContentType string) (io.ReadCloser, string) {
return combineBodyMedia(body, bodyContentType, media, mediaContentType, "")
}
// combineBodyMedia is CombineBodyMedia but with an optional mimeBoundary field.
func combineBodyMedia(body io.Reader, bodyContentType string, media io.Reader, mediaContentType, mimeBoundary string) (io.ReadCloser, string) {
mp := newMultipartReader([]typeReader{
{body, bodyContentType},
{media, mediaContentType},
}, mimeBoundary)
return mp, mp.ctype
}
func typeHeader(contentType string) textproto.MIMEHeader {
h := make(textproto.MIMEHeader)
if contentType != "" {
h.Set("Content-Type", contentType)
}
return h
}
// PrepareUpload determines whether the data in the supplied reader should be
// uploaded in a single request, or in sequential chunks.
// chunkSize is the size of the chunk that media should be split into.
//
// If chunkSize is zero, media is returned as the first value, and the other
// two return values are nil, true.
//
// Otherwise, a MediaBuffer is returned, along with a bool indicating whether the
// contents of media fit in a single chunk.
//
// After PrepareUpload has been called, media should no longer be used: the
// media content should be accessed via one of the return values.
func PrepareUpload(media io.Reader, chunkSize int) (r io.Reader, mb *MediaBuffer, singleChunk bool) {
if chunkSize == 0 { // do not chunk
return media, nil, true
}
mb = NewMediaBuffer(media, chunkSize)
_, _, _, err := mb.Chunk()
// If err is io.EOF, we can upload this in a single request. Otherwise, err is
// either nil or a non-EOF error. If it is the latter, then the next call to
// mb.Chunk will return the same error. Returning a MediaBuffer ensures that this
// error will be handled at some point.
return nil, mb, err == io.EOF
}
// MediaInfo holds information for media uploads. It is intended for use by generated
// code only.
type MediaInfo struct {
// At most one of Media and MediaBuffer will be set.
media io.Reader
buffer *MediaBuffer
singleChunk bool
mType string
size int64 // mediaSize, if known. Used only for calls to progressUpdater_.
progressUpdater googleapi.ProgressUpdater
chunkRetryDeadline time.Duration
}
// NewInfoFromMedia should be invoked from the Media method of a call. It returns a
// MediaInfo populated with chunk size and content type, and a reader or MediaBuffer
// if needed.
func NewInfoFromMedia(r io.Reader, options []googleapi.MediaOption) *MediaInfo {
mi := &MediaInfo{}
opts := googleapi.ProcessMediaOptions(options)
if !opts.ForceEmptyContentType {
mi.mType = opts.ContentType
if mi.mType == "" {
r, mi.mType = gax.DetermineContentType(r)
}
}
mi.chunkRetryDeadline = opts.ChunkRetryDeadline
mi.media, mi.buffer, mi.singleChunk = PrepareUpload(r, opts.ChunkSize)
return mi
}
// NewInfoFromResumableMedia should be invoked from the ResumableMedia method of a
// call. It returns a MediaInfo using the given reader, size and media type.
func NewInfoFromResumableMedia(r io.ReaderAt, size int64, mediaType string) *MediaInfo {
rdr := ReaderAtToReader(r, size)
mType := mediaType
if mType == "" {
rdr, mType = gax.DetermineContentType(rdr)
}
return &MediaInfo{
size: size,
mType: mType,
buffer: NewMediaBuffer(rdr, googleapi.DefaultUploadChunkSize),
media: nil,
singleChunk: false,
}
}
// SetProgressUpdater sets the progress updater for the media info.
func (mi *MediaInfo) SetProgressUpdater(pu googleapi.ProgressUpdater) {
if mi != nil {
mi.progressUpdater = pu
}
}
// UploadType determines the type of upload: a single request, or a resumable
// series of requests.
func (mi *MediaInfo) UploadType() string {
if mi.singleChunk {
return "multipart"
}
return "resumable"
}
// UploadRequest sets up an HTTP request for media upload. It adds headers
// as necessary, and returns a replacement for the body and a function for http.Request.GetBody.
func (mi *MediaInfo) UploadRequest(reqHeaders http.Header, body io.Reader) (newBody io.Reader, getBody func() (io.ReadCloser, error), cleanup func()) {
cleanup = func() {}
if mi == nil {
return body, nil, cleanup
}
var media io.Reader
if mi.media != nil {
// This only happens when the caller has turned off chunking. In that
// case, we write all of media in a single non-retryable request.
media = mi.media
} else if mi.singleChunk {
// The data fits in a single chunk, which has now been read into the MediaBuffer.
// We obtain that chunk so we can write it in a single request. The request can
// be retried because the data is stored in the MediaBuffer.
media, _, _, _ = mi.buffer.Chunk()
}
toCleanup := []io.Closer{}
if media != nil {
fb := readerFunc(body)
fm := readerFunc(media)
combined, ctype := CombineBodyMedia(body, "application/json", media, mi.mType)
toCleanup = append(toCleanup, combined)
if fb != nil && fm != nil {
getBody = func() (io.ReadCloser, error) {
rb := io.NopCloser(fb())
rm := io.NopCloser(fm())
var mimeBoundary string
if _, params, err := mime.ParseMediaType(ctype); err == nil {
mimeBoundary = params["boundary"]
}
r, _ := combineBodyMedia(rb, "application/json", rm, mi.mType, mimeBoundary)
toCleanup = append(toCleanup, r)
return r, nil
}
}
reqHeaders.Set("Content-Type", ctype)
body = combined
}
if mi.buffer != nil && mi.mType != "" && !mi.singleChunk {
// This happens when initiating a resumable upload session.
// The initial request contains a JSON body rather than media.
// It can be retried with a getBody function that re-creates the request body.
fb := readerFunc(body)
if fb != nil {
getBody = func() (io.ReadCloser, error) {
rb := io.NopCloser(fb())
toCleanup = append(toCleanup, rb)
return rb, nil
}
}
reqHeaders.Set("X-Upload-Content-Type", mi.mType)
}
// Ensure that any bodies created in getBody are cleaned up.
cleanup = func() {
for _, closer := range toCleanup {
_ = closer.Close()
}
}
return body, getBody, cleanup
}
// readerFunc returns a function that always returns an io.Reader that has the same
// contents as r, provided that can be done without consuming r. Otherwise, it
// returns nil.
// See http.NewRequest (in net/http/request.go).
func readerFunc(r io.Reader) func() io.Reader {
switch r := r.(type) {
case *bytes.Buffer:
buf := r.Bytes()
return func() io.Reader { return bytes.NewReader(buf) }
case *bytes.Reader:
snapshot := *r
return func() io.Reader { r := snapshot; return &r }
case *strings.Reader:
snapshot := *r
return func() io.Reader { r := snapshot; return &r }
default:
return nil
}
}
// ResumableUpload returns an appropriately configured ResumableUpload value if the
// upload is resumable, or nil otherwise.
func (mi *MediaInfo) ResumableUpload(locURI string) *ResumableUpload {
if mi == nil || mi.singleChunk {
return nil
}
return &ResumableUpload{
URI: locURI,
Media: mi.buffer,
MediaType: mi.mType,
Callback: func(curr int64) {
if mi.progressUpdater != nil {
mi.progressUpdater(curr, mi.size)
}
},
ChunkRetryDeadline: mi.chunkRetryDeadline,
}
}
// SetGetBody sets the GetBody field of req to f. This was once needed
// to gracefully support Go 1.7 and earlier which didn't have that
// field.
//
// Deprecated: the code generator no longer uses this as of
// 2019-02-19. Nothing else should be calling this anyway, but we
// won't delete this immediately; it will be deleted in as early as 6
// months.
func SetGetBody(req *http.Request, f func() (io.ReadCloser, error)) {
req.GetBody = f
}

View File

@@ -0,0 +1,87 @@
// Copyright 2024 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 gensupport
import (
"net/http"
"net/url"
"google.golang.org/api/googleapi"
)
// URLParams is a simplified replacement for url.Values
// that safely builds up URL parameters for encoding.
type URLParams map[string][]string
// Get returns the first value for the given key, or "".
func (u URLParams) Get(key string) string {
vs := u[key]
if len(vs) == 0 {
return ""
}
return vs[0]
}
// Set sets the key to value.
// It replaces any existing values.
func (u URLParams) Set(key, value string) {
u[key] = []string{value}
}
// SetMulti sets the key to an array of values.
// It replaces any existing values.
// Note that values must not be modified after calling SetMulti
// so the caller is responsible for making a copy if necessary.
func (u URLParams) SetMulti(key string, values []string) {
u[key] = values
}
// Encode encodes the values into “URL encoded” form
// ("bar=baz&foo=quux") sorted by key.
func (u URLParams) Encode() string {
return url.Values(u).Encode()
}
// SetOptions sets the URL params and any additional `CallOption` or
// `MultiCallOption` passed in.
func SetOptions(u URLParams, opts ...googleapi.CallOption) {
for _, o := range opts {
m, ok := o.(googleapi.MultiCallOption)
if ok {
u.SetMulti(m.GetMulti())
continue
}
u.Set(o.Get())
}
}
// SetHeaders sets common headers for all requests. The keyvals header pairs
// should have a corresponding value for every key provided. If there is an odd
// number of keyvals this method will panic.
func SetHeaders(userAgent, contentType string, userHeaders http.Header, keyvals ...string) http.Header {
reqHeaders := make(http.Header)
reqHeaders.Set("x-goog-api-client", "gl-go/"+GoVersion()+" gdcl/"+"0.179.0")
for i := 0; i < len(keyvals); i = i + 2 {
reqHeaders.Set(keyvals[i], keyvals[i+1])
}
reqHeaders.Set("User-Agent", userAgent)
if contentType != "" {
reqHeaders.Set("Content-Type", contentType)
}
for k, v := range userHeaders {
reqHeaders[k] = v
}
return reqHeaders
}

View File

@@ -0,0 +1,277 @@
// Copyright 2024 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 gensupport
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"github.com/google/uuid"
)
// ResumableUpload is used by the generated APIs to provide resumable uploads.
// It is not used by developers directly.
type ResumableUpload struct {
Client *http.Client
// URI is the resumable resource destination provided by the server after specifying "&uploadType=resumable".
URI string
UserAgent string // User-Agent for header of the request
// Media is the object being uploaded.
Media *MediaBuffer
// MediaType defines the media type, e.g. "image/jpeg".
MediaType string
mu sync.Mutex // guards progress
progress int64 // number of bytes uploaded so far
// Callback is an optional function that will be periodically called with the cumulative number of bytes uploaded.
Callback func(int64)
// Retry optionally configures retries for requests made against the upload.
Retry *RetryConfig
// ChunkRetryDeadline configures the per-chunk deadline after which no further
// retries should happen.
ChunkRetryDeadline time.Duration
// Track current request invocation ID and attempt count for retry metrics
// and idempotency headers.
invocationID string
attempts int
}
// Progress returns the number of bytes uploaded at this point.
func (rx *ResumableUpload) Progress() int64 {
rx.mu.Lock()
defer rx.mu.Unlock()
return rx.progress
}
// doUploadRequest performs a single HTTP request to upload data.
// off specifies the offset in rx.Media from which data is drawn.
// size is the number of bytes in data.
// final specifies whether data is the final chunk to be uploaded.
func (rx *ResumableUpload) doUploadRequest(ctx context.Context, data io.Reader, off, size int64, final bool) (*http.Response, error) {
req, err := http.NewRequest("POST", rx.URI, data)
if err != nil {
return nil, err
}
req.ContentLength = size
var contentRange string
if final {
if size == 0 {
contentRange = fmt.Sprintf("bytes */%v", off)
} else {
contentRange = fmt.Sprintf("bytes %v-%v/%v", off, off+size-1, off+size)
}
} else {
contentRange = fmt.Sprintf("bytes %v-%v/*", off, off+size-1)
}
req.Header.Set("Content-Range", contentRange)
req.Header.Set("Content-Type", rx.MediaType)
req.Header.Set("User-Agent", rx.UserAgent)
// TODO(b/274504690): Consider dropping gccl-invocation-id key since it
// duplicates the X-Goog-Gcs-Idempotency-Token header (added in v0.115.0).
baseXGoogHeader := "gl-go/" + GoVersion() + " gdcl/" + "0.179.0"
invocationHeader := fmt.Sprintf("gccl-invocation-id/%s gccl-attempt-count/%d", rx.invocationID, rx.attempts)
req.Header.Set("X-Goog-Api-Client", strings.Join([]string{baseXGoogHeader, invocationHeader}, " "))
// Set idempotency token header which is used by GCS uploads.
req.Header.Set("X-Goog-Gcs-Idempotency-Token", rx.invocationID)
// Google's upload endpoint uses status code 308 for a
// different purpose than the "308 Permanent Redirect"
// since-standardized in RFC 7238. Because of the conflict in
// semantics, Google added this new request header which
// causes it to not use "308" and instead reply with 200 OK
// and sets the upload-specific "X-HTTP-Status-Code-Override:
// 308" response header.
req.Header.Set("X-GUploader-No-308", "yes")
return SendRequest(ctx, rx.Client, req)
}
func statusResumeIncomplete(resp *http.Response) bool {
// This is how the server signals "status resume incomplete"
// when X-GUploader-No-308 is set to "yes":
return resp != nil && resp.Header.Get("X-Http-Status-Code-Override") == "308"
}
// reportProgress calls a user-supplied callback to report upload progress.
// If old==updated, the callback is not called.
func (rx *ResumableUpload) reportProgress(old, updated int64) {
if updated-old == 0 {
return
}
rx.mu.Lock()
rx.progress = updated
rx.mu.Unlock()
if rx.Callback != nil {
rx.Callback(updated)
}
}
// transferChunk performs a single HTTP request to upload a single chunk from rx.Media.
func (rx *ResumableUpload) transferChunk(ctx context.Context) (*http.Response, error) {
chunk, off, size, err := rx.Media.Chunk()
done := err == io.EOF
if !done && err != nil {
return nil, err
}
res, err := rx.doUploadRequest(ctx, chunk, off, int64(size), done)
if err != nil {
return res, err
}
// We sent "X-GUploader-No-308: yes" (see comment elsewhere in
// this file), so we don't expect to get a 308.
if res.StatusCode == 308 {
return nil, errors.New("unexpected 308 response status code")
}
if res.StatusCode == http.StatusOK {
rx.reportProgress(off, off+int64(size))
}
if statusResumeIncomplete(res) {
rx.Media.Next()
}
return res, nil
}
// Upload starts the process of a resumable upload with a cancellable context.
// It retries using the provided back off strategy until cancelled or the
// strategy indicates to stop retrying.
// It is called from the auto-generated API code and is not visible to the user.
// Before sending an HTTP request, Upload calls any registered hook functions,
// and calls the returned functions after the request returns (see send.go).
// rx is private to the auto-generated API code.
// Exactly one of resp or err will be nil. If resp is non-nil, the caller must call resp.Body.Close.
func (rx *ResumableUpload) Upload(ctx context.Context) (resp *http.Response, err error) {
// There are a couple of cases where it's possible for err and resp to both
// be non-nil. However, we expose a simpler contract to our callers: exactly
// one of resp and err will be non-nil. This means that any response body
// must be closed here before returning a non-nil error.
prepareReturn := func(resp *http.Response, err error) (*http.Response, error) {
if err != nil {
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
return nil, err
}
// This case is very unlikely but possible only if rx.ChunkRetryDeadline is
// set to a very small value, in which case no requests will be sent before
// the deadline. Return an error to avoid causing a panic.
if resp == nil {
return nil, fmt.Errorf("upload request to %v not sent, choose larger value for ChunkRetryDeadline", rx.URI)
}
return resp, nil
}
// Configure retryable error criteria.
errorFunc := rx.Retry.errorFunc()
// Configure per-chunk retry deadline.
var retryDeadline time.Duration
if rx.ChunkRetryDeadline != 0 {
retryDeadline = rx.ChunkRetryDeadline
} else {
retryDeadline = defaultRetryDeadline
}
// Send all chunks.
for {
var pause time.Duration
// Each chunk gets its own initialized-at-zero backoff and invocation ID.
bo := rx.Retry.backoff()
quitAfterTimer := time.NewTimer(retryDeadline)
rx.attempts = 1
rx.invocationID = uuid.New().String()
// Retry loop for a single chunk.
for {
pauseTimer := time.NewTimer(pause)
select {
case <-ctx.Done():
quitAfterTimer.Stop()
pauseTimer.Stop()
if err == nil {
err = ctx.Err()
}
return prepareReturn(resp, err)
case <-pauseTimer.C:
case <-quitAfterTimer.C:
pauseTimer.Stop()
return prepareReturn(resp, err)
}
pauseTimer.Stop()
// Check for context cancellation or timeout once more. If more than one
// case in the select statement above was satisfied at the same time, Go
// will choose one arbitrarily.
// That can cause an operation to go through even if the context was
// canceled before or the timeout was reached.
select {
case <-ctx.Done():
quitAfterTimer.Stop()
if err == nil {
err = ctx.Err()
}
return prepareReturn(resp, err)
case <-quitAfterTimer.C:
return prepareReturn(resp, err)
default:
}
resp, err = rx.transferChunk(ctx)
var status int
if resp != nil {
status = resp.StatusCode
}
// Check if we should retry the request.
if !errorFunc(status, err) {
quitAfterTimer.Stop()
break
}
rx.attempts++
pause = bo.Pause()
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}
// If the chunk was uploaded successfully, but there's still
// more to go, upload the next chunk without any delay.
if statusResumeIncomplete(resp) {
resp.Body.Close()
continue
}
return prepareReturn(resp, err)
}
}

View File

@@ -0,0 +1,131 @@
// Copyright 2024 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 gensupport
import (
"errors"
"io"
"net"
"strings"
"time"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/googleapi"
)
// Backoff is an interface around gax.Backoff's Pause method, allowing tests to provide their
// own implementation.
type Backoff interface {
Pause() time.Duration
}
// These are declared as global variables so that tests can overwrite them.
var (
// Default per-chunk deadline for resumable uploads.
defaultRetryDeadline = 32 * time.Second
// Default backoff timer.
backoff = func() Backoff {
return &gax.Backoff{Initial: 100 * time.Millisecond}
}
// syscallRetryable is a platform-specific hook, specified in retryable_linux.go
syscallRetryable func(error) bool = func(err error) bool { return false }
)
const (
// statusTooManyRequests is returned by the storage API if the
// per-project limits have been temporarily exceeded. The request
// should be retried.
// https://cloud.google.com/storage/docs/json_api/v1/status-codes#standardcodes
statusTooManyRequests = 429
// statusRequestTimeout is returned by the storage API if the
// upload connection was broken. The request should be retried.
statusRequestTimeout = 408
)
// shouldRetry indicates whether an error is retryable for the purposes of this
// package, unless a ShouldRetry func is specified by the RetryConfig instead.
// It follows guidance from
// https://cloud.google.com/storage/docs/exponential-backoff .
func shouldRetry(status int, err error) bool {
if 500 <= status && status <= 599 {
return true
}
if status == statusTooManyRequests || status == statusRequestTimeout {
return true
}
if err == io.ErrUnexpectedEOF {
return true
}
// Transient network errors should be retried.
if syscallRetryable(err) {
return true
}
if err, ok := err.(interface{ Temporary() bool }); ok {
if err.Temporary() {
return true
}
}
var opErr *net.OpError
if errors.As(err, &opErr) {
if strings.Contains(opErr.Error(), "use of closed network connection") {
// TODO: check against net.ErrClosed (go 1.16+) instead of string
return true
}
}
// If Go 1.13 error unwrapping is available, use this to examine wrapped
// errors.
if err, ok := err.(interface{ Unwrap() error }); ok {
return shouldRetry(status, err.Unwrap())
}
return false
}
// RetryConfig allows configuration of backoff timing and retryable errors.
type RetryConfig struct {
Backoff *gax.Backoff
ShouldRetry func(err error) bool
}
// Get a new backoff object based on the configured values.
func (r *RetryConfig) backoff() Backoff {
if r == nil || r.Backoff == nil {
return backoff()
}
return &gax.Backoff{
Initial: r.Backoff.Initial,
Max: r.Backoff.Max,
Multiplier: r.Backoff.Multiplier,
}
}
// This is kind of hacky; it is necessary because ShouldRetry expects to
// handle HTTP errors via googleapi.Error, but the error has not yet been
// wrapped with a googleapi.Error at this layer, and the ErrorFunc type
// in the manual layer does not pass in a status explicitly as it does
// here. So, we must wrap error status codes in a googleapi.Error so that
// ShouldRetry can parse this correctly.
func (r *RetryConfig) errorFunc() func(status int, err error) bool {
if r == nil || r.ShouldRetry == nil {
return shouldRetry
}
return func(status int, err error) bool {
if status >= 400 {
return r.ShouldRetry(&googleapi.Error{Code: status})
}
return r.ShouldRetry(err)
}
}

View File

@@ -0,0 +1,26 @@
// Copyright 2024 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 linux
// +build linux
package gensupport
import "syscall"
func init() {
// Initialize syscallRetryable to return true on transient socket-level
// errors. These errors are specific to Linux.
syscallRetryable = func(err error) bool { return err == syscall.ECONNRESET || err == syscall.ECONNREFUSED }
}

View File

@@ -0,0 +1,216 @@
// Copyright 2024 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 gensupport
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"github.com/googleapis/gax-go/v2"
"github.com/googleapis/gax-go/v2/callctx"
)
// Use this error type to return an error which allows introspection of both
// the context error and the error from the service.
type wrappedCallErr struct {
ctxErr error
wrappedErr error
}
func (e wrappedCallErr) Error() string {
return fmt.Sprintf("retry failed with %v; last error: %v", e.ctxErr, e.wrappedErr)
}
func (e wrappedCallErr) Unwrap() error {
return e.wrappedErr
}
// Is allows errors.Is to match the error from the call as well as context
// sentinel errors.
func (e wrappedCallErr) Is(target error) bool {
return errors.Is(e.ctxErr, target) || errors.Is(e.wrappedErr, target)
}
// SendRequest sends a single HTTP request using the given client.
// If ctx is non-nil, it calls all hooks, then sends the request with
// req.WithContext, then calls any functions returned by the hooks in
// reverse order.
func SendRequest(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
// Add headers set in context metadata.
if ctx != nil {
headers := callctx.HeadersFromContext(ctx)
for k, vals := range headers {
for _, v := range vals {
req.Header.Add(k, v)
}
}
}
// Disallow Accept-Encoding because it interferes with the automatic gzip handling
// done by the default http.Transport. See https://github.com/google/google-api-go-client/issues/219.
if _, ok := req.Header["Accept-Encoding"]; ok {
return nil, errors.New("google api: custom Accept-Encoding headers not allowed")
}
if ctx == nil {
return client.Do(req)
}
return send(ctx, client, req)
}
func send(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
if client == nil {
client = http.DefaultClient
}
resp, err := client.Do(req.WithContext(ctx))
// If we got an error, and the context has been canceled,
// the context's error is probably more useful.
if err != nil {
select {
case <-ctx.Done():
err = ctx.Err()
default:
}
}
return resp, err
}
// SendRequestWithRetry sends a single HTTP request using the given client,
// with retries if a retryable error is returned.
// If ctx is non-nil, it calls all hooks, then sends the request with
// req.WithContext, then calls any functions returned by the hooks in
// reverse order.
func SendRequestWithRetry(ctx context.Context, client *http.Client, req *http.Request, retry *RetryConfig) (*http.Response, error) {
// Add headers set in context metadata.
if ctx != nil {
headers := callctx.HeadersFromContext(ctx)
for k, vals := range headers {
for _, v := range vals {
req.Header.Add(k, v)
}
}
}
// Disallow Accept-Encoding because it interferes with the automatic gzip handling
// done by the default http.Transport. See https://github.com/google/google-api-go-client/issues/219.
if _, ok := req.Header["Accept-Encoding"]; ok {
return nil, errors.New("google api: custom Accept-Encoding headers not allowed")
}
if ctx == nil {
return client.Do(req)
}
return sendAndRetry(ctx, client, req, retry)
}
func sendAndRetry(ctx context.Context, client *http.Client, req *http.Request, retry *RetryConfig) (*http.Response, error) {
if client == nil {
client = http.DefaultClient
}
var resp *http.Response
var err error
attempts := 1
invocationID := uuid.New().String()
baseXGoogHeader := req.Header.Get("X-Goog-Api-Client")
// Loop to retry the request, up to the context deadline.
var pause time.Duration
var bo Backoff
if retry != nil && retry.Backoff != nil {
bo = &gax.Backoff{
Initial: retry.Backoff.Initial,
Max: retry.Backoff.Max,
Multiplier: retry.Backoff.Multiplier,
}
} else {
bo = backoff()
}
errorFunc := retry.errorFunc()
for {
t := time.NewTimer(pause)
select {
case <-ctx.Done():
t.Stop()
// If we got an error and the context has been canceled, return an error acknowledging
// both the context cancelation and the service error.
if err != nil {
return resp, wrappedCallErr{ctx.Err(), err}
}
return resp, ctx.Err()
case <-t.C:
}
if ctx.Err() != nil {
// Check for context cancellation once more. If more than one case in a
// select is satisfied at the same time, Go will choose one arbitrarily.
// That can cause an operation to go through even if the context was
// canceled before.
if err != nil {
return resp, wrappedCallErr{ctx.Err(), err}
}
return resp, ctx.Err()
}
// Set retry metrics and idempotency headers for GCS.
// TODO(b/274504690): Consider dropping gccl-invocation-id key since it
// duplicates the X-Goog-Gcs-Idempotency-Token header (added in v0.115.0).
invocationHeader := fmt.Sprintf("gccl-invocation-id/%s gccl-attempt-count/%d", invocationID, attempts)
xGoogHeader := strings.Join([]string{invocationHeader, baseXGoogHeader}, " ")
req.Header.Set("X-Goog-Api-Client", xGoogHeader)
req.Header.Set("X-Goog-Gcs-Idempotency-Token", invocationID)
resp, err = client.Do(req.WithContext(ctx))
var status int
if resp != nil {
status = resp.StatusCode
}
// Check if we can retry the request. A retry can only be done if the error
// is retryable and the request body can be re-created using GetBody (this
// will not be possible if the body was unbuffered).
if req.GetBody == nil || !errorFunc(status, err) {
break
}
attempts++
var errBody error
req.Body, errBody = req.GetBody()
if errBody != nil {
break
}
pause = bo.Pause()
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}
return resp, err
}
// DecodeResponse decodes the body of res into target. If there is no body,
// target is unchanged.
func DecodeResponse(target interface{}, res *http.Response) error {
if res.StatusCode == http.StatusNoContent {
return nil
}
return json.NewDecoder(res.Body).Decode(target)
}

View File

@@ -0,0 +1,63 @@
// Copyright 2024 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 gensupport
import (
"runtime"
"strings"
"unicode"
)
// GoVersion returns the Go runtime version. The returned string
// has no whitespace.
func GoVersion() string {
return goVersion
}
var goVersion = goVer(runtime.Version())
const develPrefix = "devel +"
func goVer(s string) string {
if strings.HasPrefix(s, develPrefix) {
s = s[len(develPrefix):]
if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
s = s[:p]
}
return s
}
if strings.HasPrefix(s, "go1") {
s = s[2:]
var prerelease string
if p := strings.IndexFunc(s, notSemverRune); p >= 0 {
s, prerelease = s[:p], s[p:]
}
if strings.HasSuffix(s, ".") {
s += "0"
} else if strings.Count(s, ".") < 2 {
s += ".0"
}
if prerelease != "" {
s += "-" + prerelease
}
return s
}
return ""
}
func notSemverRune(r rune) bool {
return !strings.ContainsRune("0123456789.", r)
}

View File

@@ -0,0 +1,18 @@
// Copyright 2022 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 internal
// Version is the current tagged release of the library.
const Version = "0.19.0"

View File

@@ -0,0 +1,14 @@
// Copyright 2024 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.

View File

@@ -0,0 +1,50 @@
// Copyright 2023 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"
gl "cloud.google.com/go/ai/generativelanguage/apiv1beta"
pb "cloud.google.com/go/ai/generativelanguage/apiv1beta/generativelanguagepb"
"google.golang.org/api/iterator"
)
func (c *Client) ListModels(ctx context.Context) *ModelInfoIterator {
return &ModelInfoIterator{
it: c.mc.ListModels(ctx, &pb.ListModelsRequest{}),
}
}
// A ModelInfoIterator iterates over Models.
type ModelInfoIterator struct {
it *gl.ModelIterator
}
// Next returns the next result. Its second return value is iterator.Done if there are no more
// results. Once Next returns Done, all subsequent calls will return Done.
func (it *ModelInfoIterator) Next() (*ModelInfo, error) {
m, err := it.it.Next()
if err != nil {
return nil, err
}
return (ModelInfo{}).fromProto(m), nil
}
// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
func (it *ModelInfoIterator) PageInfo() *iterator.PageInfo {
return it.it.PageInfo()
}

View File

@@ -0,0 +1,44 @@
// Copyright 2024 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 (
"google.golang.org/api/option"
"google.golang.org/api/option/internaloption"
)
// WithClientInfo sets request information identifying the
// product that is calling this client.
func WithClientInfo(key, value string) option.ClientOption {
return &clientInfo{key: key, value: value}
}
type clientInfo struct {
internaloption.EmbeddableAdapter
key, value string
}
// optionOfType returns the first value of opts that has type T,
// along with true. If there is no option of that type, it returns
// the zero value for T and false.
func optionOfType[T option.ClientOption](opts []option.ClientOption) (T, bool) {
for _, opt := range opts {
if opt, ok := opt.(T); ok {
return opt, true
}
}
var z T
return z, false
}