Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions go/ai/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ func GenerateWithRequest(ctx context.Context, r api.Registry, opts *GenerateActi
// Native constrained output is enabled only when the user has
// requested it, the model supports it, and there's a JSON schema.
outputCfg.Constrained = opts.Output.JsonSchema != nil &&
opts.Output.Constrained && outputCfg.Constrained && m.(*model).supportsConstrained(len(toolDefs) > 0)
opts.Output.Constrained && outputCfg.Constrained && m != nil && m.(*model).supportsConstrained(len(toolDefs) > 0)

// Add schema instructions to prompt when not using native constraints.
// This is a no-op for unstructured output requests.
Expand Down Expand Up @@ -313,12 +313,14 @@ func GenerateWithRequest(ctx context.Context, r api.Registry, opts *GenerateActi
Output: &outputCfg,
}

fn := m.Generate
var fn ModelFunc
if bm != nil {
if cb != nil {
logger.FromContext(ctx).Warn("background model does not support streaming", "model", bm.Name())
}
fn = backgroundModelToModelFn(bm.Start)
} else {
fn = m.Generate
}
fn = core.ChainMiddleware(mw...)(fn)

Expand Down
151 changes: 151 additions & 0 deletions go/plugins/googlegenai/actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0

package googlegenai

import (
"context"
"fmt"

"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/core"
"github.com/firebase/genkit/go/core/api"
"google.golang.org/genai"
)

// ListActions lists all the actions supported by the Google AI plugin.
func (ga *GoogleAI) ListActions(ctx context.Context) []api.ActionDesc {
return listActions(ctx, ga.gclient, googleAIProvider)
}

// ListActions lists all the actions supported by the Vertex AI plugin.
func (v *VertexAI) ListActions(ctx context.Context) []api.ActionDesc {
return listActions(ctx, v.gclient, vertexAIProvider)
}

// listActions is the shared implementation for listing actions.
func listActions(ctx context.Context, client *genai.Client, provider string) []api.ActionDesc {
models, err := listGenaiModels(ctx, client)
if err != nil {
return nil
}

actions := []api.ActionDesc{}

// Gemini models
for _, name := range models.gemini {
opts := GetModelOptions(name, provider)
model := newModel(client, name, opts)
if actionDef, ok := model.(api.Action); ok {
actions = append(actions, actionDef.Desc())
}
}

// Imagen models
for _, name := range models.imagen {
opts := GetModelOptions(name, provider)
model := newModel(client, name, opts)
if actionDef, ok := model.(api.Action); ok {
actions = append(actions, actionDef.Desc())
}
}

// Veo models (background models)
for _, name := range models.veo {
opts := GetModelOptions(name, provider)
veoModel := newVeoModel(client, name, opts)
if actionDef, ok := veoModel.(api.Action); ok {
actions = append(actions, actionDef.Desc())
}
}

// Embedders
for _, name := range models.embedders {
opts := GetEmbedderOptions(name, provider)
embedder := newEmbedder(client, name, &opts)
if actionDef, ok := embedder.(api.Action); ok {
actions = append(actions, actionDef.Desc())
}
}

return actions
}

// ResolveAction resolves an action with the given name.
func (ga *GoogleAI) ResolveAction(atype api.ActionType, name string) api.Action {
return resolveAction(ga.gclient, googleAIProvider, atype, name)
}

// ResolveAction resolves an action with the given name.
func (v *VertexAI) ResolveAction(atype api.ActionType, name string) api.Action {
return resolveAction(v.gclient, vertexAIProvider, atype, name)
}

// resolveAction is the shared implementation for resolving actions.
func resolveAction(client *genai.Client, provider string, atype api.ActionType, name string) api.Action {
mt := ClassifyModel(name)

switch atype {
case api.ActionTypeEmbedder:
opts := GetEmbedderOptions(name, provider)
return newEmbedder(client, name, &opts).(api.Action)

case api.ActionTypeModel:
// Veo models should not be resolved as regular models
if mt == ModelTypeVeo {
return nil
}
opts := GetModelOptions(name, provider)
opts.ConfigSchema = configToMap(mt.DefaultConfig())
return newModel(client, name, opts).(api.Action)

case api.ActionTypeBackgroundModel:
if mt != ModelTypeVeo {
return nil
}
return createVeoBackgroundAction(client, name, provider)

case api.ActionTypeCheckOperation:
if mt != ModelTypeVeo {
return nil
}
return createVeoCheckAction(client, name, provider)
}

return nil
}

// createVeoBackgroundAction creates a background model action for Veo.
func createVeoBackgroundAction(client *genai.Client, name, provider string) api.Action {
opts := GetModelOptions(name, provider)
veoModel := newVeoModel(client, name, opts)
actionName := fmt.Sprintf("%s/%s", provider, name)

return core.NewAction(actionName, api.ActionTypeBackgroundModel, nil, nil,
func(ctx context.Context, input *ai.ModelRequest) (*core.Operation[*ai.ModelResponse], error) {
op, err := veoModel.Start(ctx, input)
if err != nil {
return nil, err
}
op.Action = api.KeyFromName(api.ActionTypeBackgroundModel, actionName)
return op, nil
})
}

// createVeoCheckAction creates a check operation action for Veo.
func createVeoCheckAction(client *genai.Client, name, provider string) api.Action {
opts := GetModelOptions(name, provider)
veoModel := newVeoModel(client, name, opts)
actionName := fmt.Sprintf("%s/%s", provider, name)

return core.NewAction(actionName, api.ActionTypeCheckOperation,
map[string]any{"description": fmt.Sprintf("Check status of %s operation", name)}, nil,
func(ctx context.Context, op *core.Operation[*ai.ModelResponse]) (*core.Operation[*ai.ModelResponse], error) {
updatedOp, err := veoModel.Check(ctx, op)
if err != nil {
return nil, err
}
updatedOp.Action = api.KeyFromName(api.ActionTypeBackgroundModel, actionName)
return updatedOp, nil
})
}
121 changes: 121 additions & 0 deletions go/plugins/googlegenai/code_execution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0

package googlegenai

import (
"github.com/firebase/genkit/go/ai"
)

// CodeExecutionResult represents the result of a code execution.
type CodeExecutionResult struct {
Outcome string `json:"outcome"`
Output string `json:"output"`
}

// ExecutableCode represents executable code.
type ExecutableCode struct {
Language string `json:"language"`
Code string `json:"code"`
}

// newCodeExecutionResultPart returns a Part containing the result of code execution.
// This is internal and used by translateCandidate.
func newCodeExecutionResultPart(outcome string, output string) *ai.Part {
return ai.NewCustomPart(map[string]any{
"codeExecutionResult": map[string]any{
"outcome": outcome,
"output": output,
},
})
}

// newExecutableCodePart returns a Part containing executable code.
// This is internal and used by translateCandidate.
func newExecutableCodePart(language string, code string) *ai.Part {
return ai.NewCustomPart(map[string]any{
"executableCode": map[string]any{
"language": language,
"code": code,
},
})
}

// ToCodeExecutionResult tries to convert an ai.Part to a CodeExecutionResult.
// Returns nil if the part doesn't contain code execution results.
func ToCodeExecutionResult(part *ai.Part) *CodeExecutionResult {
if !part.IsCustom() {
return nil
}

codeExec, ok := part.Custom["codeExecutionResult"]
if !ok {
return nil
}

result, ok := codeExec.(map[string]any)
if !ok {
return nil
}

outcome, _ := result["outcome"].(string)
output, _ := result["output"].(string)

return &CodeExecutionResult{
Outcome: outcome,
Output: output,
}
}

// ToExecutableCode tries to convert an ai.Part to an ExecutableCode.
// Returns nil if the part doesn't contain executable code.
func ToExecutableCode(part *ai.Part) *ExecutableCode {
if !part.IsCustom() {
return nil
}

execCode, ok := part.Custom["executableCode"]
if !ok {
return nil
}

code, ok := execCode.(map[string]any)
if !ok {
return nil
}

language, _ := code["language"].(string)
codeStr, _ := code["code"].(string)

return &ExecutableCode{
Language: language,
Code: codeStr,
}
}

// HasCodeExecution checks if a message contains code execution results or executable code.
func HasCodeExecution(msg *ai.Message) bool {
return GetCodeExecutionResult(msg) != nil || GetExecutableCode(msg) != nil
}

// GetExecutableCode returns the first executable code from a message.
// Returns nil if the message doesn't contain executable code.
func GetExecutableCode(msg *ai.Message) *ExecutableCode {
for _, part := range msg.Content {
if code := ToExecutableCode(part); code != nil {
return code
}
}
return nil
}

// GetCodeExecutionResult returns the first code execution result from a message.
// Returns nil if the message doesn't contain a code execution result.
func GetCodeExecutionResult(msg *ai.Message) *CodeExecutionResult {
for _, part := range msg.Content {
if result := ToCodeExecutionResult(part); result != nil {
return result
}
}
return nil
}
54 changes: 54 additions & 0 deletions go/plugins/googlegenai/embedder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0

package googlegenai

import (
"context"

"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/core"
"github.com/firebase/genkit/go/core/api"
"google.golang.org/genai"
)

// newEmbedder creates an embedder without registering it.
func newEmbedder(client *genai.Client, name string, embedOpts *ai.EmbedderOptions) ai.Embedder {
provider := googleAIProvider
if client.ClientConfig().Backend == genai.BackendVertexAI {
provider = vertexAIProvider
}

if embedOpts.ConfigSchema == nil {
embedOpts.ConfigSchema = core.InferSchemaMap(genai.EmbedContentConfig{})
}

return ai.NewEmbedder(api.NewName(provider, name), embedOpts, func(ctx context.Context, req *ai.EmbedRequest) (*ai.EmbedResponse, error) {
var content []*genai.Content
var embedConfig *genai.EmbedContentConfig

if config, ok := req.Options.(*genai.EmbedContentConfig); ok {
embedConfig = config
}

for _, doc := range req.Input {
parts, err := toGeminiParts(doc.Content)
if err != nil {
return nil, err
}
content = append(content, &genai.Content{
Parts: parts,
})
}

r, err := genai.Models.EmbedContent(*client.Models, ctx, name, content, embedConfig)
if err != nil {
return nil, err
}
var res ai.EmbedResponse
for _, emb := range r.Embeddings {
res.Embeddings = append(res.Embeddings, &ai.Embedding{Embedding: emb.Values})
}
return &res, nil
})
}
Loading
Loading