Integrations
Use DEVUP AI from Go (Golang) with the official OpenAI Go SDK, openai-go, or with go-openai. Point the client at https://api.devupai.com/v1, and your code gets chat, streaming, tool calling, structured output and embeddings on DEVUP AI models.
go mod init myapp
go get github.com/openai/openai-go/v3Set your key in the terminal that runs your program:
PowerShell
$env:DEVUP_API_KEY = "YOUR_DEVUP_API_KEY"Bash
export DEVUP_API_KEY="YOUR_DEVUP_API_KEY"Save this as main.go and run go run . in the same folder:
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
)
func main() {
client := openai.NewClient(
option.WithBaseURL("https://api.devupai.com/v1"),
option.WithAPIKey(os.Getenv("DEVUP_API_KEY")),
)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
completion, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
Model: "deepseek-ai/DeepSeek-V4-Pro",
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("Reply with exactly: Salam DEVUP"),
},
})
if err != nil {
log.Fatal(err)
}
if len(completion.Choices) == 0 {
log.Fatal("response contained no choices")
}
fmt.Println(completion.Choices[0].Message.Content)
}It prints Salam DEVUP.
openai.NewClient() reads OPENAI_BASE_URL and OPENAI_API_KEY when you pass no options. Point them at DEVUP AI:
PowerShell
$env:OPENAI_BASE_URL = "https://api.devupai.com/v1"
$env:OPENAI_API_KEY = "YOUR_DEVUP_API_KEY"Bash
export OPENAI_BASE_URL="https://api.devupai.com/v1"
export OPENAI_API_KEY="YOUR_DEVUP_API_KEY"package main
import (
"context"
"fmt"
"log"
"time"
"github.com/openai/openai-go/v3"
)
func main() {
// NewClient reads OPENAI_BASE_URL and OPENAI_API_KEY from the environment.
client := openai.NewClient()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
completion, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
Model: "deepseek-ai/DeepSeek-V4-Pro",
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("Reply with exactly: Salam DEVUP"),
},
})
if err != nil {
log.Fatal(err)
}
if len(completion.Choices) == 0 {
log.Fatal("response contained no choices")
}
fmt.Println(completion.Choices[0].Message.Content)
}Wrap the client and a chat call in functions you can reuse. The functions on this page share one package; goimports or your editor adds the standard-library imports each one uses.
// newClient points the official OpenAI Go SDK at DEVUP AI.
// The key comes from the environment and is never written into source code.
func newClient() openai.Client {
return openai.NewClient(
option.WithBaseURL("https://api.devupai.com/v1"),
option.WithAPIKey(os.Getenv("DEVUP_API_KEY")),
)
}
// chat sends one message and returns the model's reply.
func chat(ctx context.Context, client openai.Client) (string, error) {
completion, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
Model: "deepseek-ai/DeepSeek-V4-Pro",
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("Reply with exactly: Salam DEVUP"),
},
})
if err != nil {
return "", err
}
if len(completion.Choices) == 0 {
return "", errors.New("response contained no choices")
}
return completion.Choices[0].Message.Content, nil
}// streamChat prints tokens as they arrive and returns the assembled completion.
// With IncludeUsage set, token usage arrives once, in the final chunk.
func streamChat(ctx context.Context, client openai.Client) (openai.ChatCompletion, error) {
stream := client.Chat.Completions.NewStreaming(ctx, openai.ChatCompletionNewParams{
Model: "deepseek-ai/DeepSeek-V4-Pro",
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("Count from 1 to 5, separated by spaces."),
},
StreamOptions: openai.ChatCompletionStreamOptionsParam{
IncludeUsage: openai.Bool(true),
},
})
defer func() { _ = stream.Close() }()
acc := openai.ChatCompletionAccumulator{}
for stream.Next() {
chunk := stream.Current()
if !acc.AddChunk(chunk) {
return openai.ChatCompletion{}, errors.New("stream chunk could not be accumulated")
}
if len(chunk.Choices) > 0 {
fmt.Print(chunk.Choices[0].Delta.Content)
}
}
if err := stream.Err(); err != nil {
return openai.ChatCompletion{}, err
}
fmt.Printf("\ntokens: %d in, %d out\n", acc.Usage.PromptTokens, acc.Usage.CompletionTokens)
return acc.ChatCompletion, nil
}With IncludeUsage set, token usage arrives once, in the final chunk, and ChatCompletionAccumulator returns the totals.
The model asks for get_order_status, your code runs it and sends the result back, and the model answers with the tracking number from your system.
// getOrderStatus stands in for a lookup in your own system.
func getOrderStatus(orderID string) string {
out, _ := json.Marshal(map[string]string{
"order_id": orderID,
"status": "shipped",
"tracking_number": "DZ-4471",
})
return string(out)
}
// toolRoundTrip lets the model call get_order_status, runs it locally,
// sends the result back, and returns the model's final answer.
func toolRoundTrip(ctx context.Context, client openai.Client) (string, error) {
params := openai.ChatCompletionNewParams{
Model: "deepseek-ai/DeepSeek-V4-Pro",
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("Where is my order 1042? Include its tracking number."),
},
Tools: []openai.ChatCompletionToolUnionParam{
openai.ChatCompletionFunctionTool(openai.FunctionDefinitionParam{
Name: "get_order_status",
Description: openai.String("Look up the shipping status and tracking number of an order"),
Parameters: openai.FunctionParameters{
"type": "object",
"properties": map[string]any{
"order_id": map[string]string{"type": "string"},
},
"required": []string{"order_id"},
},
}),
},
}
first, err := client.Chat.Completions.New(ctx, params)
if err != nil {
return "", err
}
if len(first.Choices) == 0 {
return "", errors.New("response contained no choices")
}
msg := first.Choices[0].Message
if len(msg.ToolCalls) == 0 {
return "", errors.New("the model answered without calling the tool")
}
// Every tool call must get a tool message back, including unknown ones.
params.Messages = append(params.Messages, msg.ToParam())
for _, call := range msg.ToolCalls {
result := `{"error":"unknown tool"}`
if call.Function.Name == "get_order_status" {
var args struct {
OrderID string `json:"order_id"`
}
if err := json.Unmarshal([]byte(call.Function.Arguments), &args); err != nil {
return "", fmt.Errorf("decode tool arguments: %w", err)
}
fmt.Printf("get_order_status(%q)\n", args.OrderID)
result = getOrderStatus(args.OrderID)
}
params.Messages = append(params.Messages, openai.ToolMessage(result, call.ID))
}
final, err := client.Chat.Completions.New(ctx, params)
if err != nil {
return "", err
}
if len(final.Choices) == 0 {
return "", errors.New("response contained no choices")
}
return final.Choices[0].Message.Content, nil
}Answer every tool call with a tool message, as the loop does, even for a tool name you do not recognise.
Ask for JSON that matches a strict schema, then decode it into a Go struct.
// Capital is the shape the model must return.
type Capital struct {
City string `json:"city"`
Country string `json:"country"`
}
// structuredOutput asks for JSON that matches a strict schema and decodes it.
func structuredOutput(ctx context.Context, client openai.Client) (Capital, error) {
schema := map[string]any{
"type": "object",
"properties": map[string]any{
"city": map[string]string{"type": "string"},
"country": map[string]string{"type": "string"},
},
"required": []string{"city", "country"},
"additionalProperties": false,
}
completion, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
Model: "deepseek-ai/DeepSeek-V4-Pro",
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("What is the capital of Algeria?"),
},
ResponseFormat: openai.ChatCompletionNewParamsResponseFormatUnion{
OfJSONSchema: &openai.ResponseFormatJSONSchemaParam{
JSONSchema: openai.ResponseFormatJSONSchemaJSONSchemaParam{
Name: "capital",
Schema: schema,
Strict: openai.Bool(true),
},
},
},
})
if err != nil {
return Capital{}, err
}
if len(completion.Choices) == 0 {
return Capital{}, errors.New("response contained no choices")
}
var out Capital
if err := json.Unmarshal([]byte(completion.Choices[0].Message.Content), &out); err != nil {
return Capital{}, fmt.Errorf("decode structured output: %w", err)
}
return out, nil
}It returns Algiers, Algeria.
BAAI/bge-m3 returns one 1024-dimensional vector per input.
// embed returns one vector per input, in input order.
func embed(ctx context.Context, client openai.Client, texts []string) ([][]float64, error) {
res, err := client.Embeddings.New(ctx, openai.EmbeddingNewParams{
Model: "BAAI/bge-m3",
Input: openai.EmbeddingNewParamsInputUnion{OfArrayOfStrings: texts},
})
if err != nil {
return nil, err
}
vectors := make([][]float64, len(texts))
for _, d := range res.Data {
if d.Index < 0 || int(d.Index) >= len(vectors) {
return nil, fmt.Errorf("embedding index %d out of range", d.Index)
}
vectors[d.Index] = d.Embedding
}
return vectors, nil
}API errors come back as *openai.Error with the HTTP status. By default openai-go retries connection errors and 408, 409, 429 and 5xx responses twice, honouring Retry-After; option.WithMaxRetries changes that.
// explainError turns an SDK error into a message you can act on.
// By default the SDK retries connection errors and 408, 409, 429 and 5xx
// responses twice, honouring Retry-After; option.WithMaxRetries changes that.
func explainError(err error) string {
var apiErr *openai.Error
if errors.As(err, &apiErr) {
switch apiErr.StatusCode {
case 401:
return "invalid or revoked API key (401)"
case 429:
return "rate limit reached, retry later (429)"
default:
return fmt.Sprintf("API error %d: %s", apiErr.StatusCode, apiErr.Message)
}
}
if errors.Is(err, context.DeadlineExceeded) {
return "request timed out"
}
return err.Error()
}Already on github.com/sashabaranov/go-openai? Set its BaseURL to DEVUP AI.
go get github.com/sashabaranov/go-openaiImport it as goopenai "github.com/sashabaranov/go-openai" so it does not clash with openai-go.
// newGoOpenAIClient points github.com/sashabaranov/go-openai at DEVUP AI.
func newGoOpenAIClient() *goopenai.Client {
cfg := goopenai.DefaultConfig(os.Getenv("DEVUP_API_KEY"))
cfg.BaseURL = "https://api.devupai.com/v1"
return goopenai.NewClientWithConfig(cfg)
}
func chatGoOpenAI(ctx context.Context, client *goopenai.Client) (string, error) {
resp, err := client.CreateChatCompletion(ctx, goopenai.ChatCompletionRequest{
Model: "deepseek-ai/DeepSeek-V4-Pro",
Messages: []goopenai.ChatCompletionMessage{
{Role: goopenai.ChatMessageRoleUser, Content: "Reply with exactly: Salam DEVUP"},
},
})
if err != nil {
return "", err
}
if len(resp.Choices) == 0 {
return "", errors.New("response contained no choices")
}
return resp.Choices[0].Message.Content, nil
}Streaming with usage:
// streamGoOpenAI prints tokens as they arrive and returns the full text and usage.
func streamGoOpenAI(ctx context.Context, client *goopenai.Client) (string, *goopenai.Usage, error) {
stream, err := client.CreateChatCompletionStream(ctx, goopenai.ChatCompletionRequest{
Model: "deepseek-ai/DeepSeek-V4-Pro",
Messages: []goopenai.ChatCompletionMessage{
{Role: goopenai.ChatMessageRoleUser, Content: "Count from 1 to 5, separated by spaces."},
},
StreamOptions: &goopenai.StreamOptions{IncludeUsage: true},
})
if err != nil {
return "", nil, err
}
defer func() { _ = stream.Close() }()
var text strings.Builder
var usage *goopenai.Usage
for {
resp, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return "", nil, err
}
if len(resp.Choices) > 0 {
fmt.Print(resp.Choices[0].Delta.Content)
text.WriteString(resp.Choices[0].Delta.Content)
}
if resp.Usage != nil {
usage = resp.Usage
}
}
fmt.Println()
return text.String(), usage, nil
}Use exact catalog IDs as the Model value: a chat model such as deepseek-ai/DeepSeek-V4-Pro, and BAAI/bge-m3 for embeddings. Tool calling needs a model with tool calling. See the Models catalog, Embeddings guide, and Tool Calling guide.
Yes. Pass option.WithBaseURL("https://api.devupai.com/v1") and your DEVUP AI key to openai.NewClient, then use any DEVUP AI catalog ID as the model.
Go 1.25 or newer for openai-go v3. go-openai works with Go 1.18 or newer.
Every chat and embedding request is a regular DEVUP AI API call, metered in Algerian Dinar on your account.
Yes. Set IncludeUsage in StreamOptions. Usage arrives once, in the final chunk, and ChatCompletionAccumulator returns the totals.
Yes. Create the config with goopenai.DefaultConfig, set BaseURL to https://api.devupai.com/v1, and build the client with NewClientWithConfig.
Yes. Call client.Embeddings.New with the model BAAI/bge-m3. Each input returns a 1024-dimensional vector.