Skip to content

Commit ee5243a

Browse files
authored
refactor(hawk): consume runtime-owned Eyrie transport selection (#77)
* refactor(hawk): consume runtime-owned eyrie transport selection * refactor(hawk): remove dead config cache helper * style(hawk): normalize runtime gateway wrappers * fix(hawk): preserve setup status hint contract * test(hawk): isolate version globals in cmd tests * fix(hawk): preserve explicit model selection boundary * fix(hawk): require explicit model before chat
1 parent dd7aef5 commit ee5243a

44 files changed

Lines changed: 438 additions & 653 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎cmd/chat.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ func newChatModel(ref *progRef, systemPrompt string, settings hawkconfig.Setting
138138
startup.EndPhase("newChatModel:newHawkSession")
139139

140140
startup.MarkPhase("newChatModel:configureSession")
141-
syncSessionFromPersistedSelection(sess, settings)
141+
syncSessionFromPersistedSelection(sess)
142142
sess.SetLogger(logger.New(io.Discard, logger.Error))
143143
if cfgErr := configureSession(sess, settings); cfgErr != nil {
144144
return chatModel{}, cfgErr

‎cmd/chat_commands_config.go‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@ func (m *chatModel) handleConfigCommand(parts []string, text string) (tea.Model,
1717
m.messages = append(m.messages, displayMsg{role: "error", content: err.Error()})
1818
return m, nil
1919
}
20-
engineProvider := hawkconfig.NormalizeProviderForEngine(value)
21-
m.session.SetProvider(engineProvider)
20+
m.syncSessionSelection()
2221
// Use cached model or set first from cache
2322
modelCacheMu.RLock()
24-
cached, cacheHit := modelCache[engineProvider]
23+
cached, cacheHit := modelCache[m.session.Provider()]
2524
modelCacheMu.RUnlock()
2625
if cacheHit && len(cached) > 0 {
2726
m.session.SetModel(cached[0].ID)
@@ -52,8 +51,8 @@ func (m *chatModel) handleConfigCommand(parts []string, text string) (tea.Model,
5251
m.messages = append(m.messages, displayMsg{role: "error", content: err.Error()})
5352
return m, nil
5453
}
55-
m.session.SetModel(value)
56-
m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf("Model switched to: %s\nSaved in eyrie (provider.json).", value)})
54+
m.syncSessionSelection()
55+
m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf("Model switched to: %s\nSaved in eyrie (provider.json).", m.session.Model())})
5756
return m, nil
5857
}
5958
if len(parts) >= 2 && parts[1] == "keys" {
@@ -95,9 +94,9 @@ func (m *chatModel) handleConfigCommand(parts []string, text string) (tea.Model,
9594
normalizedKey := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(key, "-", ""), "_", ""))
9695
switch normalizedKey {
9796
case "model":
98-
m.session.SetModel(value)
97+
m.syncSessionSelection()
9998
case "provider":
100-
m.session.SetProvider(hawkconfig.NormalizeProviderForEngine(value))
99+
m.syncSessionSelection()
101100
}
102101
m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf("Updated %s = %s", key, value)})
103102
return m, nil

‎cmd/chat_commands_test.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func TestHandleCommandAddDir(t *testing.T) {
5252
}
5353

5454
func TestLocalSlashCommands(t *testing.T) {
55+
preserveCLICompilerVersionState(t)
5556
version = "test-version"
5657
sess := engine.NewSession("openai", "gpt-4o", "base", tool.NewRegistry(tool.LSTool{}))
5758
m := &chatModel{
@@ -80,6 +81,7 @@ func TestLocalSlashCommands(t *testing.T) {
8081
}
8182

8283
func TestDiagnosticSummaries(t *testing.T) {
84+
preserveCLICompilerVersionState(t)
8385
version = "test-version"
8486
settings := hawkconfig.Settings{
8587
Provider: "openai",

‎cmd/chat_config_cache.go‎

Lines changed: 0 additions & 11 deletions
This file was deleted.

‎cmd/chat_config_deployment.go‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ func (m chatModel) handleConfigApplyCredentialsMsg(msg configApplyCredentialsMsg
202202
}
203203
if msg.providerID == configProviderOllama {
204204
_ = hawkconfig.SetGlobalSetting("provider", configProviderOllama)
205-
next.session.SetProvider(hawkconfig.NormalizeProviderForEngine(configProviderOllama))
205+
next.syncSessionSelection()
206206
}
207207
next.configGuideAfterKey = false
208208
if len(msg.modelOptions) == 0 {
@@ -221,10 +221,14 @@ func (m chatModel) handleConfigApplyCredentialsMsg(msg configApplyCredentialsMsg
221221
}
222222

223223
func (m chatModel) rebuildSessionTransport() (chatModel, tea.Cmd) {
224-
if err := engine.RebuildSessionTransport(context.Background(), m.session, hawkconfig.DeploymentRoutingEnabled(m.settings), m.session.Provider()); err != nil {
224+
selection := runtime.EffectiveSelection(context.Background(), runtime.SelectionOpts{
225+
ProviderOverride: firstNonEmptyTrimmed(m.session.Provider(), m.settings.Provider),
226+
ModelOverride: firstNonEmptyTrimmed(m.session.Model(), m.settings.Model),
227+
})
228+
if err := engine.RebuildSessionTransport(context.Background(), m.session, selection, m.session.Provider()); err != nil {
225229
m.configNotice = sanitizeConfigNotice(err.Error())
226230
}
227-
syncSessionFromPersistedSelection(m.session, m.settings)
231+
syncSessionFromPersistedSelection(m.session)
228232
m.invalidateConnStatus()
229233
return m, nil
230234
}

‎cmd/chat_config_gateways.go‎

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ import (
1212
)
1313

1414
type configGatewayRow struct {
15-
ID string
16-
DisplayName string
17-
HasKey bool
18-
ModelCount int
19-
Active bool
15+
ID string
16+
DisplayName string
17+
HasKey bool
18+
ModelCount int
19+
Active bool
20+
RegionLabel string
21+
RegionRequired bool
2022
}
2123

2224
type configGatewayRefreshMsg struct {
@@ -27,47 +29,52 @@ type configGatewayRefreshMsg struct {
2729

2830
func (m chatModel) configGatewayRows() []configGatewayRow {
2931
ctx := context.Background()
30-
providers := hawkconfig.AllSetupGateways()
31-
configured := configuredGatewayKeys()
3232
active := strings.TrimSpace(m.configModelProvider)
33+
activeModel := ""
3334
if active == "" && m.session != nil {
3435
active = strings.TrimSpace(m.session.Provider())
3536
}
36-
var rows []configGatewayRow
37-
for _, id := range providers {
38-
if id == "" {
37+
if m.session != nil {
38+
activeModel = strings.TrimSpace(m.session.Model())
39+
}
40+
statuses := hawkconfig.GatewayStatuses(ctx, active, activeModel)
41+
rows := make([]configGatewayRow, 0, len(statuses))
42+
for _, status := range statuses {
43+
if status.ID == "" {
3944
continue
4045
}
41-
count := hawkconfig.CachedModelCountForProvider(id)
46+
count := status.ModelCount
4247
if count == 0 {
4348
modelCacheMu.RLock()
44-
if cached, ok := modelCache[id]; ok {
49+
if cached, ok := modelCache[status.ID]; ok {
4550
count = len(cached)
4651
}
4752
modelCacheMu.RUnlock()
4853
}
49-
hasKey := configured[id] || hawkconfig.HasStoredCredentialForProvider(ctx, id)
50-
display := hawkconfig.GatewayDisplayName(id)
51-
if id == hawkconfig.ProviderXiaomiTokenPlan {
52-
if reg := hawkconfig.XiaomiTokenPlanRegionLabel(); reg != "" {
54+
hasKey := status.HasStoredCredential
55+
display := status.DisplayName
56+
if status.ID == hawkconfig.ProviderXiaomiTokenPlan {
57+
if reg := status.RegionLabel; reg != "" {
5358
display += " · " + reg
5459
} else {
5560
display += " · region required"
5661
}
5762
}
58-
if id == hawkconfig.ProviderZAICoding {
59-
if reg := hawkconfig.ZAIRegionLabel(id); reg != "" {
63+
if status.ID == hawkconfig.ProviderZAICoding {
64+
if reg := status.RegionLabel; reg != "" {
6065
display += " · " + reg
6166
} else {
6267
display += " · region"
6368
}
6469
}
6570
rows = append(rows, configGatewayRow{
66-
ID: id,
67-
DisplayName: display,
68-
HasKey: hasKey,
69-
ModelCount: count,
70-
Active: hawkconfig.NormalizeProviderForEngine(id) == hawkconfig.NormalizeProviderForEngine(active),
71+
ID: status.ID,
72+
DisplayName: display,
73+
HasKey: hasKey,
74+
ModelCount: count,
75+
Active: status.Active || hawkconfig.ActiveProviderID(status.ID) == hawkconfig.ActiveProviderID(active),
76+
RegionLabel: status.RegionLabel,
77+
RegionRequired: status.RegionRequired,
7178
})
7279
}
7380
return rows
@@ -117,11 +124,11 @@ func (m chatModel) refreshConfigGateway() (chatModel, tea.Cmd) {
117124
}
118125
idx := m.configGatewayRefreshTargetIndex(rows)
119126
row := rows[idx]
120-
if row.ID == hawkconfig.ProviderXiaomiTokenPlan && hawkconfig.NeedsXiaomiTokenPlanRegion(row.ID) {
127+
if row.ID == hawkconfig.ProviderXiaomiTokenPlan && row.RegionRequired {
121128
m.configNotice = "Pick Token Plan region (cn / sgp / ams) before refresh"
122129
return m.startConfigXiaomiTokenPlanRegion(), nil
123130
}
124-
if row.ID == hawkconfig.ProviderZAICoding && hawkconfig.NeedsZAIRegion(row.ID) {
131+
if row.ID == hawkconfig.ProviderZAICoding && row.RegionRequired {
125132
m.configNotice = "Pick Coding Plan region (international / cn) before refresh"
126133
return m.startConfigZAIRegion(row.ID), nil
127134
}
@@ -274,12 +281,12 @@ func (m chatModel) handleConfigGatewaysSelect() (chatModel, tea.Cmd) {
274281
}
275282
row := rows[m.configSel]
276283
if row.ID == hawkconfig.ProviderXiaomiTokenPlan {
277-
if !row.HasKey || hawkconfig.NeedsXiaomiTokenPlanRegion(row.ID) {
284+
if !row.HasKey || row.RegionRequired {
278285
m.configGatewayFocus = m.configSel
279286
return m.startConfigXiaomiTokenPlanRegion(), nil
280287
}
281288
}
282-
if row.ID == hawkconfig.ProviderZAICoding && (!row.HasKey || hawkconfig.NeedsZAIRegion(row.ID)) {
289+
if row.ID == hawkconfig.ProviderZAICoding && (!row.HasKey || row.RegionRequired) {
283290
m.configGatewayFocus = m.configSel
284291
return m.startConfigZAIRegion(row.ID), nil
285292
}
@@ -295,7 +302,9 @@ func (m chatModel) handleConfigGatewaysSelect() (chatModel, tea.Cmd) {
295302
m.configGatewayFocus = m.configSel
296303
m.configModelProvider = gw
297304
_ = hawkconfig.SetGlobalSetting("provider", gw)
298-
m.session.SetProvider(hawkconfig.NormalizeProviderForEngine(gw))
305+
if active := hawkconfig.ActiveProvider(context.Background()); active != "" {
306+
m.session.SetProvider(active)
307+
}
299308
m.configTab = configTabModels
300309
m.configSel = 0
301310
m.configScroll = 0
@@ -343,7 +352,15 @@ func (m chatModel) trackConfigGatewayFocus() chatModel {
343352
if m.configTab != configTabGateways {
344353
return m
345354
}
346-
rows := len(hawkconfig.AllSetupGateways())
355+
active := strings.TrimSpace(m.configModelProvider)
356+
activeModel := ""
357+
if m.session != nil {
358+
if active == "" {
359+
active = strings.TrimSpace(m.session.Provider())
360+
}
361+
activeModel = strings.TrimSpace(m.session.Model())
362+
}
363+
rows := len(hawkconfig.GatewayStatuses(context.Background(), active, activeModel))
347364
if m.configSel >= 0 && m.configSel < rows {
348365
m.configGatewayFocus = m.configSel
349366
}

‎cmd/chat_config_panel.go‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -697,11 +697,10 @@ func (m chatModel) selectConfigModelFromOptions(opts []configModelOption) (chatM
697697
m.session.SetModel(modelID)
698698
if gw := strings.TrimSpace(m.configModelProvider); gw != "" {
699699
_ = hawkconfig.SetGlobalSetting("provider", gw)
700-
m.session.SetProvider(hawkconfig.NormalizeProviderForEngine(gw))
701700
} else if prov := hawkconfig.ProviderOfModel(modelID); prov != "" {
702701
_ = hawkconfig.SetGlobalSetting("provider", prov)
703-
m.session.SetProvider(hawkconfig.NormalizeProviderForEngine(prov))
704702
}
703+
m.syncSessionSelection()
705704
next, cmd := m.rebuildSessionTransport()
706705
next.invalidateConnStatus()
707706
next = next.stopConfigModelSearch(true)

‎cmd/chat_status.go‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"strings"
77

8+
"github.com/GrayCodeAI/eyrie/runtime"
89
"github.com/charmbracelet/lipgloss"
910

1011
hawkconfig "github.com/GrayCodeAI/hawk/internal/config"
@@ -76,11 +77,25 @@ func (m chatModel) sessionGatewayModel() (gateway, model string) {
7677
}
7778
if gateway == "" || model == "" {
7879
ctx := context.Background()
79-
if gateway == "" {
80-
gateway = hawkconfig.ActiveGateway(ctx)
81-
}
82-
if model == "" {
83-
model = strings.TrimSpace(hawkconfig.ActiveModel(ctx))
80+
explicitGateway, explicitModel := explicitSelection(ctx)
81+
if explicitGateway != "" || explicitModel != "" {
82+
if gateway == "" {
83+
gateway = explicitGateway
84+
}
85+
if model == "" {
86+
model = explicitModel
87+
}
88+
} else {
89+
selection := runtime.EffectiveSelection(ctx, runtime.SelectionOpts{
90+
ProviderOverride: gateway,
91+
ModelOverride: model,
92+
})
93+
if gateway == "" {
94+
gateway = strings.TrimSpace(selection.Provider)
95+
}
96+
if model == "" {
97+
model = strings.TrimSpace(selection.Model)
98+
}
8499
}
85100
}
86101
return gateway, model

‎cmd/chat_status_test.go‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,11 @@ func TestChatConnectionStatus_NoGatewayNoModel(t *testing.T) {
163163

164164
m := chatModel{session: &engine.Session{}}
165165
got := m.chatConnectionStatus()
166-
if got != "pick model" {
167-
t.Fatalf("status = %q", got)
166+
if !strings.Contains(got, "Anthropic · ") {
167+
t.Fatalf("expected runtime-selected anthropic status, got %q", got)
168+
}
169+
if strings.Contains(got, "pick model") {
170+
t.Fatalf("expected auto-selected model when only anthropic credentials exist, got %q", got)
168171
}
169172
}
170173

‎cmd/chat_subcommand_simple.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,10 @@ func init() {
121121
handler: func(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) {
122122
savedModel := hawkconfig.ActiveModel(context.Background())
123123
if m.session.Model() == savedModel {
124-
norm := hawkconfig.NormalizeProviderForEngine(m.session.Provider())
125-
fastModel := hawkconfig.CheapestModelForProvider(norm, m.session.Model())
124+
providerName := strings.TrimSpace(m.session.Provider())
125+
fastModel := hawkconfig.CheapestModelForProvider(providerName, m.session.Model())
126126
if strings.TrimSpace(fastModel) == "" {
127-
fastModel = hawkconfig.DefaultModelForProvider(norm)
127+
fastModel = hawkconfig.DefaultModelForProvider(providerName)
128128
}
129129
if strings.TrimSpace(fastModel) == "" {
130130
m.messages = append(m.messages, displayMsg{role: "error", content: "Fast mode: no catalog model resolved for this provider"})

0 commit comments

Comments
 (0)