Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
a1d2302
feat(provider): add lightweight model configuration and retrieval logic
aryasaatvik Jun 21, 2025
02a75e6
tui: generate api client
aryasaatvik Jun 21, 2025
e2a43a7
feat(provider): enhance model selection with lightweight model suppor…
aryasaatvik Jun 21, 2025
53d3873
fix(tui): update inactive selection styling to use accent color
aryasaatvik Jun 21, 2025
053ba20
refactor(tui): improve model initialization and rendering logic for b…
aryasaatvik Jun 21, 2025
581a36e
refactor(tui): rename lightweight model to turbo model and update rel…
aryasaatvik Jun 24, 2025
6be8dbd
feat: add configurable turbo model cost threshold
aryasaatvik Jun 24, 2025
f264cad
feat(tui): enhance model display with capability indicators and emoji…
aryasaatvik Jun 24, 2025
32d11d0
refactor(provider): comment out model cost initialization in anthropi…
aryasaatvik Jun 24, 2025
ac7c6e8
refactor(tui): streamline turbo model selection logic with new findTu…
aryasaatvik Jun 24, 2025
ec2462f
fix
aryasaatvik Jun 24, 2025
6d2d965
feat(provider): add release_date and last_updated fields to models fo…
aryasaatvik Jun 21, 2025
29e36ec
feat(dialog): implement sorting functionality for model selection wit…
aryasaatvik Jun 21, 2025
3e92d4b
fix(dialog): clean up whitespace and improve scroll indicators for tu…
aryasaatvik Jun 24, 2025
017b771
Merge branch 'dev' into feat/lightweight-model-configuration
aryasaatvik Jun 27, 2025
0089da8
refactor: simplify renderPane styling logic in modelDialog
aryasaatvik Jun 27, 2025
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
11 changes: 10 additions & 1 deletion packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@ export namespace Config {
"Model to use in the format of provider/model, eg anthropic/claude-2",
)
.optional(),
turbo_model: z
.string()
.describe("Turbo model to use for tasks like window title generation")
.optional(),
turbo_cost_threshold: z
.number()
.describe("Maximum output cost for a model to be considered a turbo model (default: 4)")
.default(4)
.optional(),
provider: z
.record(
ModelsDev.Provider.partial().extend({
Expand Down Expand Up @@ -220,7 +229,7 @@ export namespace Config {
)
await fs.unlink(path.join(Global.Path.config, "config"))
})
.catch(() => {})
.catch(() => { })

return result
})
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/src/provider/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ export namespace ModelsDev {
}),
id: z.string(),
options: z.record(z.any()),
release_date: z
.string()
.regex(/^\d{4}-\d{2}(-\d{2})?$/, {
message: "Must be in YYYY-MM or YYYY-MM-DD format",
})
.optional(),
last_updated: z
.string()
.regex(/^\d{4}-\d{2}(-\d{2})?$/, {
message: "Must be in YYYY-MM or YYYY-MM-DD format",
})
.optional(),
})
.openapi({
ref: "Model.Info",
Expand Down
58 changes: 51 additions & 7 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ export namespace Provider {
type Source = "env" | "config" | "custom" | "api"

const CUSTOM_LOADERS: Record<string, CustomLoader> = {
async anthropic(provider) {
async anthropic() {
const access = await AuthAnthropic.access()
if (!access) return { autoload: false }
for (const model of Object.values(provider.models)) {
model.cost = {
input: 0,
output: 0,
}
}
// for (const model of Object.values(provider.models)) {
// model.cost = {
// input: 0,
// output: 0,
// }
// }
return {
autoload: true,
options: {
Expand Down Expand Up @@ -395,6 +395,50 @@ export namespace Provider {
}
}

export async function isTurboModel(model: ModelsDev.Model): Promise<boolean> {
const cfg = await Config.get()
const threshold = cfg.turbo_cost_threshold ?? 4
return model.cost.output <= threshold
}

export async function getTurboModel(providerID: string): Promise<{ info: ModelsDev.Model; language: LanguageModel } | null> {
const cfg = await Config.get()

// Check user override
if (cfg.turbo_model) {
try {
// Parse the turbo model to get its provider
const { providerID: turboProviderID, modelID } = parseModel(cfg.turbo_model)
return await getModel(turboProviderID, modelID)
} catch (e) {
log.warn("Failed to get configured turbo model", { turbo_model: cfg.turbo_model, error: e })
}
}

const providers = await list()
const provider = providers[providerID]
if (!provider) return null

// Use configured threshold or default to 4
const threshold = cfg.turbo_cost_threshold ?? 4

// Select cheapest model whose cost.output <= threshold for turbo tasks
let selected: { info: ModelsDev.Model; language: LanguageModel } | null = null
for (const model of Object.values(provider.info.models)) {
if (model.cost.output <= threshold) {
try {
const m = await getModel(providerID, model.id)
if (!selected || m.info.cost.output < selected.info.cost.output) {
selected = m
}
} catch {
// ignore errors and continue searching
}
}
}
return selected
}

const TOOLS = [
BashTool,
EditTool,
Expand Down
110 changes: 83 additions & 27 deletions packages/tui/internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"path/filepath"
"sort"
"strings"
"time"

"log/slog"
Expand All @@ -23,24 +22,29 @@ import (
var RootPath string

type App struct {
Info client.AppInfo
Version string
StatePath string
Config *client.ConfigInfo
Client *client.ClientWithResponses
State *config.State
Provider *client.ProviderInfo
Model *client.ModelInfo
Session *client.SessionInfo
Messages []client.MessageInfo
Commands commands.CommandRegistry
Info client.AppInfo
Version string
StatePath string
Config *client.ConfigInfo
Client *client.ClientWithResponses
State *config.State
MainProvider *client.ProviderInfo
MainModel *client.ModelInfo
TurboProvider *client.ProviderInfo
TurboModel *client.ModelInfo
Session *client.SessionInfo
Messages []client.MessageInfo
Commands commands.CommandRegistry
}

type SessionSelectedMsg = *client.SessionInfo
type ModelSelectedMsg struct {
Provider client.ProviderInfo
Model client.ModelInfo
MainProvider client.ProviderInfo
MainModel client.ModelInfo
TurboProvider client.ProviderInfo
TurboModel client.ModelInfo
}

type SessionClearedMsg struct{}
type CompactSessionMsg struct{}
type SendMsg struct {
Expand Down Expand Up @@ -89,9 +93,10 @@ func New(
appState.Theme = *configInfo.Theme
}
if configInfo.Model != nil {
splits := strings.Split(*configInfo.Model, "/")
appState.Provider = splits[0]
appState.Model = strings.Join(splits[1:], "/")
appState.MainProvider, appState.MainModel = util.ParseModel(*configInfo.Model)
}
if configInfo.TurboModel != nil {
appState.TurboProvider, appState.TurboModel = util.ParseModel(*configInfo.TurboModel)
}

// Load themes from all directories
Expand Down Expand Up @@ -174,11 +179,11 @@ func (a *App) InitializeProvider() tea.Cmd {
var currentProvider *client.ProviderInfo
var currentModel *client.ModelInfo
for _, provider := range providers {
if provider.Id == a.State.Provider {
if provider.Id == a.State.MainProvider {
currentProvider = &provider

for _, model := range provider.Models {
if model.Id == a.State.Model {
if model.Id == a.State.MainModel {
currentModel = &model
}
}
Expand All @@ -189,10 +194,15 @@ func (a *App) InitializeProvider() tea.Cmd {
currentModel = defaultModel
}

// Initialize turbo model based on config or defaults
turboProvider, turboModel := findTurboModel(a.State, a.Config, providers, currentProvider, currentModel)

// TODO: handle no provider or model setup, yet
return ModelSelectedMsg{
Provider: *currentProvider,
Model: *currentModel,
MainProvider: *currentProvider,
MainModel: *currentModel,
TurboProvider: *turboProvider,
TurboModel: *turboModel,
}
}
}
Expand All @@ -209,6 +219,44 @@ func getDefaultModel(response *client.PostProviderListResponse, provider client.
return nil
}

func findTurboModel(state *config.State, config *client.ConfigInfo, providers []client.ProviderInfo, currentProvider *client.ProviderInfo, currentModel *client.ModelInfo) (*client.ProviderInfo, *client.ModelInfo) {
// If turbo model is configured in state, use it
if state.TurboProvider != "" && state.TurboModel != "" {
for _, provider := range providers {
if provider.Id == state.TurboProvider {
for _, model := range provider.Models {
if model.Id == state.TurboModel {
return &provider, &model
}
}
}
}
}

// Get threshold from config or use default
threshold := float32(4.0)
if config != nil && config.TurboCostThreshold != nil {
threshold = *config.TurboCostThreshold
}

// Find the cheapest model in the current provider that qualifies as turbo
var turboModel *client.ModelInfo
for _, model := range currentProvider.Models {
if model.Cost.Output <= threshold {
if turboModel == nil || model.Cost.Output < turboModel.Cost.Output {
tmp := model
turboModel = &tmp
}
}
}

// Return turbo model if found, otherwise fall back to main model
if turboModel != nil {
return currentProvider, turboModel
}
return currentProvider, currentModel
}

type Attachment struct {
FilePath string
FileName string
Expand Down Expand Up @@ -247,8 +295,8 @@ func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
go func() {
response, err := a.Client.PostSessionInitialize(ctx, client.PostSessionInitializeJSONRequestBody{
SessionID: a.Session.Id,
ProviderID: a.Provider.Id,
ModelID: a.Model.Id,
ProviderID: a.MainProvider.Id,
ModelID: a.MainModel.Id,
})
if err != nil {
slog.Error("Failed to initialize project", "error", err)
Expand All @@ -265,10 +313,18 @@ func (a *App) InitializeProject(ctx context.Context) tea.Cmd {

func (a *App) CompactSession(ctx context.Context) tea.Cmd {
go func() {
// Use turbo model for summarization if available
providerID := a.MainProvider.Id
modelID := a.MainModel.Id
if a.TurboProvider != nil && a.TurboModel != nil {
providerID = a.TurboProvider.Id
modelID = a.TurboModel.Id
}

response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{
SessionID: a.Session.Id,
ProviderID: a.Provider.Id,
ModelID: a.Model.Id,
ProviderID: providerID,
ModelID: modelID,
})
if err != nil {
slog.Error("Failed to compact session", "error", err)
Expand Down Expand Up @@ -345,8 +401,8 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
response, err := a.Client.PostSessionChat(ctx, client.PostSessionChatJSONRequestBody{
SessionID: a.Session.Id,
Parts: parts,
ProviderID: a.Provider.Id,
ModelID: a.Model.Id,
ProviderID: a.MainProvider.Id,
ModelID: a.MainModel.Id,
})
if err != nil {
errormsg := fmt.Sprintf("failed to send message: %v", err)
Expand Down
13 changes: 11 additions & 2 deletions packages/tui/internal/components/chat/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,17 @@ func (m *editorComponent) Content() string {
}

model := ""
if m.app.Model != nil {
model = muted(m.app.Provider.Name) + base(" "+m.app.Model.Name)
if m.app.MainModel != nil && m.app.MainProvider != nil {
model = muted(m.app.MainProvider.Name) + base(" "+m.app.MainModel.Name)

// show turbo model if configured
if m.app.TurboModel != nil && m.app.TurboProvider != nil {
if m.app.TurboProvider.Id == m.app.MainProvider.Id {
model = model + muted(" (⚡"+m.app.TurboModel.Name+")")
} else {
model = model + muted(" (⚡"+m.app.TurboProvider.Name+"/"+m.app.TurboModel.Name+")")
}
}
}

space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
Expand Down
Loading