Skip to content

feat(go/plugins/compat_oai): add deepseek plugin #3208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,12 @@ require (

require (
cloud.google.com/go/alloydb v1.16.1 // indirect
github.com/cohesion-org/deepseek-go v1.3.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/ollama/ollama v0.6.5 // indirect
go.opencensus.io v0.24.0 // indirect
)

Expand Down
4 changes: 4 additions & 0 deletions go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cohesion-org/deepseek-go v1.3.2 h1:WTZ/2346KFYca+n+DL5p+Ar1RQxF2w/wGkU4jDvyXaQ=
github.com/cohesion-org/deepseek-go v1.3.2/go.mod h1:bOVyKj38r90UEYZFrmJOzJKPxuAh8sIzHOCnLOpiXeI=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -253,6 +255,8 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
Expand Down
3 changes: 2 additions & 1 deletion go/plugins/compat_oai/compat_oai.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ func (o *OpenAICompatible) ListActions(ctx context.Context) []core.ActionDesc {
actions := []core.ActionDesc{}

models, err := listOpenAIModels(ctx, o.client)
fmt.Printf("got: models: %#v\n\n err: %v\n\n", models, err)
if err != nil {
return nil
}
Expand All @@ -198,7 +199,7 @@ func (o *OpenAICompatible) ListActions(ctx context.Context) []core.ActionDesc {
"systemRole": true,
"tools": true,
"toolChoice": true,
"constrained": true,
"constrained": "no-tools",
},
},
"versions": []string{},
Expand Down
70 changes: 70 additions & 0 deletions go/plugins/compat_oai/deepseek/deepseek.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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.

package deepseek

import (
"context"
"fmt"

"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/core"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/compat_oai"
"github.com/openai/openai-go/option"
)

const (
provider = "deepseek"
baseURL = "https://api.deepseek.com/v1"
)

type DeepSeek struct {
Opts []option.RequestOption
openAICompatible compat_oai.OpenAICompatible
}

func (d *DeepSeek) Name() string {
return provider
}

func (d *DeepSeek) Init(ctx context.Context, g *genkit.Genkit) error {
d.Opts = append(d.Opts, option.WithBaseURL(baseURL))

d.openAICompatible.Opts = d.Opts
d.openAICompatible.Provider = provider
if err := d.openAICompatible.Init(ctx, g); err != nil {
return err
}

return nil
}

func (d *DeepSeek) Model(g *genkit.Genkit, name string) ai.Model {
return d.openAICompatible.Model(g, name, provider)
}

func (d *DeepSeek) DefineModel(g *genkit.Genkit, name string, info ai.ModelInfo) (ai.Model, error) {
return d.openAICompatible.DefineModel(g, provider, name, info)
}

func (d *DeepSeek) ListActions(ctx context.Context) []core.ActionDesc {
fmt.Printf("listing actions!!\n\n")
return d.openAICompatible.ListActions(ctx)
}

func (d *DeepSeek) ResolveAction(g *genkit.Genkit, atype core.ActionType, name string) error {
fmt.Printf("resolving actions!!\n\n")
return d.openAICompatible.ResolveAction(g, atype, name)
}
234 changes: 234 additions & 0 deletions go/plugins/compat_oai/deepseek/deepsek_live_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// 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.

package deepseek_test

import (
"context"
"math"
"os"
"strings"
"testing"

"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/compat_oai/deepseek"
"github.com/openai/openai-go"
"github.com/openai/openai-go/option"
)

func TestPlugin(t *testing.T) {
apiKey := os.Getenv("DEEPSEEK_API_KEY")
if apiKey == "" {
t.Skip("Skipping test: DEEPSEEK_API_KEY environment variable not set")
}

ctx := context.Background()

// Initialize the DeepSeek plugin
oai := &deepseek.DeepSeek{
Opts: []option.RequestOption{
option.WithAPIKey(apiKey),
},
}
g, err := genkit.Init(context.Background(),
genkit.WithDefaultModel("deepseek/deepseek-chat"),
genkit.WithPlugins(oai),
)
if err != nil {
t.Fatal(err)
}
t.Log("genkit initialized")

// Define a tool for calculating gablorkens
gablorkenTool := genkit.DefineTool(g, "gablorken", "use when need to calculate a gablorken",
func(ctx *ai.ToolContext, input struct {
Value float64
Over float64
},
) (float64, error) {
return math.Pow(input.Value, input.Over), nil
},
)

t.Run("basic completion", func(t *testing.T) {
t.Log("generating basic completion response")
resp, err := genkit.Generate(ctx, g,
ai.WithPrompt("What is the capital of France?"),
)
if err != nil {
t.Fatal("error generating basic completion response: ", err)
}
t.Logf("basic completion response: %+v", resp)

out := resp.Message.Content[0].Text
if !strings.Contains(strings.ToLower(out), "paris") {
t.Errorf("got %q, expecting it to contain 'Paris'", out)
}

// Verify usage statistics are present
if resp.Usage == nil || resp.Usage.TotalTokens == 0 {
t.Error("Expected non-zero usage statistics")
}
})

t.Run("streaming", func(t *testing.T) {
var streamedOutput string
chunks := 0

final, err := genkit.Generate(ctx, g,
ai.WithPrompt("Write a short paragraph about artificial intelligence."),
ai.WithStreaming(func(ctx context.Context, chunk *ai.ModelResponseChunk) error {
chunks++
for _, content := range chunk.Content {
streamedOutput += content.Text
}
return nil
}))
if err != nil {
t.Fatal(err)
}

// Verify streaming worked
if chunks <= 1 {
t.Error("Expected multiple chunks for streaming")
}

// Verify final output matches streamed content
finalOutput := ""
for _, content := range final.Message.Content {
finalOutput += content.Text
}
if streamedOutput != finalOutput {
t.Errorf("Streaming output doesn't match final output\nStreamed: %s\nFinal: %s",
streamedOutput, finalOutput)
}

t.Logf("streaming response: %+v", finalOutput)
})

t.Run("tool usage with basic completion", func(t *testing.T) {
resp, err := genkit.Generate(ctx, g,
ai.WithPrompt("what is a gablorken of 2 over 3.5?"),
ai.WithTools(gablorkenTool))
if err != nil {
t.Fatal(err)
}

out := resp.Message.Content[0].Text
const want = "12.25"
if !strings.Contains(out, want) {
t.Errorf("got %q, expecting it to contain %q", out, want)
}

t.Logf("tool usage with basic completion response: %+v", out)
})

t.Run("tool usage with streaming", func(t *testing.T) {
var streamedOutput string
chunks := 0

final, err := genkit.Generate(ctx, g,
ai.WithPrompt("what is a gablorken of 2 over 3.5?"),
ai.WithTools(gablorkenTool),
ai.WithStreaming(func(ctx context.Context, chunk *ai.ModelResponseChunk) error {
chunks++
for _, content := range chunk.Content {
streamedOutput += content.Text
}
return nil
}))
if err != nil {
t.Fatal(err)
}

// Verify streaming worked
if chunks <= 1 {
t.Error("Expected multiple chunks for streaming")
}

// Verify final output matches streamed content
finalOutput := ""
for _, content := range final.Message.Content {
finalOutput += content.Text
}
if streamedOutput != finalOutput {
t.Errorf("Streaming output doesn't match final output\nStreamed: %s\nFinal: %s",
streamedOutput, finalOutput)
}

const want = "12.25"
if !strings.Contains(finalOutput, want) {
t.Errorf("got %q, expecting it to contain %q", finalOutput, want)
}

t.Logf("tool usage with streaming response: %+v", finalOutput)
})

t.Run("system message", func(t *testing.T) {
resp, err := genkit.Generate(ctx, g,
ai.WithPrompt("What are you?"),
ai.WithSystem("You are a helpful math tutor who loves numbers."),
)
if err != nil {
t.Fatal(err)
}

out := resp.Message.Content[0].Text
if !strings.Contains(strings.ToLower(out), "math") {
t.Errorf("got %q, expecting response to mention being a math tutor", out)
}

t.Logf("system message response: %+v", out)
})

t.Run("generation config", func(t *testing.T) {
// Create a config with specific parameters
config := &openai.ChatCompletionNewParams{
Temperature: openai.Float(0.2),
MaxCompletionTokens: openai.Int(50),
TopP: openai.Float(0.5),
Stop: openai.ChatCompletionNewParamsStopUnion{
OfStringArray: []string{".", "!", "?"},
},
}

resp, err := genkit.Generate(ctx, g,
ai.WithPrompt("Write a short sentence about artificial intelligence."),
ai.WithConfig(config),
)
if err != nil {
t.Fatal(err)
}
out := resp.Message.Content[0].Text
t.Logf("generation config response: %+v", out)
})

t.Run("invalid config type", func(t *testing.T) {
// Try to use a string as config instead of *ai.GenerationCommonConfig
config := "not a config"

_, err := genkit.Generate(ctx, g,
ai.WithPrompt("Write a short sentence about artificial intelligence."),
ai.WithConfig(config),
)
if err == nil {
t.Fatal("expected error for invalid config type")
}
if !strings.Contains(err.Error(), "unexpected config type: string") {
t.Errorf("got error %q, want error containing 'unexpected config type: string'", err.Error())
}
t.Logf("invalid config type error: %v", err)
})
}
Loading
Loading