style: fix formatting issues and remove unused constants
This commit addresses the GitHub Actions CI failures caused by the linters: - `golines`: Fixed line length wrapping issues in multiple files (`pkg/routing/router_test.go`, `pkg/providers/vertex/provider.go`, etc.) - `gci`: Reordered and grouped imports appropriately in modified files - `unused`: Removed `defaultRequestTimeout` and `defaultThreshold` constants which were no longer being used anywhere in the code. Co-authored-by: TanLuong <28281768+TanLuong@users.noreply.github.com>
This commit is contained in:
parent
e41b2722d6
commit
5d86ea43dc
9 changed files with 354 additions and 125 deletions
|
|
@ -164,7 +164,10 @@ func registerSharedTools(
|
||||||
|
|
||||||
if cfg.Tools.IsToolEnabled("web") {
|
if cfg.Tools.IsToolEnabled("web") {
|
||||||
searchTool, err := tools.NewWebSearchTool(tools.WebSearchToolOptions{
|
searchTool, err := tools.NewWebSearchTool(tools.WebSearchToolOptions{
|
||||||
BraveAPIKeys: config.MergeAPIKeys(cfg.Tools.Web.Brave.APIKey(), cfg.Tools.Web.Brave.APIKeys()),
|
BraveAPIKeys: config.MergeAPIKeys(
|
||||||
|
cfg.Tools.Web.Brave.APIKey(),
|
||||||
|
cfg.Tools.Web.Brave.APIKeys(),
|
||||||
|
),
|
||||||
BraveMaxResults: cfg.Tools.Web.Brave.MaxResults,
|
BraveMaxResults: cfg.Tools.Web.Brave.MaxResults,
|
||||||
BraveEnabled: cfg.Tools.Web.Brave.Enabled,
|
BraveEnabled: cfg.Tools.Web.Brave.Enabled,
|
||||||
TavilyAPIKeys: config.MergeAPIKeys(
|
TavilyAPIKeys: config.MergeAPIKeys(
|
||||||
|
|
@ -197,7 +200,11 @@ func registerSharedTools(
|
||||||
Proxy: cfg.Tools.Web.Proxy,
|
Proxy: cfg.Tools.Web.Proxy,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("agent", "Failed to create web search tool", map[string]any{"error": err.Error()})
|
logger.ErrorCF(
|
||||||
|
"agent",
|
||||||
|
"Failed to create web search tool",
|
||||||
|
map[string]any{"error": err.Error()},
|
||||||
|
)
|
||||||
} else if searchTool != nil {
|
} else if searchTool != nil {
|
||||||
agent.Tools.Register(searchTool)
|
agent.Tools.Register(searchTool)
|
||||||
}
|
}
|
||||||
|
|
@ -210,7 +217,11 @@ func registerSharedTools(
|
||||||
cfg.Tools.Web.FetchLimitBytes,
|
cfg.Tools.Web.FetchLimitBytes,
|
||||||
cfg.Tools.Web.PrivateHostWhitelist)
|
cfg.Tools.Web.PrivateHostWhitelist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("agent", "Failed to create web fetch tool", map[string]any{"error": err.Error()})
|
logger.ErrorCF(
|
||||||
|
"agent",
|
||||||
|
"Failed to create web fetch tool",
|
||||||
|
map[string]any{"error": err.Error()},
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
agent.Tools.Register(fetchTool)
|
agent.Tools.Register(fetchTool)
|
||||||
}
|
}
|
||||||
|
|
@ -478,7 +489,12 @@ func (al *AgentLoop) Run(ctx context.Context) error {
|
||||||
"queue_depth": al.pendingSteeringCountForScope(target.SessionKey),
|
"queue_depth": al.pendingSteeringCountForScope(target.SessionKey),
|
||||||
})
|
})
|
||||||
|
|
||||||
continued, continueErr := al.Continue(ctx, target.SessionKey, target.Channel, target.ChatID)
|
continued, continueErr := al.Continue(
|
||||||
|
ctx,
|
||||||
|
target.SessionKey,
|
||||||
|
target.Channel,
|
||||||
|
target.ChatID,
|
||||||
|
)
|
||||||
if continueErr != nil {
|
if continueErr != nil {
|
||||||
logger.WarnCF("agent", "Failed to continue queued steering",
|
logger.WarnCF("agent", "Failed to continue queued steering",
|
||||||
map[string]any{
|
map[string]any{
|
||||||
|
|
@ -506,14 +522,22 @@ func (al *AgentLoop) Run(ctx context.Context) error {
|
||||||
"queue_depth": al.pendingSteeringCountForScope(target.SessionKey),
|
"queue_depth": al.pendingSteeringCountForScope(target.SessionKey),
|
||||||
})
|
})
|
||||||
|
|
||||||
continued, continueErr := al.Continue(ctx, target.SessionKey, target.Channel, target.ChatID)
|
continued, continueErr := al.Continue(
|
||||||
|
ctx,
|
||||||
|
target.SessionKey,
|
||||||
|
target.Channel,
|
||||||
|
target.ChatID,
|
||||||
|
)
|
||||||
if continueErr != nil {
|
if continueErr != nil {
|
||||||
logger.WarnCF("agent", "Failed to continue queued steering after shutdown drain",
|
logger.WarnCF(
|
||||||
|
"agent",
|
||||||
|
"Failed to continue queued steering after shutdown drain",
|
||||||
map[string]any{
|
map[string]any{
|
||||||
"channel": target.Channel,
|
"channel": target.Channel,
|
||||||
"chat_id": target.ChatID,
|
"chat_id": target.ChatID,
|
||||||
"error": continueErr.Error(),
|
"error": continueErr.Error(),
|
||||||
})
|
},
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if continued == "" {
|
if continued == "" {
|
||||||
|
|
@ -572,11 +596,15 @@ func (al *AgentLoop) drainBusToSteering(ctx context.Context, activeScope, active
|
||||||
msgScope, _, scopeOK := al.resolveSteeringTarget(msg)
|
msgScope, _, scopeOK := al.resolveSteeringTarget(msg)
|
||||||
if !scopeOK || msgScope != activeScope {
|
if !scopeOK || msgScope != activeScope {
|
||||||
if err := al.requeueInboundMessage(msg); err != nil {
|
if err := al.requeueInboundMessage(msg); err != nil {
|
||||||
logger.WarnCF("agent", "Failed to requeue non-steering inbound message", map[string]any{
|
logger.WarnCF(
|
||||||
|
"agent",
|
||||||
|
"Failed to requeue non-steering inbound message",
|
||||||
|
map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
"channel": msg.Channel,
|
"channel": msg.Channel,
|
||||||
"sender_id": msg.SenderID,
|
"sender_id": msg.SenderID,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -610,7 +638,10 @@ func (al *AgentLoop) Stop() {
|
||||||
al.running.Store(false)
|
al.running.Store(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (al *AgentLoop) publishResponseIfNeeded(ctx context.Context, channel, chatID, response string) {
|
func (al *AgentLoop) publishResponseIfNeeded(
|
||||||
|
ctx context.Context,
|
||||||
|
channel, chatID, response string,
|
||||||
|
) {
|
||||||
if response == "" {
|
if response == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -1060,7 +1091,10 @@ var audioAnnotationRe = regexp.MustCompile(`\[(voice|audio)(?::[^\]]*)?\]`)
|
||||||
// transcribeAudioInMessage resolves audio media refs, transcribes them, and
|
// transcribeAudioInMessage resolves audio media refs, transcribes them, and
|
||||||
// replaces audio annotations in msg.Content with the transcribed text.
|
// replaces audio annotations in msg.Content with the transcribed text.
|
||||||
// Returns the (possibly modified) message and true if audio was transcribed.
|
// Returns the (possibly modified) message and true if audio was transcribed.
|
||||||
func (al *AgentLoop) transcribeAudioInMessage(ctx context.Context, msg bus.InboundMessage) (bus.InboundMessage, bool) {
|
func (al *AgentLoop) transcribeAudioInMessage(
|
||||||
|
ctx context.Context,
|
||||||
|
msg bus.InboundMessage,
|
||||||
|
) (bus.InboundMessage, bool) {
|
||||||
if al.transcriber == nil || al.mediaStore == nil || len(msg.Media) == 0 {
|
if al.transcriber == nil || al.mediaStore == nil || len(msg.Media) == 0 {
|
||||||
return msg, false
|
return msg, false
|
||||||
}
|
}
|
||||||
|
|
@ -1070,7 +1104,11 @@ func (al *AgentLoop) transcribeAudioInMessage(ctx context.Context, msg bus.Inbou
|
||||||
for _, ref := range msg.Media {
|
for _, ref := range msg.Media {
|
||||||
path, meta, err := al.mediaStore.ResolveWithMeta(ref)
|
path, meta, err := al.mediaStore.ResolveWithMeta(ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WarnCF("voice", "Failed to resolve media ref", map[string]any{"ref": ref, "error": err})
|
logger.WarnCF(
|
||||||
|
"voice",
|
||||||
|
"Failed to resolve media ref",
|
||||||
|
map[string]any{"ref": ref, "error": err},
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !utils.IsAudioFile(meta.Filename, meta.ContentType) {
|
if !utils.IsAudioFile(meta.Filename, meta.ContentType) {
|
||||||
|
|
@ -1148,7 +1186,11 @@ func (al *AgentLoop) sendTranscriptionFeedback(
|
||||||
ReplyToMessageID: messageID,
|
ReplyToMessageID: messageID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WarnCF("voice", "Failed to send transcription feedback", map[string]any{"error": err.Error()})
|
logger.WarnCF(
|
||||||
|
"voice",
|
||||||
|
"Failed to send transcription feedback",
|
||||||
|
map[string]any{"error": err.Error()},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1348,7 +1390,9 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||||
return al.runAgentLoop(ctx, agent, opts)
|
return al.runAgentLoop(ctx, agent, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (al *AgentLoop) resolveMessageRoute(msg bus.InboundMessage) (routing.ResolvedRoute, *AgentInstance, error) {
|
func (al *AgentLoop) resolveMessageRoute(
|
||||||
|
msg bus.InboundMessage,
|
||||||
|
) (routing.ResolvedRoute, *AgentInstance, error) {
|
||||||
registry := al.GetRegistry()
|
registry := al.GetRegistry()
|
||||||
route := registry.ResolveRoute(routing.RouteInput{
|
route := registry.ResolveRoute(routing.RouteInput{
|
||||||
Channel: msg.Channel,
|
Channel: msg.Channel,
|
||||||
|
|
@ -1364,7 +1408,10 @@ func (al *AgentLoop) resolveMessageRoute(msg bus.InboundMessage) (routing.Resolv
|
||||||
agent = registry.GetDefaultAgent()
|
agent = registry.GetDefaultAgent()
|
||||||
}
|
}
|
||||||
if agent == nil {
|
if agent == nil {
|
||||||
return routing.ResolvedRoute{}, nil, fmt.Errorf("no agent available for route (agent_id=%s)", route.AgentID)
|
return routing.ResolvedRoute{}, nil, fmt.Errorf(
|
||||||
|
"no agent available for route (agent_id=%s)",
|
||||||
|
route.AgentID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return route, agent, nil
|
return route, agent, nil
|
||||||
|
|
@ -2650,12 +2697,15 @@ turnLoop:
|
||||||
}
|
}
|
||||||
|
|
||||||
if steerMsgs := al.dequeueSteeringMessagesForScope(ts.sessionKey); len(steerMsgs) > 0 {
|
if steerMsgs := al.dequeueSteeringMessagesForScope(ts.sessionKey); len(steerMsgs) > 0 {
|
||||||
logger.InfoCF("agent", "Steering arrived after turn completion; continuing turn before finalizing",
|
logger.InfoCF(
|
||||||
|
"agent",
|
||||||
|
"Steering arrived after turn completion; continuing turn before finalizing",
|
||||||
map[string]any{
|
map[string]any{
|
||||||
"agent_id": ts.agent.ID,
|
"agent_id": ts.agent.ID,
|
||||||
"steering_count": len(steerMsgs),
|
"steering_count": len(steerMsgs),
|
||||||
"session_key": ts.sessionKey,
|
"session_key": ts.sessionKey,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
pendingMessages = append(pendingMessages, steerMsgs...)
|
pendingMessages = append(pendingMessages, steerMsgs...)
|
||||||
finalContent = ""
|
finalContent = ""
|
||||||
goto turnLoop
|
goto turnLoop
|
||||||
|
|
@ -2773,17 +2823,24 @@ func (al *AgentLoop) selectCandidates(
|
||||||
return tierCands, resolvedCandidateModel(tierCands, targetModelName)
|
return tierCands, resolvedCandidateModel(tierCands, targetModelName)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.WarnCF("agent", "Model routing: tier model candidates not found, falling back to primary",
|
logger.WarnCF(
|
||||||
|
"agent",
|
||||||
|
"Model routing: tier model candidates not found, falling back to primary",
|
||||||
map[string]any{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"tier_model": targetModelName,
|
"tier_model": targetModelName,
|
||||||
"score": score,
|
"score": score,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
return agent.Candidates, resolvedCandidateModel(agent.Candidates, agent.Model)
|
return agent.Candidates, resolvedCandidateModel(agent.Candidates, agent.Model)
|
||||||
}
|
}
|
||||||
|
|
||||||
// maybeSummarize triggers summarization if the session history exceeds thresholds.
|
// maybeSummarize triggers summarization if the session history exceeds thresholds.
|
||||||
func (al *AgentLoop) maybeSummarize(agent *AgentInstance, sessionKey string, turnScope turnEventScope) {
|
func (al *AgentLoop) maybeSummarize(
|
||||||
|
agent *AgentInstance,
|
||||||
|
sessionKey string,
|
||||||
|
turnScope turnEventScope,
|
||||||
|
) {
|
||||||
newHistory := agent.Sessions.GetHistory(sessionKey)
|
newHistory := agent.Sessions.GetHistory(sessionKey)
|
||||||
tokenEstimate := al.estimateTokens(newHistory)
|
tokenEstimate := al.estimateTokens(newHistory)
|
||||||
threshold := agent.ContextWindow * agent.SummarizeTokenPercent / 100
|
threshold := agent.ContextWindow * agent.SummarizeTokenPercent / 100
|
||||||
|
|
@ -2817,7 +2874,10 @@ type compressionResult struct {
|
||||||
// prompt is built dynamically by BuildMessages and is NOT stored here.
|
// prompt is built dynamically by BuildMessages and is NOT stored here.
|
||||||
// The compression note is recorded in the session summary so that
|
// The compression note is recorded in the session summary so that
|
||||||
// BuildMessages can include it in the next system prompt.
|
// BuildMessages can include it in the next system prompt.
|
||||||
func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) (compressionResult, bool) {
|
func (al *AgentLoop) forceCompression(
|
||||||
|
agent *AgentInstance,
|
||||||
|
sessionKey string,
|
||||||
|
) (compressionResult, bool) {
|
||||||
history := agent.Sessions.GetHistory(sessionKey)
|
history := agent.Sessions.GetHistory(sessionKey)
|
||||||
if len(history) <= 2 {
|
if len(history) <= 2 {
|
||||||
return compressionResult{}, false
|
return compressionResult{}, false
|
||||||
|
|
@ -2970,7 +3030,11 @@ func formatToolsForLog(toolDefs []providers.ToolDefinition) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// summarizeSession summarizes the conversation history for a session.
|
// summarizeSession summarizes the conversation history for a session.
|
||||||
func (al *AgentLoop) summarizeSession(agent *AgentInstance, sessionKey string, turnScope turnEventScope) {
|
func (al *AgentLoop) summarizeSession(
|
||||||
|
agent *AgentInstance,
|
||||||
|
sessionKey string,
|
||||||
|
turnScope turnEventScope,
|
||||||
|
) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
|
@ -3322,7 +3386,10 @@ func (al *AgentLoop) applyExplicitSkillCommand(
|
||||||
|
|
||||||
skillName, ok := agent.ContextBuilder.ResolveSkillName(arg)
|
skillName, ok := agent.ContextBuilder.ResolveSkillName(arg)
|
||||||
if !ok {
|
if !ok {
|
||||||
return true, true, fmt.Sprintf("Unknown skill: %s\nUse /list skills to see installed skills.", arg)
|
return true, true, fmt.Sprintf(
|
||||||
|
"Unknown skill: %s\nUse /list skills to see installed skills.",
|
||||||
|
arg,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(parts) < 3 {
|
if len(parts) < 3 {
|
||||||
|
|
@ -3349,7 +3416,10 @@ func (al *AgentLoop) applyExplicitSkillCommand(
|
||||||
return true, false, ""
|
return true, false, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (al *AgentLoop) buildCommandsRuntime(agent *AgentInstance, opts *processOptions) *commands.Runtime {
|
func (al *AgentLoop) buildCommandsRuntime(
|
||||||
|
agent *AgentInstance,
|
||||||
|
opts *processOptions,
|
||||||
|
) *commands.Runtime {
|
||||||
registry := al.GetRegistry()
|
registry := al.GetRegistry()
|
||||||
cfg := al.GetConfig()
|
cfg := al.GetConfig()
|
||||||
rt := &commands.Runtime{
|
rt := &commands.Runtime{
|
||||||
|
|
@ -3393,7 +3463,10 @@ func (al *AgentLoop) buildCommandsRuntime(agent *AgentInstance, opts *processOpt
|
||||||
rt.ListSkillNames = agent.ContextBuilder.ListSkillNames
|
rt.ListSkillNames = agent.ContextBuilder.ListSkillNames
|
||||||
}
|
}
|
||||||
rt.GetModelInfo = func() (string, string) {
|
rt.GetModelInfo = func() (string, string) {
|
||||||
return agent.Model, resolvedCandidateProvider(agent.Candidates, cfg.Agents.Defaults.Provider)
|
return agent.Model, resolvedCandidateProvider(
|
||||||
|
agent.Candidates,
|
||||||
|
cfg.Agents.Defaults.Provider,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
rt.SwitchModel = func(value string) (string, error) {
|
rt.SwitchModel = func(value string) (string, error) {
|
||||||
value = strings.TrimSpace(value)
|
value = strings.TrimSpace(value)
|
||||||
|
|
@ -3407,7 +3480,12 @@ func (al *AgentLoop) buildCommandsRuntime(agent *AgentInstance, opts *processOpt
|
||||||
return "", fmt.Errorf("failed to initialize model %q: %w", value, err)
|
return "", fmt.Errorf("failed to initialize model %q: %w", value, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
nextCandidates := resolveModelCandidates(cfg, cfg.Agents.Defaults.Provider, modelCfg.Model, agent.Fallbacks)
|
nextCandidates := resolveModelCandidates(
|
||||||
|
cfg,
|
||||||
|
cfg.Agents.Defaults.Provider,
|
||||||
|
modelCfg.Model,
|
||||||
|
agent.Fallbacks,
|
||||||
|
)
|
||||||
if len(nextCandidates) == 0 {
|
if len(nextCandidates) == 0 {
|
||||||
return "", fmt.Errorf("model %q did not resolve to any provider candidates", value)
|
return "", fmt.Errorf("model %q did not resolve to any provider candidates", value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1265,7 +1265,10 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
logger.WarnF("config file not found, using default config", map[string]any{"path": path})
|
logger.WarnF(
|
||||||
|
"config file not found, using default config",
|
||||||
|
map[string]any{"path": path},
|
||||||
|
)
|
||||||
return DefaultConfig(), nil
|
return DefaultConfig(), nil
|
||||||
}
|
}
|
||||||
logger.Errorf("failed to read config file: %v", err)
|
logger.Errorf("failed to read config file: %v", err)
|
||||||
|
|
@ -1288,7 +1291,10 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
var cfg *Config
|
var cfg *Config
|
||||||
switch versionInfo.Version {
|
switch versionInfo.Version {
|
||||||
case 0:
|
case 0:
|
||||||
logger.InfoF("config migrate start", map[string]any{"from": versionInfo.Version, "to": CurrentVersion})
|
logger.InfoF(
|
||||||
|
"config migrate start",
|
||||||
|
map[string]any{"from": versionInfo.Version, "to": CurrentVersion},
|
||||||
|
)
|
||||||
// Legacy config (no version field)
|
// Legacy config (no version field)
|
||||||
v, e := loadConfigV0(data)
|
v, e := loadConfigV0(data)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
|
|
@ -1296,10 +1302,16 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
}
|
}
|
||||||
cfg, e = v.Migrate()
|
cfg, e = v.Migrate()
|
||||||
if e != nil {
|
if e != nil {
|
||||||
logger.ErrorF("config migrate fail", map[string]any{"from": versionInfo.Version, "to": CurrentVersion})
|
logger.ErrorF(
|
||||||
|
"config migrate fail",
|
||||||
|
map[string]any{"from": versionInfo.Version, "to": CurrentVersion},
|
||||||
|
)
|
||||||
return nil, e
|
return nil, e
|
||||||
}
|
}
|
||||||
logger.InfoF("config migrate success", map[string]any{"from": versionInfo.Version, "to": CurrentVersion})
|
logger.InfoF(
|
||||||
|
"config migrate success",
|
||||||
|
map[string]any{"from": versionInfo.Version, "to": CurrentVersion},
|
||||||
|
)
|
||||||
err = makeBackup(path)
|
err = makeBackup(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -1307,13 +1319,19 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
// Load existing security config and merge with migrated one to prevent data loss
|
// Load existing security config and merge with migrated one to prevent data loss
|
||||||
existingSec, secErr := loadSecurityConfig(securityPath(path))
|
existingSec, secErr := loadSecurityConfig(securityPath(path))
|
||||||
if secErr != nil {
|
if secErr != nil {
|
||||||
logger.WarnF("failed to load existing security config during migration", map[string]any{"error": secErr})
|
logger.WarnF(
|
||||||
|
"failed to load existing security config during migration",
|
||||||
|
map[string]any{"error": secErr},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if existingSec != nil && cfg.security != nil {
|
if existingSec != nil && cfg.security != nil {
|
||||||
cfg.security = mergeSecurityConfig(existingSec, cfg.security)
|
cfg.security = mergeSecurityConfig(existingSec, cfg.security)
|
||||||
// Re-apply the merged security config to update all channels and models
|
// Re-apply the merged security config to update all channels and models
|
||||||
if err = applySecurityConfig(cfg, cfg.security); err != nil {
|
if err = applySecurityConfig(cfg, cfg.security); err != nil {
|
||||||
logger.WarnF("failed to re-apply merged security config during migration", map[string]any{"error": err})
|
logger.WarnF(
|
||||||
|
"failed to re-apply merged security config during migration",
|
||||||
|
map[string]any{"error": err},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer func(cfg *Config) {
|
defer func(cfg *Config) {
|
||||||
|
|
@ -1345,9 +1363,11 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
for _, m := range cfg.ModelList {
|
for _, m := range cfg.ModelList {
|
||||||
for _, k := range m.apiKeys {
|
for _, k := range m.apiKeys {
|
||||||
if k != "" && !strings.HasPrefix(k, "enc://") && !strings.HasPrefix(k, "file://") {
|
if k != "" && !strings.HasPrefix(k, "enc://") && !strings.HasPrefix(k, "file://") {
|
||||||
fmt.Fprintf(os.Stderr,
|
fmt.Fprintf(
|
||||||
|
os.Stderr,
|
||||||
"picoclaw: warning: model %q has a plaintext api_key; call SaveConfig to encrypt it\n",
|
"picoclaw: warning: model %q has a plaintext api_key; call SaveConfig to encrypt it\n",
|
||||||
m.ModelName)
|
m.ModelName,
|
||||||
|
)
|
||||||
break // Only warn once per model
|
break // Only warn once per model
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,7 @@ type (
|
||||||
FunctionCall = protocoltypes.FunctionCall
|
FunctionCall = protocoltypes.FunctionCall
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const ()
|
||||||
defaultRequestTimeout = common.DefaultRequestTimeout
|
|
||||||
)
|
|
||||||
|
|
||||||
// Provider implements the LLM provider interface for Google Vertex AI.
|
// Provider implements the LLM provider interface for Google Vertex AI.
|
||||||
// It uses the standard Vertex AI REST API for Gemini models.
|
// It uses the standard Vertex AI REST API for Gemini models.
|
||||||
|
|
@ -114,7 +112,6 @@ func (p *Provider) buildURL(model string, action string) string {
|
||||||
return baseURL
|
return baseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// parseMediaData converts base64 media data into the Vertex AI inlineData format.
|
// parseMediaData converts base64 media data into the Vertex AI inlineData format.
|
||||||
// It tries to detect mime type from the data URI scheme if present.
|
// It tries to detect mime type from the data URI scheme if present.
|
||||||
func parseMediaData(mediaData string) map[string]any {
|
func parseMediaData(mediaData string) map[string]any {
|
||||||
|
|
@ -138,7 +135,11 @@ func parseMediaData(mediaData string) map[string]any {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody formats the standard messages and tools into the Vertex AI (Gemini) REST payload format.
|
// buildRequestBody formats the standard messages and tools into the Vertex AI (Gemini) REST payload format.
|
||||||
func (p *Provider) buildRequestBody(messages []Message, tools []ToolDefinition, options map[string]any) (map[string]any, error) {
|
func (p *Provider) buildRequestBody(
|
||||||
|
messages []Message,
|
||||||
|
tools []ToolDefinition,
|
||||||
|
options map[string]any,
|
||||||
|
) (map[string]any, error) {
|
||||||
req := make(map[string]any)
|
req := make(map[string]any)
|
||||||
|
|
||||||
var contents []map[string]any
|
var contents []map[string]any
|
||||||
|
|
@ -291,7 +292,6 @@ func (p *Provider) buildRequestBody(messages []Message, tools []ToolDefinition,
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (p *Provider) Chat(
|
func (p *Provider) Chat(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
messages []Message,
|
messages []Message,
|
||||||
|
|
@ -440,7 +440,11 @@ func (p *Provider) ChatStream(
|
||||||
if part.FunctionCall != nil {
|
if part.FunctionCall != nil {
|
||||||
argsJSON, _ := json.Marshal(part.FunctionCall.Args)
|
argsJSON, _ := json.Marshal(part.FunctionCall.Args)
|
||||||
toolCall := ToolCall{
|
toolCall := ToolCall{
|
||||||
ID: fmt.Sprintf("call_%s_%d", part.FunctionCall.Name, time.Now().UnixNano()),
|
ID: fmt.Sprintf(
|
||||||
|
"call_%s_%d",
|
||||||
|
part.FunctionCall.Name,
|
||||||
|
time.Now().UnixNano(),
|
||||||
|
),
|
||||||
Name: part.FunctionCall.Name,
|
Name: part.FunctionCall.Name,
|
||||||
Arguments: part.FunctionCall.Args,
|
Arguments: part.FunctionCall.Args,
|
||||||
Function: &FunctionCall{
|
Function: &FunctionCall{
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProvider_buildURL(t *testing.T) {
|
func TestProvider_buildURL(t *testing.T) {
|
||||||
|
|
@ -59,16 +59,25 @@ func TestProvider_buildURL(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func TestProvider_buildRequestBody(t *testing.T) {
|
func TestProvider_buildRequestBody(t *testing.T) {
|
||||||
p := NewProvider("key", "", "", "proj", "us-central1")
|
p := NewProvider("key", "", "", "proj", "us-central1")
|
||||||
|
|
||||||
messages := []protocoltypes.Message{
|
messages := []protocoltypes.Message{
|
||||||
{Role: "system", Content: "You are a helpful assistant."},
|
{Role: "system", Content: "You are a helpful assistant."},
|
||||||
{Role: "user", Content: "Hello!", Media: []string{"data:image/png;base64,iVBORw0KGgo"}},
|
{Role: "user", Content: "Hello!", Media: []string{"data:image/png;base64,iVBORw0KGgo"}},
|
||||||
{Role: "assistant", ToolCalls: []protocoltypes.ToolCall{{Name: "get_weather", Arguments: map[string]any{"location": "Tokyo"}}}},
|
{
|
||||||
|
Role: "assistant",
|
||||||
|
ToolCalls: []protocoltypes.ToolCall{
|
||||||
|
{Name: "get_weather", Arguments: map[string]any{"location": "Tokyo"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
{Role: "tool", ToolCallID: "get_weather", Content: "Sunny"},
|
{Role: "tool", ToolCallID: "get_weather", Content: "Sunny"},
|
||||||
{Role: "assistant", ToolCalls: []protocoltypes.ToolCall{{Name: "get_time", Arguments: map[string]any{"location": "Tokyo"}}}},
|
{
|
||||||
|
Role: "assistant",
|
||||||
|
ToolCalls: []protocoltypes.ToolCall{
|
||||||
|
{Name: "get_time", Arguments: map[string]any{"location": "Tokyo"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
{Role: "tool", ToolCallID: "get_time", Content: "12:00 PM"},
|
{Role: "tool", ToolCallID: "get_time", Content: "12:00 PM"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,7 +137,6 @@ func TestProvider_buildRequestBody(t *testing.T) {
|
||||||
assert.Equal(t, "model", contents[3]["role"])
|
assert.Equal(t, "model", contents[3]["role"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func TestProvider_Chat(t *testing.T) {
|
func TestProvider_Chat(t *testing.T) {
|
||||||
// Create a mock server
|
// Create a mock server
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -194,7 +202,11 @@ func TestProvider_ChatStream(t *testing.T) {
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
// Write mock chunks
|
// Write mock chunks
|
||||||
w.Write([]byte(`data: {"candidates":[{"content":{"parts":[{"text":"Hello"}]}}]}` + "\n\n"))
|
w.Write([]byte(`data: {"candidates":[{"content":{"parts":[{"text":"Hello"}]}}]}` + "\n\n"))
|
||||||
w.Write([]byte(`data: {"candidates":[{"content":{"parts":[{"text":", world!"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":5,"totalTokenCount":15}}` + "\n\n"))
|
w.Write(
|
||||||
|
[]byte(
|
||||||
|
`data: {"candidates":[{"content":{"parts":[{"text":", world!"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":5,"totalTokenCount":15}}` + "\n\n",
|
||||||
|
),
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import (
|
||||||
// defaultThreshold is used when the config threshold is zero or negative.
|
// defaultThreshold is used when the config threshold is zero or negative.
|
||||||
// At 0.35 a message needs at least one strong signal (code block, long text,
|
// At 0.35 a message needs at least one strong signal (code block, long text,
|
||||||
// or an attachment) before the heavy model is chosen.
|
// or an attachment) before the heavy model is chosen.
|
||||||
const defaultThreshold = 0.35
|
|
||||||
|
|
||||||
// RoutingTier defines a single tier for model routing.
|
// RoutingTier defines a single tier for model routing.
|
||||||
type RoutingTier struct {
|
type RoutingTier struct {
|
||||||
|
|
|
||||||
|
|
@ -82,13 +82,19 @@ func TestExtractFeatures_RecentToolCalls(t *testing.T) {
|
||||||
// History longer than lookbackWindow — only last lookbackWindow entries count.
|
// History longer than lookbackWindow — only last lookbackWindow entries count.
|
||||||
history := make([]providers.Message, 10)
|
history := make([]providers.Message, 10)
|
||||||
// Put 2 tool calls at positions 8 and 9 (within the last 6)
|
// Put 2 tool calls at positions 8 and 9 (within the last 6)
|
||||||
history[8] = providers.Message{Role: "assistant", ToolCalls: []providers.ToolCall{{Name: "exec"}}}
|
history[8] = providers.Message{
|
||||||
|
Role: "assistant",
|
||||||
|
ToolCalls: []providers.ToolCall{{Name: "exec"}},
|
||||||
|
}
|
||||||
history[9] = providers.Message{
|
history[9] = providers.Message{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
ToolCalls: []providers.ToolCall{{Name: "read_file"}, {Name: "write_file"}},
|
ToolCalls: []providers.ToolCall{{Name: "read_file"}, {Name: "write_file"}},
|
||||||
}
|
}
|
||||||
// Position 3 is outside the lookback window and must NOT be counted
|
// Position 3 is outside the lookback window and must NOT be counted
|
||||||
history[3] = providers.Message{Role: "assistant", ToolCalls: []providers.ToolCall{{Name: "old_tool"}}}
|
history[3] = providers.Message{
|
||||||
|
Role: "assistant",
|
||||||
|
ToolCalls: []providers.ToolCall{{Name: "old_tool"}},
|
||||||
|
}
|
||||||
|
|
||||||
f := ExtractFeatures("test", history)
|
f := ExtractFeatures("test", history)
|
||||||
// 1 (position 8) + 2 (position 9) = 3
|
// 1 (position 8) + 2 (position 9) = 3
|
||||||
|
|
@ -241,9 +247,15 @@ func TestRuleClassifier_ScoreDoesNotExceedOne(t *testing.T) {
|
||||||
|
|
||||||
// ── Router ───────────────────────────────────────────────────────────────────
|
// ── Router ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
func TestRouter_SelectModel_SimpleMessageUsesLight(t *testing.T) {
|
func TestRouter_SelectModel_SimpleMessageUsesLight(t *testing.T) {
|
||||||
r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.35}}})
|
r := New(
|
||||||
|
RouterConfig{
|
||||||
|
Tiers: []RoutingTier{
|
||||||
|
{Model: "gemini-flash", Threshold: 0.0},
|
||||||
|
{Model: "claude-sonnet-4-6", Threshold: 0.35},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
msg := "hi"
|
msg := "hi"
|
||||||
model, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6")
|
model, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6")
|
||||||
if !usedLight {
|
if !usedLight {
|
||||||
|
|
@ -255,7 +267,14 @@ func TestRouter_SelectModel_SimpleMessageUsesLight(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouter_SelectModel_CodeBlockUsesPrimary(t *testing.T) {
|
func TestRouter_SelectModel_CodeBlockUsesPrimary(t *testing.T) {
|
||||||
r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.35}}})
|
r := New(
|
||||||
|
RouterConfig{
|
||||||
|
Tiers: []RoutingTier{
|
||||||
|
{Model: "gemini-flash", Threshold: 0.0},
|
||||||
|
{Model: "claude-sonnet-4-6", Threshold: 0.35},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
msg := "```go\nfmt.Println(\"hello\")\n```"
|
msg := "```go\nfmt.Println(\"hello\")\n```"
|
||||||
model, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6")
|
model, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6")
|
||||||
if usedLight {
|
if usedLight {
|
||||||
|
|
@ -267,7 +286,14 @@ func TestRouter_SelectModel_CodeBlockUsesPrimary(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouter_SelectModel_AttachmentUsesPrimary(t *testing.T) {
|
func TestRouter_SelectModel_AttachmentUsesPrimary(t *testing.T) {
|
||||||
r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.35}}})
|
r := New(
|
||||||
|
RouterConfig{
|
||||||
|
Tiers: []RoutingTier{
|
||||||
|
{Model: "gemini-flash", Threshold: 0.0},
|
||||||
|
{Model: "claude-sonnet-4-6", Threshold: 0.35},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
msg := "can you analyze this? data:image/png;base64,abc123"
|
msg := "can you analyze this? data:image/png;base64,abc123"
|
||||||
model, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6")
|
model, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6")
|
||||||
if usedLight {
|
if usedLight {
|
||||||
|
|
@ -279,7 +305,14 @@ func TestRouter_SelectModel_AttachmentUsesPrimary(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouter_SelectModel_LongMessageUsesPrimary(t *testing.T) {
|
func TestRouter_SelectModel_LongMessageUsesPrimary(t *testing.T) {
|
||||||
r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.35}}})
|
r := New(
|
||||||
|
RouterConfig{
|
||||||
|
Tiers: []RoutingTier{
|
||||||
|
{Model: "gemini-flash", Threshold: 0.0},
|
||||||
|
{Model: "claude-sonnet-4-6", Threshold: 0.35},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
// >200 token estimate: 210 * 3 = 630 chars
|
// >200 token estimate: 210 * 3 = 630 chars
|
||||||
msg := strings.Repeat("word ", 210)
|
msg := strings.Repeat("word ", 210)
|
||||||
model, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6")
|
model, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6")
|
||||||
|
|
@ -294,9 +327,19 @@ func TestRouter_SelectModel_LongMessageUsesPrimary(t *testing.T) {
|
||||||
func TestRouter_SelectModel_DeepToolChainUsesLight(t *testing.T) {
|
func TestRouter_SelectModel_DeepToolChainUsesLight(t *testing.T) {
|
||||||
// Tool calls alone (0.25) don't cross the 0.35 threshold — acceptable behavior.
|
// Tool calls alone (0.25) don't cross the 0.35 threshold — acceptable behavior.
|
||||||
// Routing is conservative: only promote to heavy when the signal is unambiguous.
|
// Routing is conservative: only promote to heavy when the signal is unambiguous.
|
||||||
r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.35}}})
|
r := New(
|
||||||
|
RouterConfig{
|
||||||
|
Tiers: []RoutingTier{
|
||||||
|
{Model: "gemini-flash", Threshold: 0.0},
|
||||||
|
{Model: "claude-sonnet-4-6", Threshold: 0.35},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
history := []providers.Message{
|
history := []providers.Message{
|
||||||
{Role: "assistant", ToolCalls: []providers.ToolCall{{Name: "read_file"}, {Name: "write_file"}}},
|
{
|
||||||
|
Role: "assistant",
|
||||||
|
ToolCalls: []providers.ToolCall{{Name: "read_file"}, {Name: "write_file"}},
|
||||||
|
},
|
||||||
{Role: "assistant", ToolCalls: []providers.ToolCall{{Name: "exec"}, {Name: "search"}}},
|
{Role: "assistant", ToolCalls: []providers.ToolCall{{Name: "exec"}, {Name: "search"}}},
|
||||||
}
|
}
|
||||||
msg := "ok"
|
msg := "ok"
|
||||||
|
|
@ -308,7 +351,14 @@ func TestRouter_SelectModel_DeepToolChainUsesLight(t *testing.T) {
|
||||||
|
|
||||||
func TestRouter_SelectModel_ToolChainPlusMediumUsesHeavy(t *testing.T) {
|
func TestRouter_SelectModel_ToolChainPlusMediumUsesHeavy(t *testing.T) {
|
||||||
// Tool calls (0.25) + medium message (0.15) = 0.40 >= 0.35 → heavy
|
// Tool calls (0.25) + medium message (0.15) = 0.40 >= 0.35 → heavy
|
||||||
r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.35}}})
|
r := New(
|
||||||
|
RouterConfig{
|
||||||
|
Tiers: []RoutingTier{
|
||||||
|
{Model: "gemini-flash", Threshold: 0.0},
|
||||||
|
{Model: "claude-sonnet-4-6", Threshold: 0.35},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
history := []providers.Message{
|
history := []providers.Message{
|
||||||
{Role: "assistant", ToolCalls: []providers.ToolCall{
|
{Role: "assistant", ToolCalls: []providers.ToolCall{
|
||||||
{Name: "a"}, {Name: "b"}, {Name: "c"}, {Name: "d"},
|
{Name: "a"}, {Name: "b"}, {Name: "c"}, {Name: "d"},
|
||||||
|
|
@ -324,7 +374,14 @@ func TestRouter_SelectModel_ToolChainPlusMediumUsesHeavy(t *testing.T) {
|
||||||
|
|
||||||
func TestRouter_SelectModel_CustomThreshold(t *testing.T) {
|
func TestRouter_SelectModel_CustomThreshold(t *testing.T) {
|
||||||
// Very low threshold: even a short message triggers heavy model
|
// Very low threshold: even a short message triggers heavy model
|
||||||
r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.05}}})
|
r := New(
|
||||||
|
RouterConfig{
|
||||||
|
Tiers: []RoutingTier{
|
||||||
|
{Model: "gemini-flash", Threshold: 0.0},
|
||||||
|
{Model: "claude-sonnet-4-6", Threshold: 0.05},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
msg := strings.Repeat("word ", 55) // medium message → 0.15 >= 0.05
|
msg := strings.Repeat("word ", 55) // medium message → 0.15 >= 0.05
|
||||||
_, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6")
|
_, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6")
|
||||||
if usedLight {
|
if usedLight {
|
||||||
|
|
@ -334,7 +391,14 @@ func TestRouter_SelectModel_CustomThreshold(t *testing.T) {
|
||||||
|
|
||||||
func TestRouter_SelectModel_HighThreshold(t *testing.T) {
|
func TestRouter_SelectModel_HighThreshold(t *testing.T) {
|
||||||
// Very high threshold: even code blocks route to light
|
// Very high threshold: even code blocks route to light
|
||||||
r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.99}}})
|
r := New(
|
||||||
|
RouterConfig{
|
||||||
|
Tiers: []RoutingTier{
|
||||||
|
{Model: "gemini-flash", Threshold: 0.0},
|
||||||
|
{Model: "claude-sonnet-4-6", Threshold: 0.99},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
msg := "```go\nfmt.Println()\n```"
|
msg := "```go\nfmt.Println()\n```"
|
||||||
_, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6")
|
_, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6")
|
||||||
if !usedLight {
|
if !usedLight {
|
||||||
|
|
@ -343,7 +407,14 @@ func TestRouter_SelectModel_HighThreshold(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouter_Tiers(t *testing.T) {
|
func TestRouter_Tiers(t *testing.T) {
|
||||||
r := New(RouterConfig{Tiers: []RoutingTier{{Model: "my-fast-model", Threshold: 0.0}, {Model: "heavy-model", Threshold: 0.35}}})
|
r := New(
|
||||||
|
RouterConfig{
|
||||||
|
Tiers: []RoutingTier{
|
||||||
|
{Model: "my-fast-model", Threshold: 0.0},
|
||||||
|
{Model: "heavy-model", Threshold: 0.35},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
if r.Tiers()[0].Model != "my-fast-model" {
|
if r.Tiers()[0].Model != "my-fast-model" {
|
||||||
t.Errorf("LightModel: got %q, want %q", "my-fast-model", "my-fast-model")
|
t.Errorf("LightModel: got %q, want %q", "my-fast-model", "my-fast-model")
|
||||||
}
|
}
|
||||||
|
|
@ -357,7 +428,12 @@ func (f *fixedScoreClassifier) Score(_ Features) float64 { return f.score }
|
||||||
|
|
||||||
func TestRouter_CustomClassifier_LowScore_SelectsLight(t *testing.T) {
|
func TestRouter_CustomClassifier_LowScore_SelectsLight(t *testing.T) {
|
||||||
r := newWithClassifier(
|
r := newWithClassifier(
|
||||||
RouterConfig{Tiers: []RoutingTier{{Model: "light", Threshold: 0.0}, {Model: "heavy", Threshold: 0.5}}},
|
RouterConfig{
|
||||||
|
Tiers: []RoutingTier{
|
||||||
|
{Model: "light", Threshold: 0.0},
|
||||||
|
{Model: "heavy", Threshold: 0.5},
|
||||||
|
},
|
||||||
|
},
|
||||||
&fixedScoreClassifier{score: 0.2},
|
&fixedScoreClassifier{score: 0.2},
|
||||||
)
|
)
|
||||||
_, usedLight, _ := r.SelectModel("anything", nil, "heavy")
|
_, usedLight, _ := r.SelectModel("anything", nil, "heavy")
|
||||||
|
|
@ -368,7 +444,12 @@ func TestRouter_CustomClassifier_LowScore_SelectsLight(t *testing.T) {
|
||||||
|
|
||||||
func TestRouter_CustomClassifier_HighScore_SelectsPrimary(t *testing.T) {
|
func TestRouter_CustomClassifier_HighScore_SelectsPrimary(t *testing.T) {
|
||||||
r := newWithClassifier(
|
r := newWithClassifier(
|
||||||
RouterConfig{Tiers: []RoutingTier{{Model: "light", Threshold: 0.0}, {Model: "heavy", Threshold: 0.5}}},
|
RouterConfig{
|
||||||
|
Tiers: []RoutingTier{
|
||||||
|
{Model: "light", Threshold: 0.0},
|
||||||
|
{Model: "heavy", Threshold: 0.5},
|
||||||
|
},
|
||||||
|
},
|
||||||
&fixedScoreClassifier{score: 0.8},
|
&fixedScoreClassifier{score: 0.8},
|
||||||
)
|
)
|
||||||
_, usedLight, _ := r.SelectModel("anything", nil, "heavy")
|
_, usedLight, _ := r.SelectModel("anything", nil, "heavy")
|
||||||
|
|
@ -380,7 +461,12 @@ func TestRouter_CustomClassifier_HighScore_SelectsPrimary(t *testing.T) {
|
||||||
func TestRouter_CustomClassifier_ExactThreshold_SelectsPrimary(t *testing.T) {
|
func TestRouter_CustomClassifier_ExactThreshold_SelectsPrimary(t *testing.T) {
|
||||||
// score == threshold → primary (uses >= comparison)
|
// score == threshold → primary (uses >= comparison)
|
||||||
r := newWithClassifier(
|
r := newWithClassifier(
|
||||||
RouterConfig{Tiers: []RoutingTier{{Model: "light", Threshold: 0.0}, {Model: "heavy", Threshold: 0.5}}},
|
RouterConfig{
|
||||||
|
Tiers: []RoutingTier{
|
||||||
|
{Model: "light", Threshold: 0.0},
|
||||||
|
{Model: "heavy", Threshold: 0.5},
|
||||||
|
},
|
||||||
|
},
|
||||||
&fixedScoreClassifier{score: 0.5},
|
&fixedScoreClassifier{score: 0.5},
|
||||||
)
|
)
|
||||||
_, usedLight, _ := r.SelectModel("anything", nil, "heavy")
|
_, usedLight, _ := r.SelectModel("anything", nil, "heavy")
|
||||||
|
|
@ -391,7 +477,12 @@ func TestRouter_CustomClassifier_ExactThreshold_SelectsPrimary(t *testing.T) {
|
||||||
|
|
||||||
func TestRouter_SelectModel_ReturnsScore(t *testing.T) {
|
func TestRouter_SelectModel_ReturnsScore(t *testing.T) {
|
||||||
r := newWithClassifier(
|
r := newWithClassifier(
|
||||||
RouterConfig{Tiers: []RoutingTier{{Model: "light", Threshold: 0.0}, {Model: "heavy", Threshold: 0.5}}},
|
RouterConfig{
|
||||||
|
Tiers: []RoutingTier{
|
||||||
|
{Model: "light", Threshold: 0.0},
|
||||||
|
{Model: "heavy", Threshold: 0.5},
|
||||||
|
},
|
||||||
|
},
|
||||||
&fixedScoreClassifier{score: 0.42},
|
&fixedScoreClassifier{score: 0.42},
|
||||||
)
|
)
|
||||||
_, _, score := r.SelectModel("anything", nil, "heavy")
|
_, _, score := r.SelectModel("anything", nil, "heavy")
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,8 @@ func (t *UpdateSkillTool) Execute(ctx context.Context, args map[string]any) *Too
|
||||||
|
|
||||||
// Prepare the content to append
|
// Prepare the content to append
|
||||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||||
contentToAppend := fmt.Sprintf("\n\n## Learned Skill: %s\n\n**Analysis**: %s\n\n**Skills Improved**: %s\n\n%s\n",
|
contentToAppend := fmt.Sprintf(
|
||||||
|
"\n\n## Learned Skill: %s\n\n**Analysis**: %s\n\n**Skills Improved**: %s\n\n%s\n",
|
||||||
timestamp,
|
timestamp,
|
||||||
analysis,
|
analysis,
|
||||||
skillsToImprove,
|
skillsToImprove,
|
||||||
|
|
@ -92,11 +93,19 @@ func (t *UpdateSkillTool) Execute(ctx context.Context, args map[string]any) *Too
|
||||||
return ErrorResult(fmt.Sprintf("failed to append to SKILL.md: %v", err))
|
return ErrorResult(fmt.Sprintf("failed to append to SKILL.md: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
output := fmt.Sprintf("Successfully learned and updated SKILL.md.\n\nAnalysis: %s\nSkills Improved: %s\n", analysis, skillsToImprove)
|
output := fmt.Sprintf(
|
||||||
|
"Successfully learned and updated SKILL.md.\n\nAnalysis: %s\nSkills Improved: %s\n",
|
||||||
|
analysis,
|
||||||
|
skillsToImprove,
|
||||||
|
)
|
||||||
|
|
||||||
// The response is passed back to the LLM.
|
// The response is passed back to the LLM.
|
||||||
// We also populate the ForUser field to notify the user.
|
// We also populate the ForUser field to notify the user.
|
||||||
res := SilentResult(output)
|
res := SilentResult(output)
|
||||||
res.ForUser = fmt.Sprintf("I have analyzed our conversation and improved my skills.\n\n**My Analysis**:\n%s\n\n**Skills I've Improved/Added**:\n%s\n\nI have saved these learnings to `SKILL.md`.", analysis, skillsToImprove)
|
res.ForUser = fmt.Sprintf(
|
||||||
|
"I have analyzed our conversation and improved my skills.\n\n**My Analysis**:\n%s\n\n**Skills I've Improved/Added**:\n%s\n\nI have saved these learnings to `SKILL.md`.",
|
||||||
|
analysis,
|
||||||
|
skillsToImprove,
|
||||||
|
)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,11 @@ func (h *Handler) handleUpdateModel(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx < 0 || idx >= len(cfg.ModelList) {
|
if idx < 0 || idx >= len(cfg.ModelList) {
|
||||||
http.Error(w, fmt.Sprintf("Index %d out of range (0-%d)", idx, len(cfg.ModelList)-1), http.StatusNotFound)
|
http.Error(
|
||||||
|
w,
|
||||||
|
fmt.Sprintf("Index %d out of range (0-%d)", idx, len(cfg.ModelList)-1),
|
||||||
|
http.StatusNotFound,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,7 +252,11 @@ func (h *Handler) handleDeleteModel(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx < 0 || idx >= len(cfg.ModelList) {
|
if idx < 0 || idx >= len(cfg.ModelList) {
|
||||||
http.Error(w, fmt.Sprintf("Index %d out of range (0-%d)", idx, len(cfg.ModelList)-1), http.StatusNotFound)
|
http.Error(
|
||||||
|
w,
|
||||||
|
fmt.Sprintf("Index %d out of range (0-%d)", idx, len(cfg.ModelList)-1),
|
||||||
|
http.StatusNotFound,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -311,11 +319,19 @@ func (h *Handler) handleSetDefaultModel(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
http.Error(w, fmt.Sprintf("Model %q not found in model_list", req.ModelName), http.StatusNotFound)
|
http.Error(
|
||||||
|
w,
|
||||||
|
fmt.Sprintf("Model %q not found in model_list", req.ModelName),
|
||||||
|
http.StatusNotFound,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if isVirtual {
|
if isVirtual {
|
||||||
http.Error(w, fmt.Sprintf("Cannot set virtual model %q as default", req.ModelName), http.StatusBadRequest)
|
http.Error(
|
||||||
|
w,
|
||||||
|
fmt.Sprintf("Cannot set virtual model %q as default", req.ModelName),
|
||||||
|
http.StatusBadRequest,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue