Skip to content

Commit 320b303

Browse files
mikeb26Octium Agent
andcommitted
feat(openai-go): add EINO model component using official OpenAI Go SDK
Create a new `components/model/openai-go` module implementing EINO model support backed by `github.com/openai/openai-go/v3` and the Responses API. The component supports tool calling (including required/choices), multimodal inputs, and streaming. Co-authored-by: Octium Agent <agent@octium.dev>
1 parent 3b3b8b5 commit 320b303

13 files changed

Lines changed: 2850 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# OpenAI (official openai-go SDK)
2+
3+
An OpenAI model implementation for [Eino](https://github.com/cloudwego/eino) using the official OpenAI Go SDK (`github.com/openai/openai-go/v3`). This is intended as a starting point for eventual replacement of the existing openai implementation (see ../openai) which is based on github.com/sashabaranov/go-openai. Newer models from OpenAI increasingly do not fully support the older chat completions API which github.com/sashabaranov/go-openai is based on. Consequently, this component targets the **Responses API only**.
4+
5+
## Features
6+
7+
- Implements `github.com/cloudwego/eino/components/model.ToolCallingChatModel`
8+
- Responses API (non-stream + streaming)
9+
- Tool calling support (function tools)
10+
- Multimodal inputs via `schema.Message.UserInputMultiContent`:
11+
- text
12+
- image_url (URL or base64 via `Base64Data` + `MIMEType`)
13+
- file_url (URL or base64)
14+
15+
## Installation
16+
17+
```bash
18+
go get github.com/cloudwego/eino-ext/components/model/openai-go@latest
19+
```
20+
21+
## Quick start
22+
23+
```go
24+
package main
25+
26+
import (
27+
"context"
28+
"log"
29+
"os"
30+
31+
"github.com/cloudwego/eino/schema"
32+
"github.com/cloudwego/eino-ext/components/model/openai-go"
33+
)
34+
35+
func main() {
36+
ctx := context.Background()
37+
38+
cm, err := openaigo.NewChatModel(ctx, &openaigo.Config{
39+
APIKey: os.Getenv("OPENAI_API_KEY"),
40+
Model: "gpt-5.4", // any Responses API capable model
41+
})
42+
if err != nil {
43+
log.Fatal(err)
44+
}
45+
46+
out, err := cm.Generate(ctx, []*schema.Message{
47+
{Role: schema.User, Content: "Hello"},
48+
})
49+
if err != nil {
50+
log.Fatal(err)
51+
}
52+
53+
log.Println(out.Content)
54+
}
55+
```
56+
57+
## Tool calling
58+
59+
Bind tools using `WithTools()`:
60+
61+
```go
62+
cm2, err := cm.WithTools([]*schema.ToolInfo{
63+
{
64+
Name: "get_weather",
65+
Desc: "Get weather at the given location",
66+
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
67+
"location": {Type: schema.String, Required: true},
68+
}),
69+
},
70+
})
71+
```
72+
73+
Then control selection with Eino common options:
74+
75+
- `model.WithTools(...)`
76+
- `model.WithToolChoice(schema.ToolChoiceAllowed|Forced|Forbidden, allowedToolNames...)`
77+
78+
## Streaming
79+
80+
Use `Stream()` to receive incremental `*schema.Message` deltas.
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
* Copyright 2026 CloudWeGo Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package openaigo
18+
19+
import (
20+
"context"
21+
"errors"
22+
"fmt"
23+
"net/http"
24+
"time"
25+
26+
"github.com/cloudwego/eino/callbacks"
27+
"github.com/cloudwego/eino/components"
28+
"github.com/cloudwego/eino/components/model"
29+
"github.com/cloudwego/eino/schema"
30+
"github.com/openai/openai-go/v3"
31+
"github.com/openai/openai-go/v3/option"
32+
"github.com/openai/openai-go/v3/responses"
33+
)
34+
35+
var _ model.ToolCallingChatModel = (*ChatModel)(nil)
36+
37+
type Config struct {
38+
APIKey string `json:"api_key"`
39+
40+
// Timeout specifies the maximum duration to wait for API responses.
41+
// If HTTPClient is set, Timeout will not be used.
42+
// Optional. Default: no timeout
43+
Timeout time.Duration `json:"timeout"`
44+
45+
// HTTPClient specifies the client to send HTTP requests.
46+
// If HTTPClient is set, Timeout will not be used.
47+
// Optional. Default &http.Client{Timeout: Timeout}
48+
HTTPClient *http.Client `json:"http_client"`
49+
50+
// BaseURL specifies the OpenAI endpoint URL
51+
// Optional. Default: https://api.openai.com/v1
52+
BaseURL string `json:"base_url"`
53+
54+
// Model specifies the ID of the model to use.
55+
// Optional.
56+
Model string `json:"model,omitempty"`
57+
58+
// MaxOutputTokens is an upper bound for the number of tokens that can be generated for a response,
59+
// including visible output tokens and reasoning tokens.
60+
MaxOutputTokens *int `json:"max_output_tokens,omitempty"`
61+
62+
TopP *float32 `json:"top_p,omitempty"`
63+
Temperature *float32 `json:"temperature,omitempty"`
64+
65+
// Reasoning config for reasoning models.
66+
Reasoning *Reasoning `json:"reasoning,omitempty"`
67+
68+
// Store indicates whether to store the generated model response for later retrieval.
69+
Store *bool `json:"store,omitempty"`
70+
71+
// Metadata set of key-value pairs that can be attached to an object.
72+
Metadata map[string]string `json:"metadata,omitempty"`
73+
74+
// ExtraFields will override any existing fields with the same key.
75+
// Optional. Useful for experimental features not yet officially supported.
76+
ExtraFields map[string]any `json:"extra_fields,omitempty"`
77+
}
78+
79+
type ChatModel struct {
80+
cli openai.Client
81+
82+
model string
83+
maxOutTok *int
84+
topP *float32
85+
temperature *float32
86+
reasoning *Reasoning
87+
store *bool
88+
metadata map[string]string
89+
extraFields map[string]any
90+
91+
tools []responses.ToolUnionParam
92+
rawTools []*schema.ToolInfo
93+
toolChoice *schema.ToolChoice
94+
}
95+
96+
func NewChatModel(_ context.Context, config *Config) (*ChatModel, error) {
97+
if config == nil {
98+
return nil, fmt.Errorf("config cannot be nil")
99+
}
100+
101+
opts := make([]option.RequestOption, 0, 4)
102+
if config.APIKey != "" {
103+
opts = append(opts, option.WithAPIKey(config.APIKey))
104+
}
105+
if config.BaseURL != "" {
106+
opts = append(opts, option.WithBaseURL(config.BaseURL))
107+
}
108+
if config.HTTPClient != nil {
109+
opts = append(opts, option.WithHTTPClient(config.HTTPClient))
110+
} else if config.Timeout > 0 {
111+
opts = append(opts, option.WithHTTPClient(&http.Client{Timeout: config.Timeout}))
112+
}
113+
114+
cli := openai.NewClient(opts...)
115+
116+
cm := &ChatModel{
117+
cli: cli,
118+
model: config.Model,
119+
maxOutTok: config.MaxOutputTokens,
120+
topP: config.TopP,
121+
temperature: config.Temperature,
122+
reasoning: config.Reasoning,
123+
store: config.Store,
124+
metadata: cloneStringMap(config.Metadata),
125+
extraFields: cloneAnyMap(config.ExtraFields),
126+
}
127+
128+
return cm, nil
129+
}
130+
131+
func (cm *ChatModel) Generate(ctx context.Context, in []*schema.Message, opts ...model.Option) (outMsg *schema.Message, err error) {
132+
ctx = callbacks.EnsureRunInfo(ctx, cm.GetType(), components.ComponentOfChatModel)
133+
134+
params, cbIn, err := cm.buildParams(in, false, opts...)
135+
if err != nil {
136+
return nil, err
137+
}
138+
139+
ctx = callbacks.OnStart(ctx, cbIn)
140+
defer func() {
141+
if err != nil {
142+
callbacks.OnError(ctx, err)
143+
}
144+
}()
145+
146+
resp, err := cm.cli.Responses.New(ctx, params)
147+
if err != nil {
148+
return nil, err
149+
}
150+
151+
outMsg, err = cm.convertResponseToMessage(resp)
152+
if err != nil {
153+
return nil, err
154+
}
155+
156+
callbacks.OnEnd(ctx, &model.CallbackOutput{
157+
Message: outMsg,
158+
Config: cbIn.Config,
159+
TokenUsage: toModelTokenUsage(outMsg.ResponseMeta),
160+
Extra: map[string]any{
161+
callbackExtraModelName: string(resp.Model),
162+
},
163+
})
164+
165+
return outMsg, nil
166+
}
167+
168+
func (cm *ChatModel) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) {
169+
if len(tools) == 0 {
170+
return nil, errors.New("no tools to bind")
171+
}
172+
openAITools, rawTools, err := toOpenAITools(tools)
173+
if err != nil {
174+
return nil, err
175+
}
176+
177+
tc := schema.ToolChoiceAllowed
178+
ncm := *cm
179+
ncm.tools = openAITools
180+
ncm.rawTools = rawTools
181+
ncm.toolChoice = &tc
182+
return &ncm, nil
183+
}
184+
185+
const typ = "OpenAI"
186+
187+
func (cm *ChatModel) GetType() string { return typ }
188+
189+
func (cm *ChatModel) IsCallbacksEnabled() bool { return true }

0 commit comments

Comments
 (0)