feat: implement OpenRouter Free default and paid model authorization check

This commit is contained in:
Bernardo 2026-03-10 14:54:12 +01:00
parent 985289a3b7
commit d2366b4785
3 changed files with 100 additions and 3 deletions

View file

@ -664,8 +664,29 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
SendResponse: false,
}
// context-dependent commands check their own Runtime fields and report
// "unavailable" when the required capability is nil.
// Check for pending authorization response
if isYesResponse(msg.Content) {
if pending, _ := agent.Sessions.GetMetadata(opts.SessionKey, "pending_paid_auth"); pending == true {
agent.Sessions.SetMetadata(opts.SessionKey, "paid_model_authorized", true)
agent.Sessions.SetMetadata(opts.SessionKey, "pending_paid_auth", false)
// Retrieve the stashed message to resume
if stashed, ok := agent.Sessions.GetMetadata(opts.SessionKey, "stashed_user_message"); ok {
opts.UserMessage = stashed.(string)
// Clear stash
agent.Sessions.SetMetadata(opts.SessionKey, "stashed_user_message", nil)
al.bus.PublishOutbound(ctx, bus.OutboundMessage{
Channel: msg.Channel,
ChatID: msg.ChatID,
Content: "✅ Authorized! Processing your request with the advanced model now...",
})
} else {
return "✅ Authorized! DeepSeek and other paid models are now enabled for this session. What would you like me to do?", nil
}
}
}
if response, handled := al.handleCommand(ctx, msg, agent, &opts); handled {
return response, nil
}
@ -926,6 +947,20 @@ func (al *AgentLoop) runLLMIteration(
// tool chain doesn't switch models mid-way through.
activeCandidates, activeModel := al.selectCandidates(agent, opts.UserMessage, messages)
// Check for paid model authorization
if isPaidModel(activeModel) {
authorized, _ := agent.Sessions.GetMetadata(opts.SessionKey, "paid_model_authorized")
if authorized != true {
// Stash the message and ask for permission
agent.Sessions.SetMetadata(opts.SessionKey, "pending_paid_auth", true)
agent.Sessions.SetMetadata(opts.SessionKey, "stashed_user_message", opts.UserMessage)
agent.Sessions.Save(opts.SessionKey)
authMsg := fmt.Sprintf("🛡️ **Authorization Required**\n\nI've detected this task requires an advanced model (%s) which may incur costs. \n\nReply with **'yes'** or **'y'** to authorize this session, or I can continue with the free model if you prefer.", activeModel)
return authMsg, 0, nil
}
}
for iteration < agent.MaxIterations {
iteration++
@ -1865,3 +1900,22 @@ func extractParentPeer(msg bus.InboundMessage) *routing.RoutePeer {
}
return &routing.RoutePeer{Kind: parentKind, ID: parentID}
}
func isPaidModel(model string) bool {
m := strings.ToLower(model)
// Explicitly free models
if strings.Contains(m, "/free") || strings.HasPrefix(m, "ollama/") || strings.HasPrefix(m, "vllm/") {
return false
}
// Models on OpenRouter that are free or we want to treat as free
if strings.HasPrefix(m, "openrouter/free") {
return false
}
// Everything else (DeepSeek, Claude, GPT-4, etc.) is considered paid
return true
}
func isYesResponse(content string) bool {
c := strings.ToLower(strings.TrimSpace(content))
return c == "yes" || c == "y" || c == "ok" || c == "confirm" || c == "proceed"
}

View file

@ -29,7 +29,8 @@ func DefaultConfig() *Config {
Workspace: workspacePath,
RestrictToWorkspace: true,
Provider: "",
Model: "",
ModelName: "openrouter-free",
Model: "openrouter/free",
MaxTokens: 32768,
Temperature: nil, // nil means use provider default
MaxToolIterations: 10,
@ -248,6 +249,12 @@ func DefaultConfig() *Config {
},
// OpenRouter (100+ models) - https://openrouter.ai/keys
{
ModelName: "openrouter-free",
Model: "openrouter/free",
APIBase: "https://openrouter.ai/api/v1",
APIKey: "",
},
{
ModelName: "openrouter-auto",
Model: "openrouter/auto",

View file

@ -15,6 +15,7 @@ type Session struct {
Key string `json:"key"`
Messages []providers.Message `json:"messages"`
Summary string `json:"summary,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
@ -145,6 +146,41 @@ func (sm *SessionManager) TruncateHistory(key string, keepLast int) {
session.Updated = time.Now()
}
func (sm *SessionManager) SetMetadata(key string, metaKey string, value any) {
sm.mu.Lock()
defer sm.mu.Unlock()
session, ok := sm.sessions[key]
if !ok {
session = &Session{
Key: key,
Messages: []providers.Message{},
Metadata: make(map[string]any),
Created: time.Now(),
}
sm.sessions[key] = session
}
if session.Metadata == nil {
session.Metadata = make(map[string]any)
}
session.Metadata[metaKey] = value
session.Updated = time.Now()
}
func (sm *SessionManager) GetMetadata(key string, metaKey string) (any, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
session, ok := sm.sessions[key]
if !ok || session.Metadata == nil {
return nil, false
}
val, ok := session.Metadata[metaKey]
return val, ok
}
// sanitizeFilename converts a session key into a cross-platform safe filename.
// Session keys use "channel:chatID" (e.g. "telegram:123456") but ':' is the
// volume separator on Windows, so filepath.Base would misinterpret the key.