// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
package anthropic
import (
"context"
"fmt"
"log/slog"
"os"
"reflect"
"sync"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/core/api"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/internal/base"
ant "github.com/firebase/genkit/go/plugins/internal/anthropic"
"github.com/invopop/jsonschema"
)
const (
provider = "anthropic"
anthropicLabelPrefix = "Anthropic"
)
// Anthropic is a Genkit plugin for interacting with the Anthropic services
type Anthropic struct {
APIKey string // If not provided, defaults to ANTHROPIC_API_KEY
BaseURL string // Optional. If not provided, defaults to ANTHROPIC_BASE_URL
aclient anthropic.Client // Anthropic client
mu sync.Mutex // Mutex to control access
initted bool // Whether the plugin has been initialized
}
// Name returns the name of the plugin
func (a *Anthropic) Name() string {
return provider
}
// Init initializes the Anthropic plugin and all known models
func (a *Anthropic) Init(ctx context.Context) []api.Action {
if a == nil {
a = &Anthropic{}
}
a.mu.Lock()
defer a.mu.Unlock()
if a.initted {
panic("plugin already initialized")
}
apiKey := a.APIKey
if apiKey == "" {
apiKey = os.Getenv("ANTHROPIC_API_KEY")
}
if apiKey == "" {
panic("Anthropic requires setting ANTHROPIC_API_KEY in the environment")
}
opts := []option.RequestOption{option.WithAPIKey(apiKey)}
baseURL := a.BaseURL
if baseURL == "" {
baseURL = os.Getenv("ANTHROPIC_BASE_URL")
}
if baseURL != "" {
opts = append(opts, option.WithBaseURL(baseURL))
}
ac := anthropic.NewClient(opts...)
a.aclient = ac
a.initted = true
return []api.Action{}
}
// DefineModel defines an unknown model with the given name.
// The second argument describes the capability of the model.
// Use [IsDefinedModel] to determine if a model is already defined.
// After [Init] is called, only the known models are defined.
func (a *Anthropic) DefineModel(g *genkit.Genkit, name string, opts *ai.ModelOptions) (ai.Model, error) {
return ant.DefineModel(a.aclient, provider, name, *opts), nil
}
// ListActions lists all the actions supported by the Anthropic plugin
func (a *Anthropic) ListActions(ctx context.Context) []api.ActionDesc {
actions := []api.ActionDesc{}
models, err := listModels(ctx, &a.aclient)
if err != nil {
slog.Error("unable to list anthropic models from Anthropic API", "error", err)
return nil
}
for _, name := range models {
model := newModel(a.aclient, name, defaultClaudeOpts)
if actionDef, ok := model.(api.Action); ok {
actions = append(actions, actionDef.Desc())
}
}
return actions
}
// Model returns a previously registered model
func Model(g *genkit.Genkit, name string) ai.Model {
return genkit.LookupModel(g, api.NewName(provider, name))
}
// IsDefinedModel returns whether a model is already defined
func IsDefinedModel(g *genkit.Genkit, name string) bool {
return genkit.LookupModel(g, api.NewName(provider, name)) != nil
}
// ResolveAction resolves an action with the given name
func (a *Anthropic) ResolveAction(atype api.ActionType, id string) api.Action {
switch atype {
case api.ActionTypeModel:
return newModel(a.aclient, id, ai.ModelOptions{
Label: fmt.Sprintf("%s - %s", anthropicLabelPrefix, id),
Stage: ai.ModelStageStable,
Versions: []string{},
Supports: defaultClaudeOpts.Supports,
}).(api.Action)
}
return nil
}
// newModel creates a model wihout registering it
func newModel(client anthropic.Client, name string, opts ai.ModelOptions) ai.Model {
config := &anthropic.MessageNewParams{}
meta := &ai.ModelOptions{
Label: opts.Label,
Supports: opts.Supports,
Versions: opts.Versions,
ConfigSchema: configToMap(config),
Stage: opts.Stage,
}
fn := func(
ctx context.Context,
input *ai.ModelRequest,
cb func(context.Context, *ai.ModelResponseChunk) error,
) (*ai.ModelResponse, error) {
return ant.Generate(ctx, client, name, input, cb)
}
return ai.NewModel(api.NewName(provider, name), meta, fn)
}
// configToMap converts a config struct to a map[string]any.
func configToMap(config any) map[string]any {
r := jsonschema.Reflector{
DoNotReference: false, // Prevent $ref usage
AllowAdditionalProperties: false,
RequiredFromJSONSchemaTags: true,
}
// The anthropic SDK uses a number of wrapper types for float, int, etc.
// By default, jsonschema will treat these as objects, but we want to
// treat them as their underlying primitive types.
r.Mapper = func(r reflect.Type) *jsonschema.Schema {
if r.Name() == "Opt[float64]" {
return &jsonschema.Schema{
Type: "number",
}
}
if r.Name() == "Opt[int64]" {
return &jsonschema.Schema{
Type: "integer",
}
}
if r.Name() == "Opt[string]" {
return &jsonschema.Schema{
Type: "string",
}
}
if r.Name() == "Opt[bool]" {
return &jsonschema.Schema{
Type: "boolean",
}
}
return nil
}
schema := r.Reflect(config)
result := base.SchemaAsMap(schema)
return result
}