Skip to content

feat(go/ai): add typed-config NewTyped* constructors - #5849

Open
apascal07 wants to merge 10 commits into
ap/go-action-constructorsfrom
ap/go-with-config-constructors
Open

feat(go/ai): add typed-config NewTyped* constructors#5849
apascal07 wants to merge 10 commits into
ap/go-action-constructorsfrom
ap/go-with-config-constructors

Conversation

@apascal07

@apascal07 apascal07 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Introduces a typed-config constructor family for the primitives as a non-breaking addition for the next minor release. Every new function lives alongside its existing counterpart under a new name (ai.NewTyped*); the old constructors are deprecated and now delegate to the new ones, so behavior is shared on one code path. Built on #5862, which adds the core.New*ActionOf options-struct constructors these compose over. This is the middle PR of the plugin migration train: once providers move to NewTypedModel et al., their hand-rolled configFromRequest mapping and validation can be deleted.

Only New* constructors are added; registration composes via the existing Register methods, so no parallel Define* surface ships. The genkit.DefineTyped* wrappers are just NewTyped* + Register(g.reg), and the deprecated Define* functions point at New* + Register as the replacement.

Typed config for primitives

ai.NewTypedModel (and the embedder, evaluator, batch-evaluator, and background-model equivalents) takes a Config type parameter inferred from the provided function's signature. The framework validates the request's raw config against the config schema, deserializes it into the typed value (exact type, *Config, or map[string]any; anything else is INVALID_ARGUMENT), normalizes the request's type-erased config slot to the converted value, and passes the typed value to the function.

Before: every provider hand-rolls config mapping, and gets no validation for it.

func configFromRequest(input *ai.ModelRequest) (*genai.GenerateContentConfig, error) {
    switch config := input.Config.(type) {
    case genai.GenerateContentConfig:  return &config, nil
    case *genai.GenerateContentConfig: return config, nil
    case map[string]any:               return base.MapToStruct[genai.GenerateContentConfig](config)
    case nil:                          return &genai.GenerateContentConfig{}, nil
    default:
        return nil, core.NewPublicError(core.INVALID_ARGUMENT, "Invalid configuration type ...", nil)
    }
}

fn := func(ctx context.Context, input *ai.ModelRequest, cb ai.ModelStreamCallback) (*ai.ModelResponse, error) {
    gcc, err := configFromRequest(input)
    ...
}
ai.NewModel(name, meta, fn)

After: the framework validates, deserializes, and hands over the typed value.

fn := func(ctx context.Context, input *ai.ModelRequest, config genai.GenerateContentConfig, cb ai.ModelStreamCallback) (*ai.ModelResponse, error) {
    ...
}
ai.NewTypedModel(name, meta, fn)
  • The config JSON schema is inferred from Config unless ConfigSchema on the options struct overrides it. The override remains the escape hatch for types whose reflection is insufficient (e.g. SDK wrapper generics like Opt[float64]); those plugins use the new functions and supply the schema explicitly.
  • The request input schema's config slot is wrapped as anyOf [schema, null] so a typed-nil config (which marshals to JSON null) passes input validation.
  • Config resolution runs outermost in the model's built-in chain (normalizeConfig), so the built-in wrappers and the model function all see the typed value. Version validation moved there from validateSupport because it must run on the raw config: conversion into a Config type without a version field would silently drop the key.
  • Batch evaluators previously advertised no input schema at all; they now infer one like everything else.
  • ModelOptions gains a Metadata slot merged into the action descriptor (reserved type/model keys win), matching TypedBackgroundModelOptions.

Concrete return types

The typed constructors and the genkit.DefineTyped* wrappers return newly exported concrete types instead of the interfaces: *ModelAction, *EmbedderAction, *EvaluatorAction, and *BackgroundModelAction. This matches the existing NewTool returning *ToolDef, lets methods be added later without the interface-growth breakage problem, and satisfies api.Action structurally, so plugin Init bodies can append models to their returned action slice without type assertions.

Each type embeds core.Action (or core.BackgroundAction) through an unexported package-level alias, which promotes the action methods (Desc, Register, RunJSON, ...) without exporting the embedded field itself. The interfaces (Model, Embedder, Evaluator, BackgroundModel) are unchanged and remain the accepted-argument and lookup types; the concrete types satisfy them, and the deprecated constructors keep their interface returns.

NewTypedBackgroundModel (and its genkit wrapper) takes a dedicated flat TypedBackgroundModelOptions that inlines the ModelOptions fields, a descriptor Metadata slot (now actually wired into the descriptor; it was silently ignored before), and the optional Cancel hook. The rule on the provider surface: required functions are positional (compile-enforced, drive type inference), optional lifecycle hooks live in the options struct so future additions are a field rather than a signature break. core.NewBackgroundActionOf keeps its positional cancel because holding a generic CancelOpFunc[Out] would force the core options struct to become generic. The shipped BackgroundModelOptions is deprecated and only feeds the deprecated NewBackgroundModel.

Registry-taking Define helpers deleted

Continuing the cleanup from #5862 into the ai layer: a registry is unobtainable outside the framework, so the registry-taking Define* helpers had no reachable external callers and are deleted outright rather than deprecated; the genkit wrappers remain the user touchpoints and now compose New* + Register directly.

  • Deleted: DefineModel, DefineEmbedder, DefineEvaluator, DefineBatchEvaluator, DefineBackgroundModel, DefineMiddleware, DefineTool, DefineToolWithInputSchema, DefineMultipartTool, DefineResource (plus the experimental exp.DefineTool/exp.DefineInterruptibleTool)
  • Kept, because the registry is a structural input rather than just a registration target: ai.DefinePrompt/DefineDataPrompt, ai.DefineFormat (replacement rides with the format-registration key fix), ai.DefineGenerateAction (init bootstrap), the exp agent constructors, and the frozen retriever surface.

Deprecations

Deprecated, delegating to the new functions with identical behavior (Config=any passes the raw config through unchanged, pinned by test):

  • ai: NewModel, NewEmbedder, NewEvaluator, NewBatchEvaluator, NewBackgroundModel (pointing at NewTyped*)
  • genkit: DefineModel, DefineBackgroundModel, DefineEmbedder, DefineEvaluator, DefineBatchEvaluator (each gains a DefineTyped* counterpart)

When the deprecated functions are removed in the next major, the new ones can collapse back to the plain names with the same signatures, so migrating now is the last rename.

Behavior notes

Intentional changes observable through the deprecated constructors:

  • A version error now surfaces before capability errors (version validation runs outermost instead of inside validateSupport).
  • BackgroundModelOptions.Metadata now reaches the action descriptor through the deprecated constructors (it was silently dropped before).
  • When ConfigSchema is set, the request input schema's config slot is the null-tolerant anyOf wrapping instead of the raw schema.
  • Batch evaluators now advertise an inferred input schema.

Scope

Verification

Go 1.26.3, full go build ./... and go test ./... in go/: 40 packages with tests pass, no failures.

New coverage in ai/config_test.go: a typed model receives its config deserialized from each accepted shape and rejects the rest with INVALID_ARGUMENT; the config is normalized before the built-in wrappers run, so they see the typed value; the config schema is inferred from Config, overridden by an explicit ConfigSchema, and absent for Config=any; the deprecated constructors pass the raw config through unchanged; and Metadata reaches the descriptor for both the typed model and the typed background model. ai/define_test.go, ai/exp/define_test.go, and plugins/middleware/define_test.go hold test-local New* + Register helpers standing in for the deleted registry-taking Define*.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces generic WithConfig and WithOptions variants for defining and registering actions (including models, background models, embedders, retrievers, and evaluators) in the Go SDK. This allows the framework to automatically deserialize raw, type-erased configuration/options into strongly-typed Go structs before executing user-defined functions. Additionally, action creation in go/core has been refactored to use a consolidated ActionOptions struct, deprecating the older flat-argument constructors. As there are no review comments provided, I have no additional feedback to offer.

@apascal07
apascal07 force-pushed the ap/go-with-config-constructors branch 4 times, most recently from 51a1b16 to 70ee46b Compare July 29, 2026 22:34
@apascal07 apascal07 changed the title feat(go): add typed-config WithConfig constructors and core ActionOptions feat(go): add typed-config NewTyped* constructors and core ActionOptions Jul 29, 2026
@apascal07
apascal07 changed the base branch from main to ap/go-action-constructors July 30, 2026 16:32
@apascal07
apascal07 force-pushed the ap/go-with-config-constructors branch from f462550 to 34de461 Compare July 30, 2026 16:32
@apascal07 apascal07 changed the title feat(go): add typed-config NewTyped* constructors and core ActionOptions feat(go/ai): add typed-config NewTyped* constructors Jul 30, 2026
@apascal07
apascal07 force-pushed the ap/go-with-config-constructors branch from 34de461 to e16ee2b Compare July 30, 2026 17:32
apascal07 added 10 commits July 31, 2026 15:25
NewModelWithConfig, NewEmbedderWithConfig, NewEvaluatorWithConfig,
NewBatchEvaluatorWithConfig, and NewBackgroundModelWithConfig take a
Config type parameter inferred from the provided function's signature.
The framework validates the request's raw config against the config
schema, deserializes it into the typed value (exact type, *Config, or
map[string]any; anything else is INVALID_ARGUMENT), normalizes the
request's type-erased config slot to the converted value, and passes
the typed value to the function. This removes the per-provider
configFromRequest boilerplate once plugins migrate.

The config JSON schema is inferred from Config unless ConfigSchema
overrides it (kept for types whose reflection is insufficient, e.g. SDK
wrapper generics like Opt[float64]). The request input schema's config
slot accepts an explicit JSON null so typed-nil configs pass input
validation. Config resolution runs outermost in the model's built-in
chain; version validation moves there from validateSupport because it
must see the raw, pre-conversion config. Batch evaluators now advertise
an inferred input schema.

The untyped New*/Define* constructors are deprecated and delegate with
Config=any, which passes the raw config through unchanged (pinned by
TestDeprecatedModelPassesConfigThrough). Retrievers are left untouched
since the subsystem is slated for removal.
DefineModelWithConfig, DefineBackgroundModelWithConfig,
DefineEmbedderWithConfig, DefineEvaluatorWithConfig, and
DefineBatchEvaluatorWithConfig wrap the ai New*WithConfig constructors
as New* + Register(g.reg). The untyped Define* wrappers are deprecated
in favor of them.
NewTypedModel, NewTypedEmbedder, NewTypedEvaluator,
NewTypedBatchEvaluator, and NewTypedBackgroundModel name what the
variant actually changes: the function receives the request's config as
a typed value. Func aliases follow (TypedModelFunc[Config] et al.), and
the genkit wrappers become DefineTyped*. These names shipped earlier on
this branch and were never released, so this is a rename, not a break.
A registry is unobtainable outside the framework: plugins return
actions from Init for the framework to register and applications go
through the genkit package, so the registry-taking Define helpers had
no reachable external callers. Instead of carrying them through a
deprecation cycle, delete them; the genkit wrappers remain the user
touchpoints and now compose New* + Register directly.

Deleted: ai.DefineModel, ai.DefineEmbedder, ai.DefineEvaluator,
ai.DefineBatchEvaluator, ai.DefineBackgroundModel, ai.DefineMiddleware,
ai.DefineTool, ai.DefineToolWithInputSchema, ai.DefineMultipartTool,
ai.DefineResource, and the experimental exp.DefineTool and
exp.DefineInterruptibleTool.

Kept, because the registry is a structural input rather than just a
registration target: ai.DefinePrompt, ai.DefineDataPrompt,
ai.DefineFormat, ai.DefineGenerateAction (init bootstrap), the exp
agent constructors, and the frozen retriever surface.

Tests use package-local define helpers that compose New* + Register,
which also exercises the composition path users are pointed at.
Exports the concrete primitive types as ModelAction, EmbedderAction,
EvaluatorAction, and BackgroundModelAction, and returns them from the
NewTyped* constructors and the genkit.DefineTyped* wrappers instead of
the interfaces. A concrete return lets methods be added without the
interface-growth breakage problem, satisfies api.Action structurally so
plugin Init bodies can append models without type assertions, and
matches the existing Tool/ToolDef pattern.

The types embed core.Action through an unexported alias so the action
methods promote without exporting the embedded field itself. The
interfaces stay as the accepted-argument and lookup types, and the
deprecated constructors keep their interface returns.
NewTypedBackgroundModel and genkit.DefineTypedBackgroundModel gain a
positional nil-able cancelFn, matching core.NewBackgroundActionOf and
the invariant that options structs hold descriptor data only. The
shipped BackgroundModelOptions.Cancel field is deprecated and kept as a
fallback when cancelFn is nil, so the deprecated constructors and
existing option-based callers behave unchanged.

Also asserts api.Action on BackgroundModelAction: the plugin path
registers Init-returned actions via their Register method, so the
overriding Register handles the check and cancel sub-actions correctly.
One parameter per line on the NewTyped* and DefineTyped* family so the
signatures render readably on pkg.go.dev, matching the formatting of
core.NewBackgroundActionOf.
…structor

NewTypedBackgroundModel and genkit.DefineTypedBackgroundModel take a
dedicated options struct that inlines the ModelOptions fields instead of
embedding them, plus a descriptor Metadata slot. The Metadata field is
actually wired into the action descriptor (it was silently ignored
before, both here and on main), with the reserved type and model keys
winning over caller values.

BackgroundModelOptions is deprecated and only feeds the deprecated
NewBackgroundModel, which converts and forwards its Cancel field
positionally, so the typed path no longer reads implementation functions
from options.
Caller-provided metadata merges into the model action's descriptor with
the reserved type and model keys winning, matching the background model
options. Adding the field is additive; the deprecated constructors pick
it up through delegation.
…delOptions

Provider-surface rule: required functions stay positional (they are
compile-enforced and drive type inference), while optional lifecycle
hooks live in the options struct so future additions cost a field
instead of a signature break. Cancel is the first such hook. The core
constructor keeps its positional cancel because holding CancelOpFunc[Out]
would force BackgroundActionOptions to become generic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant