on main help sign in
feat: Add support for OpenRouter (#92)
98e2910Isaac Scarrott
README.md+7−3
@@ -11,7 +11,7 @@ OpenCode is a Go-based CLI application that brings AI assistance to your termina
11## Features
11## Features
12
12
13- **Interactive TUI**: Built with [Bubble Tea](https://github.com/charmbracelet/bubbletea) for a smooth terminal experience
13- **Interactive TUI**: Built with [Bubble Tea](https://github.com/charmbracelet/bubbletea) for a smooth terminal experience
14−- **Multiple AI Providers**: Support for OpenAI, Anthropic Claude, Google Gemini, AWS Bedrock, Groq, and Azure OpenAI
14+- **Multiple AI Providers**: Support for OpenAI, Anthropic Claude, Google Gemini, AWS Bedrock, Groq, Azure OpenAI, and OpenRouter
15- **Session Management**: Save and manage multiple conversation sessions
15- **Session Management**: Save and manage multiple conversation sessions
16- **Tool Integration**: AI can execute commands, search files, and modify code
16- **Tool Integration**: AI can execute commands, search files, and modify code
17- **Vim-like Editor**: Integrated editor with text input capabilities
17- **Vim-like Editor**: Integrated editor with text input capabilities
@@ -97,8 +97,12 @@ You can configure OpenCode using environment variables:
97 "disabled": false
97 "disabled": false
98 },
98 },
99 "groq": {
99 "groq": {
100− "apiKey": "your-api-key",
100+ "apiKey": "your-api-key",
101+ "disabled": false
102+ },
103+ "openrouter": {
104+ "apiKey": "your-api-key",
101− "disabled": false
105+ "disabled": false
102 }
106 }
103 },
107 },
104 "agents": {
108 "agents": {
cmd/schema/main.go+1
@@ -173,6 +173,7 @@ func generateSchema() map[string]any {
173 string(models.ProviderOpenAI),
173 string(models.ProviderOpenAI),
174 string(models.ProviderGemini),
174 string(models.ProviderGemini),
175 string(models.ProviderGROQ),
175 string(models.ProviderGROQ),
176+ string(models.ProviderOpenRouter),
176 string(models.ProviderBedrock),
177 string(models.ProviderBedrock),
177 string(models.ProviderAzure),
178 string(models.ProviderAzure),
178 }
179 }
internal/config/config.go+39
@@ -267,6 +267,15 @@ func setProviderDefaults() {
267 return
267 return
268 }
268 }
269
269
270+ // OpenRouter configuration
271+ if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
272+ viper.SetDefault("providers.openrouter.apiKey", apiKey)
273+ viper.SetDefault("agents.coder.model", models.OpenRouterClaude37Sonnet)
274+ viper.SetDefault("agents.task.model", models.OpenRouterClaude37Sonnet)
275+ viper.SetDefault("agents.title.model", models.OpenRouterClaude35Haiku)
276+ return
277+ }
278+
270 // AWS Bedrock configuration
279 // AWS Bedrock configuration
271 if hasAWSCredentials() {
280 if hasAWSCredentials() {
272 viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
281 viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
@@ -527,6 +536,8 @@ func getProviderAPIKey(provider models.ModelProvider) string {
527 return os.Getenv("GROQ_API_KEY")
536 return os.Getenv("GROQ_API_KEY")
528 case models.ProviderAzure:
537 case models.ProviderAzure:
529 return os.Getenv("AZURE_OPENAI_API_KEY")
538 return os.Getenv("AZURE_OPENAI_API_KEY")
539+ case models.ProviderOpenRouter:
540+ return os.Getenv("OPENROUTER_API_KEY")
530 case models.ProviderBedrock:
541 case models.ProviderBedrock:
531 if hasAWSCredentials() {
542 if hasAWSCredentials() {
532 return "aws-credentials-available"
543 return "aws-credentials-available"
@@ -578,6 +589,34 @@ func setDefaultModelForAgent(agent AgentName) bool {
578 return true
589 return true
579 }
590 }
580
591
592+ if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
593+ var model models.ModelID
594+ maxTokens := int64(5000)
595+ reasoningEffort := ""
596+
597+ switch agent {
598+ case AgentTitle:
599+ model = models.OpenRouterClaude35Haiku
600+ maxTokens = 80
601+ case AgentTask:
602+ model = models.OpenRouterClaude37Sonnet
603+ default:
604+ model = models.OpenRouterClaude37Sonnet
605+ }
606+
607+ // Check if model supports reasoning
608+ if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
609+ reasoningEffort = "medium"
610+ }
611+
612+ cfg.Agents[agent] = Agent{
613+ Model: model,
614+ MaxTokens: maxTokens,
615+ ReasoningEffort: reasoningEffort,
616+ }
617+ return true
618+ }
619+
581 if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
620 if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
582 var model models.ModelID
621 var model models.ModelID
583 maxTokens := int64(5000)
622 maxTokens := int64(5000)
internal/llm/models/models.go+1
@@ -86,4 +86,5 @@ func init() {
86 maps.Copy(SupportedModels, GeminiModels)
86 maps.Copy(SupportedModels, GeminiModels)
87 maps.Copy(SupportedModels, GroqModels)
87 maps.Copy(SupportedModels, GroqModels)
88 maps.Copy(SupportedModels, AzureModels)
88 maps.Copy(SupportedModels, AzureModels)
89+ maps.Copy(SupportedModels, OpenRouterModels)
89}
90}
internal/llm/models/openrouter.gonew file+262
@@ -0,0 +1,262 @@
1+package models
2+
3+const (
4+ ProviderOpenRouter ModelProvider = "openrouter"
5+
6+ OpenRouterGPT41 ModelID = "openrouter.gpt-4.1"
7+ OpenRouterGPT41Mini ModelID = "openrouter.gpt-4.1-mini"
8+ OpenRouterGPT41Nano ModelID = "openrouter.gpt-4.1-nano"
9+ OpenRouterGPT45Preview ModelID = "openrouter.gpt-4.5-preview"
10+ OpenRouterGPT4o ModelID = "openrouter.gpt-4o"
11+ OpenRouterGPT4oMini ModelID = "openrouter.gpt-4o-mini"
12+ OpenRouterO1 ModelID = "openrouter.o1"
13+ OpenRouterO1Pro ModelID = "openrouter.o1-pro"
14+ OpenRouterO1Mini ModelID = "openrouter.o1-mini"
15+ OpenRouterO3 ModelID = "openrouter.o3"
16+ OpenRouterO3Mini ModelID = "openrouter.o3-mini"
17+ OpenRouterO4Mini ModelID = "openrouter.o4-mini"
18+ OpenRouterGemini25Flash ModelID = "openrouter.gemini-2.5-flash"
19+ OpenRouterGemini25 ModelID = "openrouter.gemini-2.5"
20+ OpenRouterClaude35Sonnet ModelID = "openrouter.claude-3.5-sonnet"
21+ OpenRouterClaude3Haiku ModelID = "openrouter.claude-3-haiku"
22+ OpenRouterClaude37Sonnet ModelID = "openrouter.claude-3.7-sonnet"
23+ OpenRouterClaude35Haiku ModelID = "openrouter.claude-3.5-haiku"
24+ OpenRouterClaude3Opus ModelID = "openrouter.claude-3-opus"
25+)
26+
27+var OpenRouterModels = map[ModelID]Model{
28+ OpenRouterGPT41: {
29+ ID: OpenRouterGPT41,
30+ Name: "OpenRouter – GPT 4.1",
31+ Provider: ProviderOpenRouter,
32+ APIModel: "openai/gpt-4.1",
33+ CostPer1MIn: OpenAIModels[GPT41].CostPer1MIn,
34+ CostPer1MInCached: OpenAIModels[GPT41].CostPer1MInCached,
35+ CostPer1MOut: OpenAIModels[GPT41].CostPer1MOut,
36+ CostPer1MOutCached: OpenAIModels[GPT41].CostPer1MOutCached,
37+ ContextWindow: OpenAIModels[GPT41].ContextWindow,
38+ DefaultMaxTokens: OpenAIModels[GPT41].DefaultMaxTokens,
39+ },
40+ OpenRouterGPT41Mini: {
41+ ID: OpenRouterGPT41Mini,
42+ Name: "OpenRouter – GPT 4.1 mini",
43+ Provider: ProviderOpenRouter,
44+ APIModel: "openai/gpt-4.1-mini",
45+ CostPer1MIn: OpenAIModels[GPT41Mini].CostPer1MIn,
46+ CostPer1MInCached: OpenAIModels[GPT41Mini].CostPer1MInCached,
47+ CostPer1MOut: OpenAIModels[GPT41Mini].CostPer1MOut,
48+ CostPer1MOutCached: OpenAIModels[GPT41Mini].CostPer1MOutCached,
49+ ContextWindow: OpenAIModels[GPT41Mini].ContextWindow,
50+ DefaultMaxTokens: OpenAIModels[GPT41Mini].DefaultMaxTokens,
51+ },
52+ OpenRouterGPT41Nano: {
53+ ID: OpenRouterGPT41Nano,
54+ Name: "OpenRouter – GPT 4.1 nano",
55+ Provider: ProviderOpenRouter,
56+ APIModel: "openai/gpt-4.1-nano",
57+ CostPer1MIn: OpenAIModels[GPT41Nano].CostPer1MIn,
58+ CostPer1MInCached: OpenAIModels[GPT41Nano].CostPer1MInCached,
59+ CostPer1MOut: OpenAIModels[GPT41Nano].CostPer1MOut,
60+ CostPer1MOutCached: OpenAIModels[GPT41Nano].CostPer1MOutCached,
61+ ContextWindow: OpenAIModels[GPT41Nano].ContextWindow,
62+ DefaultMaxTokens: OpenAIModels[GPT41Nano].DefaultMaxTokens,
63+ },
64+ OpenRouterGPT45Preview: {
65+ ID: OpenRouterGPT45Preview,
66+ Name: "OpenRouter – GPT 4.5 preview",
67+ Provider: ProviderOpenRouter,
68+ APIModel: "openai/gpt-4.5-preview",
69+ CostPer1MIn: OpenAIModels[GPT45Preview].CostPer1MIn,
70+ CostPer1MInCached: OpenAIModels[GPT45Preview].CostPer1MInCached,
71+ CostPer1MOut: OpenAIModels[GPT45Preview].CostPer1MOut,
72+ CostPer1MOutCached: OpenAIModels[GPT45Preview].CostPer1MOutCached,
73+ ContextWindow: OpenAIModels[GPT45Preview].ContextWindow,
74+ DefaultMaxTokens: OpenAIModels[GPT45Preview].DefaultMaxTokens,
75+ },
76+ OpenRouterGPT4o: {
77+ ID: OpenRouterGPT4o,
78+ Name: "OpenRouter – GPT 4o",
79+ Provider: ProviderOpenRouter,
80+ APIModel: "openai/gpt-4o",
81+ CostPer1MIn: OpenAIModels[GPT4o].CostPer1MIn,
82+ CostPer1MInCached: OpenAIModels[GPT4o].CostPer1MInCached,
83+ CostPer1MOut: OpenAIModels[GPT4o].CostPer1MOut,
84+ CostPer1MOutCached: OpenAIModels[GPT4o].CostPer1MOutCached,
85+ ContextWindow: OpenAIModels[GPT4o].ContextWindow,
86+ DefaultMaxTokens: OpenAIModels[GPT4o].DefaultMaxTokens,
87+ },
88+ OpenRouterGPT4oMini: {
89+ ID: OpenRouterGPT4oMini,
90+ Name: "OpenRouter – GPT 4o mini",
91+ Provider: ProviderOpenRouter,
92+ APIModel: "openai/gpt-4o-mini",
93+ CostPer1MIn: OpenAIModels[GPT4oMini].CostPer1MIn,
94+ CostPer1MInCached: OpenAIModels[GPT4oMini].CostPer1MInCached,
95+ CostPer1MOut: OpenAIModels[GPT4oMini].CostPer1MOut,
96+ CostPer1MOutCached: OpenAIModels[GPT4oMini].CostPer1MOutCached,
97+ ContextWindow: OpenAIModels[GPT4oMini].ContextWindow,
98+ },
99+ OpenRouterO1: {
100+ ID: OpenRouterO1,
101+ Name: "OpenRouter – O1",
102+ Provider: ProviderOpenRouter,
103+ APIModel: "openai/o1",
104+ CostPer1MIn: OpenAIModels[O1].CostPer1MIn,
105+ CostPer1MInCached: OpenAIModels[O1].CostPer1MInCached,
106+ CostPer1MOut: OpenAIModels[O1].CostPer1MOut,
107+ CostPer1MOutCached: OpenAIModels[O1].CostPer1MOutCached,
108+ ContextWindow: OpenAIModels[O1].ContextWindow,
109+ DefaultMaxTokens: OpenAIModels[O1].DefaultMaxTokens,
110+ CanReason: OpenAIModels[O1].CanReason,
111+ },
112+ OpenRouterO1Pro: {
113+ ID: OpenRouterO1Pro,
114+ Name: "OpenRouter – o1 pro",
115+ Provider: ProviderOpenRouter,
116+ APIModel: "openai/o1-pro",
117+ CostPer1MIn: OpenAIModels[O1Pro].CostPer1MIn,
118+ CostPer1MInCached: OpenAIModels[O1Pro].CostPer1MInCached,
119+ CostPer1MOut: OpenAIModels[O1Pro].CostPer1MOut,
120+ CostPer1MOutCached: OpenAIModels[O1Pro].CostPer1MOutCached,
121+ ContextWindow: OpenAIModels[O1Pro].ContextWindow,
122+ DefaultMaxTokens: OpenAIModels[O1Pro].DefaultMaxTokens,
123+ CanReason: OpenAIModels[O1Pro].CanReason,
124+ },
125+ OpenRouterO1Mini: {
126+ ID: OpenRouterO1Mini,
127+ Name: "OpenRouter – o1 mini",
128+ Provider: ProviderOpenRouter,
129+ APIModel: "openai/o1-mini",
130+ CostPer1MIn: OpenAIModels[O1Mini].CostPer1MIn,
131+ CostPer1MInCached: OpenAIModels[O1Mini].CostPer1MInCached,
132+ CostPer1MOut: OpenAIModels[O1Mini].CostPer1MOut,
133+ CostPer1MOutCached: OpenAIModels[O1Mini].CostPer1MOutCached,
134+ ContextWindow: OpenAIModels[O1Mini].ContextWindow,
135+ DefaultMaxTokens: OpenAIModels[O1Mini].DefaultMaxTokens,
136+ CanReason: OpenAIModels[O1Mini].CanReason,
137+ },
138+ OpenRouterO3: {
139+ ID: OpenRouterO3,
140+ Name: "OpenRouter – o3",
141+ Provider: ProviderOpenRouter,
142+ APIModel: "openai/o3",
143+ CostPer1MIn: OpenAIModels[O3].CostPer1MIn,
144+ CostPer1MInCached: OpenAIModels[O3].CostPer1MInCached,
145+ CostPer1MOut: OpenAIModels[O3].CostPer1MOut,
146+ CostPer1MOutCached: OpenAIModels[O3].CostPer1MOutCached,
147+ ContextWindow: OpenAIModels[O3].ContextWindow,
148+ DefaultMaxTokens: OpenAIModels[O3].DefaultMaxTokens,
149+ CanReason: OpenAIModels[O3].CanReason,
150+ },
151+ OpenRouterO3Mini: {
152+ ID: OpenRouterO3Mini,
153+ Name: "OpenRouter – o3 mini",
154+ Provider: ProviderOpenRouter,
155+ APIModel: "openai/o3-mini-high",
156+ CostPer1MIn: OpenAIModels[O3Mini].CostPer1MIn,
157+ CostPer1MInCached: OpenAIModels[O3Mini].CostPer1MInCached,
158+ CostPer1MOut: OpenAIModels[O3Mini].CostPer1MOut,
159+ CostPer1MOutCached: OpenAIModels[O3Mini].CostPer1MOutCached,
160+ ContextWindow: OpenAIModels[O3Mini].ContextWindow,
161+ DefaultMaxTokens: OpenAIModels[O3Mini].DefaultMaxTokens,
162+ CanReason: OpenAIModels[O3Mini].CanReason,
163+ },
164+ OpenRouterO4Mini: {
165+ ID: OpenRouterO4Mini,
166+ Name: "OpenRouter – o4 mini",
167+ Provider: ProviderOpenRouter,
168+ APIModel: "openai/o4-mini-high",
169+ CostPer1MIn: OpenAIModels[O4Mini].CostPer1MIn,
170+ CostPer1MInCached: OpenAIModels[O4Mini].CostPer1MInCached,
171+ CostPer1MOut: OpenAIModels[O4Mini].CostPer1MOut,
172+ CostPer1MOutCached: OpenAIModels[O4Mini].CostPer1MOutCached,
173+ ContextWindow: OpenAIModels[O4Mini].ContextWindow,
174+ DefaultMaxTokens: OpenAIModels[O4Mini].DefaultMaxTokens,
175+ CanReason: OpenAIModels[O4Mini].CanReason,
176+ },
177+ OpenRouterGemini25Flash: {
178+ ID: OpenRouterGemini25Flash,
179+ Name: "OpenRouter – Gemini 2.5 Flash",
180+ Provider: ProviderOpenRouter,
181+ APIModel: "google/gemini-2.5-flash-preview:thinking",
182+ CostPer1MIn: GeminiModels[Gemini25Flash].CostPer1MIn,
183+ CostPer1MInCached: GeminiModels[Gemini25Flash].CostPer1MInCached,
184+ CostPer1MOut: GeminiModels[Gemini25Flash].CostPer1MOut,
185+ CostPer1MOutCached: GeminiModels[Gemini25Flash].CostPer1MOutCached,
186+ ContextWindow: GeminiModels[Gemini25Flash].ContextWindow,
187+ DefaultMaxTokens: GeminiModels[Gemini25Flash].DefaultMaxTokens,
188+ },
189+ OpenRouterGemini25: {
190+ ID: OpenRouterGemini25,
191+ Name: "OpenRouter – Gemini 2.5 Pro",
192+ Provider: ProviderOpenRouter,
193+ APIModel: "google/gemini-2.5-pro-preview-03-25",
194+ CostPer1MIn: GeminiModels[Gemini25].CostPer1MIn,
195+ CostPer1MInCached: GeminiModels[Gemini25].CostPer1MInCached,
196+ CostPer1MOut: GeminiModels[Gemini25].CostPer1MOut,
197+ CostPer1MOutCached: GeminiModels[Gemini25].CostPer1MOutCached,
198+ ContextWindow: GeminiModels[Gemini25].ContextWindow,
199+ DefaultMaxTokens: GeminiModels[Gemini25].DefaultMaxTokens,
200+ },
201+ OpenRouterClaude35Sonnet: {
202+ ID: OpenRouterClaude35Sonnet,
203+ Name: "OpenRouter – Claude 3.5 Sonnet",
204+ Provider: ProviderOpenRouter,
205+ APIModel: "anthropic/claude-3.5-sonnet",
206+ CostPer1MIn: AnthropicModels[Claude35Sonnet].CostPer1MIn,
207+ CostPer1MInCached: AnthropicModels[Claude35Sonnet].CostPer1MInCached,
208+ CostPer1MOut: AnthropicModels[Claude35Sonnet].CostPer1MOut,
209+ CostPer1MOutCached: AnthropicModels[Claude35Sonnet].CostPer1MOutCached,
210+ ContextWindow: AnthropicModels[Claude35Sonnet].ContextWindow,
211+ DefaultMaxTokens: AnthropicModels[Claude35Sonnet].DefaultMaxTokens,
212+ },
213+ OpenRouterClaude3Haiku: {
214+ ID: OpenRouterClaude3Haiku,
215+ Name: "OpenRouter – Claude 3 Haiku",
216+ Provider: ProviderOpenRouter,
217+ APIModel: "anthropic/claude-3-haiku",
218+ CostPer1MIn: AnthropicModels[Claude3Haiku].CostPer1MIn,
219+ CostPer1MInCached: AnthropicModels[Claude3Haiku].CostPer1MInCached,
220+ CostPer1MOut: AnthropicModels[Claude3Haiku].CostPer1MOut,
221+ CostPer1MOutCached: AnthropicModels[Claude3Haiku].CostPer1MOutCached,
222+ ContextWindow: AnthropicModels[Claude3Haiku].ContextWindow,
223+ DefaultMaxTokens: AnthropicModels[Claude3Haiku].DefaultMaxTokens,
224+ },
225+ OpenRouterClaude37Sonnet: {
226+ ID: OpenRouterClaude37Sonnet,
227+ Name: "OpenRouter – Claude 3.7 Sonnet",
228+ Provider: ProviderOpenRouter,
229+ APIModel: "anthropic/claude-3.7-sonnet",
230+ CostPer1MIn: AnthropicModels[Claude37Sonnet].CostPer1MIn,
231+ CostPer1MInCached: AnthropicModels[Claude37Sonnet].CostPer1MInCached,
232+ CostPer1MOut: AnthropicModels[Claude37Sonnet].CostPer1MOut,
233+ CostPer1MOutCached: AnthropicModels[Claude37Sonnet].CostPer1MOutCached,
234+ ContextWindow: AnthropicModels[Claude37Sonnet].ContextWindow,
235+ DefaultMaxTokens: AnthropicModels[Claude37Sonnet].DefaultMaxTokens,
236+ CanReason: AnthropicModels[Claude37Sonnet].CanReason,
237+ },
238+ OpenRouterClaude35Haiku: {
239+ ID: OpenRouterClaude35Haiku,
240+ Name: "OpenRouter – Claude 3.5 Haiku",
241+ Provider: ProviderOpenRouter,
242+ APIModel: "anthropic/claude-3.5-haiku",
243+ CostPer1MIn: AnthropicModels[Claude35Haiku].CostPer1MIn,
244+ CostPer1MInCached: AnthropicModels[Claude35Haiku].CostPer1MInCached,
245+ CostPer1MOut: AnthropicModels[Claude35Haiku].CostPer1MOut,
246+ CostPer1MOutCached: AnthropicModels[Claude35Haiku].CostPer1MOutCached,
247+ ContextWindow: AnthropicModels[Claude35Haiku].ContextWindow,
248+ DefaultMaxTokens: AnthropicModels[Claude35Haiku].DefaultMaxTokens,
249+ },
250+ OpenRouterClaude3Opus: {
251+ ID: OpenRouterClaude3Opus,
252+ Name: "OpenRouter – Claude 3 Opus",
253+ Provider: ProviderOpenRouter,
254+ APIModel: "anthropic/claude-3-opus",
255+ CostPer1MIn: AnthropicModels[Claude3Opus].CostPer1MIn,
256+ CostPer1MInCached: AnthropicModels[Claude3Opus].CostPer1MInCached,
257+ CostPer1MOut: AnthropicModels[Claude3Opus].CostPer1MOut,
258+ CostPer1MOutCached: AnthropicModels[Claude3Opus].CostPer1MOutCached,
259+ ContextWindow: AnthropicModels[Claude3Opus].ContextWindow,
260+ DefaultMaxTokens: AnthropicModels[Claude3Opus].DefaultMaxTokens,
261+ },
262+}
internal/llm/provider/openai.go+29−3
@@ -21,6 +21,7 @@ type openaiOptions struct {
21 baseURL string
21 baseURL string
22 disableCache bool
22 disableCache bool
23 reasoningEffort string
23 reasoningEffort string
24+ extraHeaders map[string]string
24}
25}
25
26
26type OpenAIOption func(*openaiOptions)
27type OpenAIOption func(*openaiOptions)
@@ -49,6 +50,12 @@ func newOpenAIClient(opts providerClientOptions) OpenAIClient {
49 openaiClientOptions = append(openaiClientOptions, option.WithBaseURL(openaiOpts.baseURL))
50 openaiClientOptions = append(openaiClientOptions, option.WithBaseURL(openaiOpts.baseURL))
50 }
51 }
51
52
53+ if openaiOpts.extraHeaders != nil {
54+ for key, value := range openaiOpts.extraHeaders {
55+ openaiClientOptions = append(openaiClientOptions, option.WithHeader(key, value))
56+ }
57+ }
58+
52 client := openai.NewClient(openaiClientOptions...)
59 client := openai.NewClient(openaiClientOptions...)
53 return &openaiClient{
60 return &openaiClient{
54 providerOptions: opts,
61 providerOptions: opts,
@@ -204,11 +211,18 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too
204 content = openaiResponse.Choices[0].Message.Content
211 content = openaiResponse.Choices[0].Message.Content
205 }
212 }
206
213
214+ toolCalls := o.toolCalls(*openaiResponse)
215+ finishReason := o.finishReason(string(openaiResponse.Choices[0].FinishReason))
216+
217+ if len(toolCalls) > 0 {
218+ finishReason = message.FinishReasonToolUse
219+ }
220+
207 return &ProviderResponse{
221 return &ProviderResponse{
208 Content: content,
222 Content: content,
209− ToolCalls: o.toolCalls(*openaiResponse),
223+ ToolCalls: toolCalls,
210 Usage: o.usage(*openaiResponse),
224 Usage: o.usage(*openaiResponse),
211− FinishReason: o.finishReason(string(openaiResponse.Choices[0].FinishReason)),
225+ FinishReason: finishReason,
212 }, nil
226 }, nil
213 }
227 }
214}
228}
@@ -267,13 +281,19 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
267 err := openaiStream.Err()
281 err := openaiStream.Err()
268 if err == nil || errors.Is(err, io.EOF) {
282 if err == nil || errors.Is(err, io.EOF) {
269 // Stream completed successfully
283 // Stream completed successfully
284+ finishReason := o.finishReason(string(acc.ChatCompletion.Choices[0].FinishReason))
285+
286+ if len(toolCalls) > 0 {
287+ finishReason = message.FinishReasonToolUse
288+ }
289+
270 eventChan <- ProviderEvent{
290 eventChan <- ProviderEvent{
271 Type: EventComplete,
291 Type: EventComplete,
272 Response: &ProviderResponse{
292 Response: &ProviderResponse{
273 Content: currentContent,
293 Content: currentContent,
274 ToolCalls: toolCalls,
294 ToolCalls: toolCalls,
275 Usage: o.usage(acc.ChatCompletion),
295 Usage: o.usage(acc.ChatCompletion),
276− FinishReason: o.finishReason(string(acc.ChatCompletion.Choices[0].FinishReason)),
296+ FinishReason: finishReason,
277 },
297 },
278 }
298 }
279 close(eventChan)
299 close(eventChan)
@@ -375,6 +395,12 @@ func WithOpenAIBaseURL(baseURL string) OpenAIOption {
375 }
395 }
376}
396}
377
397
398+func WithOpenAIExtraHeaders(headers map[string]string) OpenAIOption {
399+ return func(options *openaiOptions) {
400+ options.extraHeaders = headers
401+ }
402+}
403+
378func WithOpenAIDisableCache() OpenAIOption {
404func WithOpenAIDisableCache() OpenAIOption {
379 return func(options *openaiOptions) {
405 return func(options *openaiOptions) {
380 options.disableCache = true
406 options.disableCache = true
internal/llm/provider/provider.go+12
@@ -120,6 +120,18 @@ func NewProvider(providerName models.ModelProvider, opts ...ProviderClientOption
120 options: clientOptions,
120 options: clientOptions,
121 client: newAzureClient(clientOptions),
121 client: newAzureClient(clientOptions),
122 }, nil
122 }, nil
123+ case models.ProviderOpenRouter:
124+ clientOptions.openaiOptions = append(clientOptions.openaiOptions,
125+ WithOpenAIBaseURL("https://openrouter.ai/api/v1"),
126+ WithOpenAIExtraHeaders(map[string]string{
127+ "HTTP-Referer": "opencode.ai",
128+ "X-Title": "OpenCode",
129+ }),
130+ )
131+ return &baseProvider[OpenAIClient]{
132+ options: clientOptions,
133+ client: newOpenAIClient(clientOptions),
134+ }, nil
123 case models.ProviderMock:
135 case models.ProviderMock:
124 // TODO: implement mock client for test
136 // TODO: implement mock client for test
125 panic("not implemented")
137 panic("not implemented")
opencode-schema.json+54−32
@@ -49,23 +49,38 @@
49 "gpt-4.1-mini",
49 "gpt-4.1-mini",
50 "azure.gpt-4.1-mini",
50 "azure.gpt-4.1-mini",
51 "gemini-2.5",
51 "gemini-2.5",
52− "meta-llama/llama-4-scout-17b-16e-instruct"
52+ "meta-llama/llama-4-scout-17b-16e-instruct",
53+ "openrouter.deepseek-chat-free",
54+ "openrouter.deepseek-r1-free",
55+ "openrouter.gpt-4.1",
56+ "openrouter.gpt-4.1-mini",
57+ "openrouter.gpt-4.1-nano",
58+ "openrouter.gpt-4.5-preview",
59+ "openrouter.gpt-4o",
60+ "openrouter.gpt-4o-mini",
61+ "openrouter.o1",
62+ "openrouter.o1-pro",
63+ "openrouter.o1-mini",
64+ "openrouter.o3",
65+ "openrouter.o3-mini",
66+ "openrouter.o4-mini",
67+ "openrouter.gemini-2.5-flash",
68+ "openrouter.gemini-2.5",
69+ "openrouter.claude-3.5-sonnet",
70+ "openrouter.claude-3-haiku",
71+ "openrouter.claude-3.7-sonnet",
72+ "openrouter.claude-3.5-haiku",
73+ "openrouter.claude-3-opus"
53 ],
74 ],
54 "type": "string"
75 "type": "string"
55 },
76 },
56 "reasoningEffort": {
77 "reasoningEffort": {
57 "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
78 "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
58− "enum": [
59− "low",
60− "medium",
79+ "enum": ["low", "medium", "high"],
61− "high"
62− ],
63 "type": "string"
80 "type": "string"
64 }
81 }
65 },
82 },
66− "required": [
83+ "required": ["model"],
67− "model"
68− ],
69 "type": "object"
84 "type": "object"
70 }
85 }
71 },
86 },
@@ -120,23 +135,38 @@
120 "gpt-4.1-mini",
135 "gpt-4.1-mini",
121 "azure.gpt-4.1-mini",
136 "azure.gpt-4.1-mini",
122 "gemini-2.5",
137 "gemini-2.5",
123− "meta-llama/llama-4-scout-17b-16e-instruct"
138+ "meta-llama/llama-4-scout-17b-16e-instruct",
139+ "openrouter.deepseek-chat-free",
140+ "openrouter.deepseek-r1-free",
141+ "openrouter.gpt-4.1",
142+ "openrouter.gpt-4.1-mini",
143+ "openrouter.gpt-4.1-nano",
144+ "openrouter.gpt-4.5-preview",
145+ "openrouter.gpt-4o",
146+ "openrouter.gpt-4o-mini",
147+ "openrouter.o1",
148+ "openrouter.o1-pro",
149+ "openrouter.o1-mini",
150+ "openrouter.o3",
151+ "openrouter.o3-mini",
152+ "openrouter.o4-mini",
153+ "openrouter.gemini-2.5-flash",
154+ "openrouter.gemini-2.5",
155+ "openrouter.claude-3.5-sonnet",
156+ "openrouter.claude-3-haiku",
157+ "openrouter.claude-3.7-sonnet",
158+ "openrouter.claude-3.5-haiku",
159+ "openrouter.claude-3-opus"
124 ],
160 ],
125 "type": "string"
161 "type": "string"
126 },
162 },
127 "reasoningEffort": {
163 "reasoningEffort": {
128 "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
164 "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
129− "enum": [
130− "low",
131− "medium",
165+ "enum": ["low", "medium", "high"],
132− "high"
133− ],
134 "type": "string"
166 "type": "string"
135 }
167 }
136 },
168 },
137− "required": [
169+ "required": ["model"],
138− "model"
139− ],
140 "type": "object"
170 "type": "object"
141 },
171 },
142 "description": "Agent configurations",
172 "description": "Agent configurations",
@@ -182,9 +212,7 @@
182 "type": "string"
212 "type": "string"
183 }
213 }
184 },
214 },
185− "required": [
186− "directory"
215+ "required": ["directory"],
187− ],
188 "type": "object"
216 "type": "object"
189 },
217 },
190 "debug": {
218 "debug": {
@@ -222,9 +250,7 @@
222 "type": "object"
250 "type": "object"
223 }
251 }
224 },
252 },
225− "required": [
253+ "required": ["command"],
226− "command"
227− ],
228 "type": "object"
254 "type": "object"
229 },
255 },
230 "description": "Language Server Protocol configurations",
256 "description": "Language Server Protocol configurations",
@@ -262,10 +288,7 @@
262 "type": {
288 "type": {
263 "default": "stdio",
289 "default": "stdio",
264 "description": "Type of MCP server",
290 "description": "Type of MCP server",
265− "enum": [
266− "stdio",
291+ "enum": ["stdio", "sse"],
267− "sse"
268− ],
269 "type": "string"
292 "type": "string"
270 },
293 },
271 "url": {
294 "url": {
@@ -273,9 +296,7 @@
273 "type": "string"
296 "type": "string"
274 }
297 }
275 },
298 },
276− "required": [
299+ "required": ["command"],
277− "command"
278− ],
279 "type": "object"
300 "type": "object"
280 },
301 },
281 "description": "Model Control Protocol server configurations",
302 "description": "Model Control Protocol server configurations",
@@ -302,7 +323,8 @@
302 "gemini",
323 "gemini",
303 "groq",
324 "groq",
304 "bedrock",
325 "bedrock",
305− "azure"
326+ "azure",
327+ "openrouter"
306 ],
328 ],
307 "type": "string"
329 "type": "string"
308 }
330 }