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
25 changes: 18 additions & 7 deletions go/plugins/anthropic/anthropic.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ func (a *Anthropic) DefineModel(g *genkit.Genkit, name string, opts *ai.ModelOpt
return ant.DefineModel(a.aclient, provider, name, *opts), nil
}

// modelOptions returns the ModelOptions for a Claude model name. Known models
// (see knownModels) carry curated capabilities; any other model falls back to
// defaultClaudeOpts. The returned options always carry a provider-prefixed
// label. This is the single source of model capabilities shared by ListActions
// and ResolveAction, mirroring the JS plugin's claudeModelReference.
func modelOptions(name string) ai.ModelOptions {
opts, ok := knownModels[name]
if !ok {
opts = defaultClaudeOpts
}
if opts.Label == "" {
opts.Label = fmt.Sprintf("%s - %s", anthropicLabelPrefix, name)
}
return opts
}
Comment on lines +109 to +118

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.

medium

When a user requests a model using its fully-qualified versioned ID (e.g., claude-opus-4-5-20251101), modelOptions looks up the full versioned ID in knownModels. Since knownModels only contains the unversioned aliases (e.g., claude-opus-4-5), the lookup fails and falls back to defaultClaudeOpts. This means versioned model IDs will not receive their curated capabilities (such as JSON output support).

To fix this, we should strip the 8-digit date suffix (e.g., -YYYYMMDD) from the model name before looking it up in knownModels.

func modelOptions(name string) ai.ModelOptions {
	baseName := name
	if len(name) >= 9 && name[len(name)-9] == '-' {
		isDate := true
		for i := len(name) - 8; i < len(name); i++ {
			if name[i] < '0' || name[i] > '9' {
				isDate = false
				break
			}
		}
		if isDate {
			baseName = name[:len(name)-9]
		}
	}

	opts, ok := knownModels[baseName]
	if !ok {
		opts = defaultClaudeOpts
	}
	if opts.Label == "" {
		opts.Label = fmt.Sprintf("%s - %s", anthropicLabelPrefix, name)
	}
	return opts
}


// ListActions lists all the actions supported by the Anthropic plugin
func (a *Anthropic) ListActions(ctx context.Context) []api.ActionDesc {
actions := []api.ActionDesc{}
Expand All @@ -114,7 +130,7 @@ func (a *Anthropic) ListActions(ctx context.Context) []api.ActionDesc {
for _, name := range models {
// When listing discovered models, the Genkit action name and the
// Anthropic API model ID are identical.
model := newModel(a.aclient, name, name, defaultClaudeOpts)
model := newModel(a.aclient, name, name, modelOptions(name))
if actionDef, ok := model.(api.Action); ok {
actions = append(actions, actionDef.Desc())
}
Expand Down Expand Up @@ -151,12 +167,7 @@ func (a *Anthropic) ResolveAction(atype api.ActionType, id string) api.Action {

// We register the model using the ID requested by the user, but
// use the resolved 'realID' (e.g. versioned) for actual API calls.
return newModel(a.aclient, id, realID, ai.ModelOptions{
Label: fmt.Sprintf("%s - %s", anthropicLabelPrefix, id),
Stage: ai.ModelStageStable,
Versions: []string{},
Supports: defaultClaudeOpts.Supports,
}).(api.Action)
return newModel(a.aclient, id, realID, modelOptions(id)).(api.Action)
}
return nil
}
Expand Down
56 changes: 56 additions & 0 deletions go/plugins/anthropic/anthropic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,65 @@
package anthropic

import (
"slices"
"testing"

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

// TestModelOptionsKnownModels verifies the curated Claude models resolve through
// the shared modelOptions helper (used by both ListActions and ResolveAction)
// with JS ADVANCED_MODEL_INFO-equivalent supports (JSON output) and a stable
// stage. The set mirrors the JS plugin's ADVANCED entries in KNOWN_MODELS.
func TestModelOptionsKnownModels(t *testing.T) {
advancedModels := []string{
"claude-opus-4-8",
"claude-opus-4-7",
"claude-opus-4-6",
"claude-opus-4-5",
"claude-opus-4-1",
"claude-sonnet-4-6",
"claude-sonnet-4-5",
"claude-haiku-4-5",
}
for _, name := range advancedModels {
opts := modelOptions(name)
if opts.Supports == nil {
t.Errorf("modelOptions(%q): Supports is nil", name)
continue
}
if !slices.Contains(opts.Supports.Output, "json") {
t.Errorf("modelOptions(%q): Output = %v, want it to include \"json\"", name, opts.Supports.Output)
}
if !opts.Supports.Tools || !opts.Supports.SystemRole {
t.Errorf("modelOptions(%q): expected Tools and SystemRole supported, got %+v", name, opts.Supports)
}
if opts.Stage != ai.ModelStageStable {
t.Errorf("modelOptions(%q): Stage = %q, want Stable", name, opts.Stage)
}
if opts.Label == "" {
t.Errorf("modelOptions(%q): Label is empty", name)
}
}
}

// TestModelOptionsUnknownFallback verifies models not in knownModels fall back to
// defaultClaudeOpts (no JSON output) but still get a provider-prefixed label.
func TestModelOptionsUnknownFallback(t *testing.T) {
const name = "claude-something-unreleased"
opts := modelOptions(name)

if opts.Supports == nil {
t.Fatalf("modelOptions(%q): Supports is nil", name)
}
if slices.Contains(opts.Supports.Output, "json") {
t.Errorf("modelOptions(%q): unknown model should use default supports without JSON output, got %v", name, opts.Supports.Output)
}
if want := anthropicLabelPrefix + " - " + name; opts.Label != want {
t.Errorf("modelOptions(%q): Label = %q, want %q", name, opts.Label, want)
}
}

func TestResolveModelID(t *testing.T) {
availableModels := []string{
"claude-opus-4-6",
Expand Down
38 changes: 38 additions & 0 deletions go/plugins/anthropic/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,44 @@ var defaultClaudeOpts = ai.ModelOptions{
Stage: ai.ModelStageStable,
}

// advancedClaudeSupports mirrors the JS plugin's ADVANCED_MODEL_INFO: capable
// Claude models that additionally support JSON output.
var advancedClaudeSupports = ai.ModelSupports{
Multiturn: true,
Tools: true,
ToolChoice: true,
SystemRole: true,
Media: true,
Constrained: ai.ConstrainedSupportAll,
Output: []string{"text", "json"},
}

// advancedModel builds the ModelOptions for a known, JSON-capable Claude model
// with the given display label.
func advancedModel(label string) ai.ModelOptions {
return ai.ModelOptions{
Label: anthropicLabelPrefix + " - " + label,
Supports: &advancedClaudeSupports,
Versions: []string{},
Stage: ai.ModelStageStable,
}
}

// knownModels are Claude models with curated capabilities, mirroring the JS
// plugin's KNOWN_MODELS. They are looked up via modelOptions by both
// ListActions and ResolveAction; any model not listed here falls back to
// defaultClaudeOpts and is still resolved dynamically from the Anthropic API.
var knownModels = map[string]ai.ModelOptions{
"claude-opus-4-8": advancedModel("Claude Opus 4.8"),
"claude-opus-4-7": advancedModel("Claude Opus 4.7"),
"claude-opus-4-6": advancedModel("Claude Opus 4.6"),
"claude-opus-4-5": advancedModel("Claude Opus 4.5"),
"claude-opus-4-1": advancedModel("Claude Opus 4.1"),
"claude-sonnet-4-6": advancedModel("Claude Sonnet 4.6"),
"claude-sonnet-4-5": advancedModel("Claude Sonnet 4.5"),
"claude-haiku-4-5": advancedModel("Claude Haiku 4.5"),
}

// listModels returns a list of model names supported by the Anthropic client
func listModels(ctx context.Context, client *anthropic.Client) ([]string, error) {
iter := client.Models.ListAutoPaging(ctx, anthropic.ModelListParams{})
Expand Down
50 changes: 49 additions & 1 deletion go/samples/anthropic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func main() {

g := genkit.Init(ctx, genkit.WithPlugins(&ant.Anthropic{}))

// Define a simple flow that generates a short story about a given topic
// Define a simple flow that generates a short story about a given topic.
genkit.DefineStreamingFlow(g, "storyFlow", func(ctx context.Context, input string, cb ai.ModelStreamCallback) (string, error) {
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("anthropic/claude-sonnet-4-20250514"),
Expand All @@ -50,5 +50,53 @@ func main() {
return resp.Text(), nil
})

// Same story flow on the latest Opus model. Opus 4.8 uses adaptive thinking;
// note we do NOT set Temperature or a fixed thinking budget — both are
// rejected by Opus 4.7+ in favour of adaptive thinking.
genkit.DefineStreamingFlow(g, "opusStoryFlow", func(ctx context.Context, input string, cb ai.ModelStreamCallback) (string, error) {
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("anthropic/claude-opus-4-8"),
ai.WithConfig(&anthropic.MessageNewParams{
MaxTokens: 8000,
Thinking: anthropic.ThinkingConfigParamUnion{
OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{},
},
}),
ai.WithStreaming(cb),
ai.WithPrompt(`Tell a short story about %s`, input))
if err != nil {
return "", err
}

return resp.Text(), nil
})

// Structured-output flow on the latest Sonnet model. This exercises the JSON
// output capability that the known Claude models now advertise; Genkit
// constrains the response to the Character schema.
genkit.DefineFlow(g, "characterFlow", func(ctx context.Context, topic string) (*Character, error) {
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("anthropic/claude-sonnet-4-6"),
ai.WithConfig(&anthropic.MessageNewParams{MaxTokens: 2000}),
ai.WithOutputType(Character{}),
ai.WithPrompt(`Invent a memorable character for a story about %s.`, topic))
if err != nil {
return nil, err
}

var c Character
if err := resp.Output(&c); err != nil {
return nil, err
}
return &c, nil
})

<-ctx.Done()
}

// Character is the structured output produced by characterFlow.
type Character struct {
Name string `json:"name"`
Role string `json:"role"`
Traits []string `json:"traits"`
}
Loading