mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-26 16:19:02 +00:00
151 lines
4.1 KiB
Go
151 lines
4.1 KiB
Go
package caps
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// ChatOptions identifies a billable non-agent LLM call for usage-cap enforcement.
|
|
type ChatOptions struct {
|
|
TenantID uuid.UUID
|
|
AgentID uuid.UUID
|
|
ProviderName string
|
|
ModelID string
|
|
ReservationKey string
|
|
Purpose string
|
|
MaxOutputTokens int
|
|
}
|
|
|
|
// Chat wraps Provider.Chat with the same usage-cap preflight and reconciliation
|
|
// used by agent loops. A nil service intentionally falls back to direct calls
|
|
// for Lite/subscription-only runtimes.
|
|
func (s *Service) Chat(ctx context.Context, provider providers.Provider, req providers.ChatRequest, opts ChatOptions) (*providers.ChatResponse, error) {
|
|
if provider == nil {
|
|
return nil, errors.New("usage cap chat: provider is nil")
|
|
}
|
|
if s == nil || s.store == nil {
|
|
return provider.Chat(ctx, req)
|
|
}
|
|
if fallback, ok := provider.(*providers.ModelFallbackProvider); ok {
|
|
return fallback.ChatWithHook(ctx, req, func(callCtx context.Context, entry providers.FallbackCandidate, actualReq providers.ChatRequest) (providers.FallbackAfterCall, error) {
|
|
callOpts := opts
|
|
callOpts.ProviderName = entry.ProviderName
|
|
if callOpts.ProviderName == "" && entry.Provider != nil {
|
|
callOpts.ProviderName = entry.Provider.Name()
|
|
}
|
|
callOpts.ModelID = actualReq.Model
|
|
callOpts.ReservationKey = ""
|
|
usageReq := s.chatRequest(callCtx, entry.Provider, actualReq, callOpts)
|
|
scopedCtx := scopedRequestContext(callCtx, usageReq)
|
|
reservation, err := s.Preflight(scopedCtx, usageReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return func(resp *providers.ChatResponse, callErr error, _ providers.FallbackCallInfo) {
|
|
if reservation != nil {
|
|
reservation.Reconcile(scopedCtx, resp, callErr)
|
|
}
|
|
}, nil
|
|
})
|
|
}
|
|
|
|
usageReq := s.chatRequest(ctx, provider, req, opts)
|
|
scopedCtx := scopedRequestContext(ctx, usageReq)
|
|
reservation, err := s.Preflight(scopedCtx, usageReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := provider.Chat(scopedCtx, req)
|
|
if reservation != nil {
|
|
reservation.Reconcile(scopedCtx, resp, err)
|
|
}
|
|
return resp, err
|
|
}
|
|
|
|
func (s *Service) chatRequest(ctx context.Context, provider providers.Provider, req providers.ChatRequest, opts ChatOptions) Request {
|
|
tenantID := opts.TenantID
|
|
if tenantID == uuid.Nil {
|
|
tenantID = store.TenantIDFromContext(ctx)
|
|
}
|
|
if tenantID == uuid.Nil {
|
|
tenantID = store.MasterTenantID
|
|
}
|
|
agentID := opts.AgentID
|
|
if agentID == uuid.Nil {
|
|
agentID = store.AgentIDFromContext(ctx)
|
|
}
|
|
providerName := opts.ProviderName
|
|
if providerName == "" && provider != nil {
|
|
providerName = provider.Name()
|
|
}
|
|
modelID := opts.ModelID
|
|
if modelID == "" {
|
|
modelID = req.Model
|
|
}
|
|
if modelID == "" && provider != nil {
|
|
modelID = provider.DefaultModel()
|
|
}
|
|
return Request{
|
|
TenantID: tenantID,
|
|
AgentID: agentID,
|
|
ProviderName: providerName,
|
|
ModelID: modelID,
|
|
ReservationKey: reservationKey(opts),
|
|
Messages: req.Messages,
|
|
MaxOutputTokens: maxOutputTokens(req, opts.MaxOutputTokens),
|
|
}
|
|
}
|
|
|
|
func scopedRequestContext(ctx context.Context, req Request) context.Context {
|
|
if req.TenantID != uuid.Nil && store.TenantIDFromContext(ctx) != req.TenantID {
|
|
ctx = store.WithTenantID(ctx, req.TenantID)
|
|
}
|
|
if req.AgentID != uuid.Nil && store.AgentIDFromContext(ctx) != req.AgentID {
|
|
ctx = store.WithAgentID(ctx, req.AgentID)
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
func reservationKey(opts ChatOptions) string {
|
|
if opts.ReservationKey != "" {
|
|
return opts.ReservationKey
|
|
}
|
|
purpose := opts.Purpose
|
|
if purpose == "" {
|
|
purpose = "llm"
|
|
}
|
|
return fmt.Sprintf("%s:%s", purpose, uuid.NewString())
|
|
}
|
|
|
|
func maxOutputTokens(req providers.ChatRequest, fallback int) int {
|
|
if fallback <= 0 {
|
|
fallback = 1024
|
|
}
|
|
if req.Options == nil {
|
|
return fallback
|
|
}
|
|
v, ok := req.Options[providers.OptMaxTokens]
|
|
if !ok {
|
|
return fallback
|
|
}
|
|
switch n := v.(type) {
|
|
case int:
|
|
return n
|
|
case int64:
|
|
return int(n)
|
|
case int32:
|
|
return int(n)
|
|
case float64:
|
|
return int(n)
|
|
case float32:
|
|
return int(n)
|
|
default:
|
|
return fallback
|
|
}
|
|
}
|