Skip to content

WIP Provider Config #98

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 3 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: 2 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

tea "github.com/charmbracelet/bubbletea"
zone "github.com/lrstanley/bubblezone"
"github.com/opencode-ai/opencode/internal/app"
"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/db"
Expand All @@ -16,7 +17,6 @@ import (
"github.com/opencode-ai/opencode/internal/pubsub"
"github.com/opencode-ai/opencode/internal/tui"
"github.com/opencode-ai/opencode/internal/version"
zone "github.com/lrstanley/bubblezone"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -80,6 +80,7 @@ to assist developers in writing, debugging, and understanding code directly from
tui.New(app),
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
tea.WithReportFocus(),
)

// Initialize MCP tools in the background
Expand Down
25 changes: 18 additions & 7 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ type MCPServer struct {

type AgentName string

type ProviderType string

const (
ProviderTypeOpenAI ProviderType = "openai"
)

const (
AgentCoder AgentName = "coder"
AgentTask AgentName = "task"
Expand All @@ -41,15 +47,20 @@ const (

// Agent defines configuration for different LLM models and their token limits.
type Agent struct {
Model models.ModelID `json:"model"`
MaxTokens int64 `json:"maxTokens"`
ReasoningEffort string `json:"reasoningEffort"` // For openai models low,medium,heigh
Model models.ModelID `json:"model"`
Provider models.ModelProvider `json:"provider"`
MaxTokens int64 `json:"maxTokens"`
ReasoningEffort string `json:"reasoningEffort"` // For openai models low,medium,heigh
}

// Provider defines configuration for an LLM provider.
type Provider struct {
APIKey string `json:"apiKey"`
Disabled bool `json:"disabled"`
APIKey string `json:"apiKey"`
Disabled bool `json:"disabled"`
Type ProviderType `json:"type"` // will be used to set the parent provider, e.x openai for openai compatible APIs
Models map[models.ModelID]models.Model `json:"models"`
BaseURL string `json:"baseUrl"`
EnableReasoningEffort bool `json:"enableReasoningEffort"`
}

// Data defines storage configuration.
Expand Down Expand Up @@ -332,8 +343,8 @@ func Validate() error {
// Validate agent models
for name, agent := range cfg.Agents {
// Check if model exists
model, modelExists := models.SupportedModels[agent.Model]
if !modelExists {
model, err := GetModel(agent.Model, agent.Provider)
if err != nil {
logging.Warn("unsupported model configured, reverting to default",
"agent", name,
"configured_model", agent.Model)
Expand Down
83 changes: 83 additions & 0 deletions internal/config/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package config

import (
"fmt"

"github.com/opencode-ai/opencode/internal/llm/models"
)

func GetModel(model models.ModelID, provider models.ModelProvider) (models.Model, error) {
if model == "" {
return models.Model{}, fmt.Errorf("model id is empty")
}

m, foundModel := models.SupportedModels[model]

if foundModel {
return m, nil
}

providerName := m.Provider
if providerName == "" {
providerName = provider
}

if providerName == "" {
return models.Model{}, fmt.Errorf("model %s not found", model)
}
providerCfg, foundProvider := cfg.Providers[providerName]
if !foundProvider {
return models.Model{}, fmt.Errorf("provider %s not supported", providerName)
}
if providerCfg.Disabled {
return models.Model{}, fmt.Errorf("provider %s is not enabled", providerName)
}

// try to find the model in the provider config
if !foundModel {
m, foundModel = providerCfg.Models[model]
// Add some default behavior
if foundModel {
m.Provider = providerName
if m.ID == "" {
m.ID = model
}
if m.APIModel == "" {
m.APIModel = string(model)
}
if m.Name == "" {
m.Name = fmt.Sprintf("%s: %s", providerName, model)
}
if m.Ref != "" {
existingModel, foundExisting := models.SupportedModels[models.ModelID(m.Ref)]
if foundExisting {
m.CostPer1MIn = existingModel.CostPer1MIn
m.CostPer1MInCached = existingModel.CostPer1MInCached
m.CostPer1MOut = existingModel.CostPer1MOut
m.CostPer1MOutCached = existingModel.CostPer1MOutCached
m.ContextWindow = existingModel.ContextWindow
m.DefaultMaxTokens = existingModel.DefaultMaxTokens
m.ContextWindow = existingModel.ContextWindow
}
}
if m.DefaultMaxTokens == 0 {
m.DefaultMaxTokens = 4096
}
if m.ContextWindow == 0 {
m.ContextWindow = 50_000
}
}

// if not found create a simple model just based on the model id
if !foundModel {
m = models.Model{
ID: model,
APIModel: string(model),
Provider: providerName,
Name: fmt.Sprintf("%s: %s", providerName, model),
DefaultMaxTokens: 4096,
}
}
}
return m, nil
}
14 changes: 10 additions & 4 deletions internal/db/messages.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions internal/db/migrations/20250427135639_add_provider.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE messages ADD COLUMN provider TEXT DEFAULT '';
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
ALTER TABLE messages DROP COLUMN provider;
-- +goose StatementEnd
1 change: 1 addition & 0 deletions internal/db/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion internal/db/sql/messages.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ INSERT INTO messages (
role,
parts,
model,
provider,
created_at,
updated_at
) VALUES (
?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
?, ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
)
RETURNING *;

Expand Down
36 changes: 26 additions & 10 deletions internal/llm/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,10 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg
eventChan := a.provider.StreamResponse(ctx, msgHistory, a.tools)

assistantMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
Role: message.Assistant,
Parts: []message.ContentPart{},
Model: a.provider.Model().ID,
Role: message.Assistant,
Parts: []message.ContentPart{},
Model: a.provider.Model().ID,
Provider: a.provider.Model().Provider,
})
if err != nil {
return assistantMsg, nil, fmt.Errorf("failed to create assistant message: %w", err)
Expand Down Expand Up @@ -438,22 +439,37 @@ func (a *agent) TrackUsage(ctx context.Context, sessionID string, model models.M

func createAgentProvider(agentName config.AgentName) (provider.Provider, error) {
cfg := config.Get()
agentConfig, ok := cfg.Agents[agentName]
if !ok {
agentConfig, foundModel := cfg.Agents[agentName]
if !foundModel {
return nil, fmt.Errorf("agent %s not found", agentName)
}
model, ok := models.SupportedModels[agentConfig.Model]
if !ok {
return nil, fmt.Errorf("model %s not supported", agentConfig.Model)

if agentConfig.Model == "" {
return nil, fmt.Errorf("agent %s has no model configured", agentName)
}

providerCfg, ok := cfg.Providers[model.Provider]
if !ok {
model, err := config.GetModel(agentConfig.Model, agentConfig.Provider)
providerCfg, foundProvider := cfg.Providers[model.Provider]
if !foundProvider {
return nil, fmt.Errorf("provider %s not supported", model.Provider)
}
if providerCfg.Disabled {
return nil, fmt.Errorf("provider %s is not enabled", model.Provider)
}

// try to find the model in the provider config
if !foundModel {
model, foundModel = providerCfg.Models[agentConfig.Model]
// if not found create a simple model just based on the model id
if !foundModel {
model = models.Model{
ID: agentConfig.Model,
APIModel: string(agentConfig.Model),
Provider: model.Provider,
}
}
}

maxTokens := model.DefaultMaxTokens
if agentConfig.MaxTokens > 0 {
maxTokens = agentConfig.MaxTokens
Expand Down
7 changes: 7 additions & 0 deletions internal/llm/models/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Models

This package holds preconfigured models that users can use without adding manual configurations.

### Naming convention

To make sure that the IDs of the models do not clash with each other make sure to prepend the provider e.x `bedrock.*` when using a third party API not the main.
20 changes: 20 additions & 0 deletions internal/llm/models/bedrock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package models

const (
ProviderBedrock ModelProvider = "bedrock"

BedrockClaude37Sonnet ModelID = "bedrock.claude-3.7-sonnet"
)

var BedrockModels = map[ModelID]Model{
BedrockClaude37Sonnet: {
ID: BedrockClaude37Sonnet,
Name: "Bedrock: Claude 3.7 Sonnet",
Provider: ProviderBedrock,
APIModel: "anthropic.claude-3-7-sonnet-20250219-v1:0",
CostPer1MIn: 3.0,
CostPer1MInCached: 3.75,
CostPer1MOutCached: 0.30,
CostPer1MOut: 15.0,
},
}
Loading