1package provider
2
3import (
4 "context"
5 "fmt"
6
7 "github.com/opencode-ai/opencode/internal/llm/models"
8 "github.com/opencode-ai/opencode/internal/llm/tools"
9 "github.com/opencode-ai/opencode/internal/message"
10)
11
12type EventType string
13
14const maxRetries = 8
15
16const (
17 EventContentStart EventType = "content_start"
18 EventToolUseStart EventType = "tool_use_start"
19 EventToolUseDelta EventType = "tool_use_delta"
20 EventToolUseStop EventType = "tool_use_stop"
21 EventContentDelta EventType = "content_delta"
22 EventThinkingDelta EventType = "thinking_delta"
23 EventContentStop EventType = "content_stop"
24 EventComplete EventType = "complete"
25 EventError EventType = "error"
26 EventWarning EventType = "warning"
27)
28
29type TokenUsage struct {
30 InputTokens int64
31 OutputTokens int64
32 CacheCreationTokens int64
33 CacheReadTokens int64
34}
35
36type ProviderResponse struct {
37 Content string
38 ToolCalls []message.ToolCall
39 Usage TokenUsage
40 FinishReason message.FinishReason
41}
42
43type ProviderEvent struct {
44 Type EventType
45
46 Content string
47 Thinking string
48 Response *ProviderResponse
49 ToolCall *message.ToolCall
50 Error error
51}
52type Provider interface {
53 SendMessages(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error)
54
55 StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
56
57 Model() models.Model
58}
59
60type providerClientOptions struct {
61 apiKey string
62 model models.Model
63 maxTokens int64
64 systemMessage string
65
66 anthropicOptions []AnthropicOption
67 openaiOptions []OpenAIOption
68 geminiOptions []GeminiOption
69 bedrockOptions []BedrockOption
70}
71
72type ProviderClientOption func(*providerClientOptions)
73
74type ProviderClient interface {
75 send(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error)
76 stream(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
77}
78
79type baseProvider[C ProviderClient] struct {
80 options providerClientOptions
81 client C
82}
83
84func NewProvider(providerName models.ModelProvider, opts ...ProviderClientOption) (Provider, error) {
85 clientOptions := providerClientOptions{}
86 for _, o := range opts {
87 o(&clientOptions)
88 }
89 switch providerName {
90 case models.ProviderAnthropic:
91 return &baseProvider[AnthropicClient]{
92 options: clientOptions,
93 client: newAnthropicClient(clientOptions),
94 }, nil
95 case models.ProviderOpenAI:
96 return &baseProvider[OpenAIClient]{
97 options: clientOptions,
98 client: newOpenAIClient(clientOptions),
99 }, nil
100 case models.ProviderGemini:
101 return &baseProvider[GeminiClient]{
102 options: clientOptions,
103 client: newGeminiClient(clientOptions),
104 }, nil
105 case models.ProviderBedrock:
106 return &baseProvider[BedrockClient]{
107 options: clientOptions,
108 client: newBedrockClient(clientOptions),
109 }, nil
110 case models.ProviderGROQ:
111 clientOptions.openaiOptions = append(clientOptions.openaiOptions,
112 WithOpenAIBaseURL("https://api.groq.com/openai/v1"),
113 )
114 return &baseProvider[OpenAIClient]{
115 options: clientOptions,
116 client: newOpenAIClient(clientOptions),
117 }, nil
118 case models.ProviderAzure:
119 return &baseProvider[AzureClient]{
120 options: clientOptions,
121 client: newAzureClient(clientOptions),
122 }, nil
123 case models.ProviderVertexAI:
124 return &baseProvider[VertexAIClient]{
125 options: clientOptions,
126 client: newVertexAIClient(clientOptions),
127 }, nil
128 case models.ProviderOpenRouter:
129 clientOptions.openaiOptions = append(clientOptions.openaiOptions,
130 WithOpenAIBaseURL("https://openrouter.ai/api/v1"),
131 WithOpenAIExtraHeaders(map[string]string{
132 "HTTP-Referer": "opencode.ai",
133 "X-Title": "OpenCode",
134 }),
135 )
136 return &baseProvider[OpenAIClient]{
137 options: clientOptions,
138 client: newOpenAIClient(clientOptions),
139 }, nil
140 case models.ProviderXAI:
141 clientOptions.openaiOptions = append(clientOptions.openaiOptions,
142 WithOpenAIBaseURL("https://api.x.ai/v1"),
143 )
144 return &baseProvider[OpenAIClient]{
145 options: clientOptions,
146 client: newOpenAIClient(clientOptions),
147 }, nil
148
149 case models.ProviderMock:
150 // TODO: implement mock client for test
151 panic("not implemented")
152 }
153 return nil, fmt.Errorf("provider not supported: %s", providerName)
154}
155
156func (p *baseProvider[C]) cleanMessages(messages []message.Message) (cleaned []message.Message) {
157 for _, msg := range messages {
158 // The message has no content
159 if len(msg.Parts) == 0 {
160 continue
161 }
162 cleaned = append(cleaned, msg)
163 }
164 return
165}
166
167func (p *baseProvider[C]) SendMessages(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error) {
168 messages = p.cleanMessages(messages)
169 return p.client.send(ctx, messages, tools)
170}
171
172func (p *baseProvider[C]) Model() models.Model {
173 return p.options.model
174}
175
176func (p *baseProvider[C]) StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent {
177 messages = p.cleanMessages(messages)
178 return p.client.stream(ctx, messages, tools)
179}
180
181func WithAPIKey(apiKey string) ProviderClientOption {
182 return func(options *providerClientOptions) {
183 options.apiKey = apiKey
184 }
185}
186
187func WithModel(model models.Model) ProviderClientOption {
188 return func(options *providerClientOptions) {
189 options.model = model
190 }
191}
192
193func WithMaxTokens(maxTokens int64) ProviderClientOption {
194 return func(options *providerClientOptions) {
195 options.maxTokens = maxTokens
196 }
197}
198
199func WithSystemMessage(systemMessage string) ProviderClientOption {
200 return func(options *providerClientOptions) {
201 options.systemMessage = systemMessage
202 }
203}
204
205func WithAnthropicOptions(anthropicOptions ...AnthropicOption) ProviderClientOption {
206 return func(options *providerClientOptions) {
207 options.anthropicOptions = anthropicOptions
208 }
209}
210
211func WithOpenAIOptions(openaiOptions ...OpenAIOption) ProviderClientOption {
212 return func(options *providerClientOptions) {
213 options.openaiOptions = openaiOptions
214 }
215}
216
217func WithGeminiOptions(geminiOptions ...GeminiOption) ProviderClientOption {
218 return func(options *providerClientOptions) {
219 options.geminiOptions = geminiOptions
220 }
221}
222
223func WithBedrockOptions(bedrockOptions ...BedrockOption) ProviderClientOption {
224 return func(options *providerClientOptions) {
225 options.bedrockOptions = bedrockOptions
226 }
227}