Merge remote-tracking branch 'origin/main' into dev

This commit is contained in:
viettranx
2026-04-06 12:56:44 +07:00
7 changed files with 71 additions and 14 deletions
+6 -1
View File
@@ -41,7 +41,9 @@ func registerProviders(registry *providers.Registry, cfg *config.Config) {
}
if cfg.Providers.OpenRouter.APIKey != "" {
registry.Register(providers.NewOpenAIProvider("openrouter", cfg.Providers.OpenRouter.APIKey, "https://openrouter.ai/api/v1", "anthropic/claude-sonnet-4-5-20250929"))
orProv := providers.NewOpenAIProvider("openrouter", cfg.Providers.OpenRouter.APIKey, "https://openrouter.ai/api/v1", "anthropic/claude-sonnet-4-5-20250929")
orProv.WithSiteInfo("https://goclaw.sh", "GoClaw")
registry.Register(orProv)
slog.Info("registered provider", "name", "openrouter")
}
@@ -404,6 +406,9 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi
if p.ProviderType == store.ProviderMiniMax {
prov.WithChatPath("/text/chatcompletion_v2")
}
if p.ProviderType == store.ProviderOpenRouter {
prov.WithSiteInfo("https://goclaw.sh", "GoClaw")
}
registry.RegisterForTenant(p.TenantID, prov)
}
slog.Info("registered provider from DB", "name", p.Name)
+8 -1
View File
@@ -55,7 +55,14 @@ func buildMCPToolsInlineSection(descs map[string]string) []string {
mcpOptionalParamInstruction,
"",
}
for name, desc := range descs {
// Sort MCP tool names for deterministic ordering — critical for prompt caching.
sortedNames := make([]string, 0, len(descs))
for name := range descs {
sortedNames = append(sortedNames, name)
}
slices.Sort(sortedNames)
for _, name := range sortedNames {
desc := descs[name]
if len(desc) > mcpToolDescMaxLen {
desc = desc[:mcpToolDescMaxLen] + "…"
}
+17
View File
@@ -27,6 +27,8 @@ type OpenAIProvider struct {
authPrefix string // auth header prefix, defaults to "Bearer " if empty
defaultModel string
providerType string // DB provider_type (e.g. "gemini_native", "openai", "minimax_native")
siteURL string // optional site URL for provider identification (e.g. OpenRouter HTTP-Referer)
siteTitle string // optional site title for provider identification (e.g. OpenRouter X-Title)
client *http.Client
retryConfig RetryConfig
}
@@ -114,6 +116,14 @@ func (p *OpenAIProvider) WithAuthPrefix(prefix string) *OpenAIProvider {
return p
}
// WithSiteInfo sets site identification headers sent with API requests.
// Used by OpenRouter for rankings (HTTP-Referer, X-Title).
func (p *OpenAIProvider) WithSiteInfo(url, title string) *OpenAIProvider {
p.siteURL = url
p.siteTitle = title
return p
}
func (p *OpenAIProvider) Name() string { return p.name }
func (p *OpenAIProvider) DefaultModel() string { return p.defaultModel }
func (p *OpenAIProvider) SupportsThinking() bool { return true }
@@ -576,6 +586,13 @@ func (p *OpenAIProvider) doRequest(ctx context.Context, body any) (io.ReadCloser
}
httpReq.Header.Set("Authorization", prefix+p.apiKey)
}
// OpenRouter identification headers for rankings/analytics
if p.siteURL != "" {
httpReq.Header.Set("HTTP-Referer", p.siteURL)
}
if p.siteTitle != "" {
httpReq.Header.Set("X-Title", p.siteTitle)
}
resp, err := p.client.Do(httpReq)
if err != nil {
+2 -2
View File
@@ -25,7 +25,7 @@ func (s *PGAgentStore) GetAgentContextFiles(ctx context.Context, agentID uuid.UU
return nil, err
}
rows, err := s.db.QueryContext(ctx,
"SELECT agent_id, file_name, content FROM agent_context_files WHERE agent_id = $1"+tClause,
"SELECT agent_id, file_name, content FROM agent_context_files WHERE agent_id = $1"+tClause+" ORDER BY file_name",
append([]any{agentID}, tArgs...)...)
if err != nil {
return nil, err
@@ -87,7 +87,7 @@ func (s *PGAgentStore) GetUserContextFiles(ctx context.Context, agentID uuid.UUI
return nil, err
}
rows, err := s.db.QueryContext(ctx,
"SELECT agent_id, user_id, file_name, content FROM user_context_files WHERE agent_id = $1 AND user_id = $2"+tClause,
"SELECT agent_id, user_id, file_name, content FROM user_context_files WHERE agent_id = $1 AND user_id = $2"+tClause+" ORDER BY file_name",
append([]any{agentID, userID}, tArgs...)...)
if err != nil {
return nil, err
+2 -2
View File
@@ -27,7 +27,7 @@ func (s *SQLiteAgentStore) GetAgentContextFiles(ctx context.Context, agentID uui
return nil, err
}
rows, err := s.db.QueryContext(ctx,
"SELECT agent_id, file_name, content FROM agent_context_files WHERE agent_id = ?"+tClause,
"SELECT agent_id, file_name, content FROM agent_context_files WHERE agent_id = ?"+tClause+" ORDER BY file_name",
append([]any{agentID}, tArgs...)...)
if err != nil {
return nil, err
@@ -97,7 +97,7 @@ func (s *SQLiteAgentStore) GetUserContextFiles(ctx context.Context, agentID uuid
return nil, err
}
rows, err := s.db.QueryContext(ctx,
"SELECT agent_id, user_id, file_name, content FROM user_context_files WHERE agent_id = ? AND user_id = ?"+tClause,
"SELECT agent_id, user_id, file_name, content FROM user_context_files WHERE agent_id = ? AND user_id = ?"+tClause+" ORDER BY file_name",
append([]any{agentID, userID}, tArgs...)...)
if err != nil {
return nil, err
+10 -2
View File
@@ -130,8 +130,16 @@ func (pe *PolicyEngine) FilterTools(
}
}
// Add registry aliases for allowed canonical tools
for alias, canonical := range registry.Aliases() {
// Add registry aliases for allowed canonical tools.
// Sort alias names for deterministic ordering (prompt caching).
aliasMap := registry.Aliases()
aliasList := make([]string, 0, len(aliasMap))
for alias := range aliasMap {
aliasList = append(aliasList, alias)
}
slices.Sort(aliasList)
for _, alias := range aliasList {
canonical := aliasMap[alias]
if !allowedSet[canonical] {
continue
}
+26 -6
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"log/slog"
"maps"
"slices"
"strings"
"sync"
"time"
@@ -222,18 +223,34 @@ func safeExecute(tool Tool, ctx context.Context, args map[string]any) (result *R
// ProviderDefs returns tool definitions for LLM provider APIs.
// Includes alias definitions (same params/description, alias name).
// Results are sorted by tool name for deterministic ordering (prompt caching).
func (r *Registry) ProviderDefs() []providers.ToolDefinition {
r.mu.RLock()
defer r.mu.RUnlock()
defs := make([]providers.ToolDefinition, 0, len(r.tools)+len(r.aliases))
for name, tool := range r.tools {
if r.disabled[name] {
continue
// Sort canonical tool names for deterministic ordering.
sortedNames := make([]string, 0, len(r.tools))
for name := range r.tools {
if !r.disabled[name] {
sortedNames = append(sortedNames, name)
}
defs = append(defs, ToProviderDef(tool))
}
for alias, canonical := range r.aliases {
slices.Sort(sortedNames)
defs := make([]providers.ToolDefinition, 0, len(sortedNames)+len(r.aliases))
for _, name := range sortedNames {
defs = append(defs, ToProviderDef(r.tools[name]))
}
// Sort alias names for deterministic ordering.
sortedAliases := make([]string, 0, len(r.aliases))
for alias := range r.aliases {
sortedAliases = append(sortedAliases, alias)
}
slices.Sort(sortedAliases)
for _, alias := range sortedAliases {
canonical := r.aliases[alias]
if r.disabled[canonical] {
continue
}
@@ -254,6 +271,8 @@ func (r *Registry) ProviderDefs() []providers.ToolDefinition {
}
// List returns all registered canonical tool names (excludes aliases).
// Results are sorted lexicographically for deterministic ordering — critical
// for LLM prompt caching (Anthropic/OpenAI cache by exact prefix match).
func (r *Registry) List() []string {
r.mu.RLock()
defer r.mu.RUnlock()
@@ -263,6 +282,7 @@ func (r *Registry) List() []string {
names = append(names, name)
}
}
slices.Sort(names)
return names
}