perf: reduce API costs with 6 optimization fixes
1. read_file: truncate output to 10K chars (was unlimited) 2. max_tool_iterations: reduce default from 50 to 15 3. web_fetch: reduce default from 50K to 20K chars 4. Anthropic: add cache_control on last tool definition for prompt caching 5. Token usage logging: track and log cumulative input/output tokens per session 6. max_tokens: reduce default from 32K to 8K, resolve real context window per model Made-with: Cursor
This commit is contained in:
parent
6b66da52f0
commit
a99cf50f27
6 changed files with 63 additions and 8 deletions
|
|
@ -148,6 +148,8 @@ func NewAgentInstance(
|
||||||
|
|
||||||
candidates := providers.ResolveCandidatesWithLookup(modelCfg, defaults.Provider, resolveFromModelList)
|
candidates := providers.ResolveCandidatesWithLookup(modelCfg, defaults.Provider, resolveFromModelList)
|
||||||
|
|
||||||
|
contextWindow := resolveContextWindow(model, maxTokens)
|
||||||
|
|
||||||
return &AgentInstance{
|
return &AgentInstance{
|
||||||
ID: agentID,
|
ID: agentID,
|
||||||
Name: agentName,
|
Name: agentName,
|
||||||
|
|
@ -157,7 +159,7 @@ func NewAgentInstance(
|
||||||
MaxIterations: maxIter,
|
MaxIterations: maxIter,
|
||||||
MaxTokens: maxTokens,
|
MaxTokens: maxTokens,
|
||||||
Temperature: temperature,
|
Temperature: temperature,
|
||||||
ContextWindow: maxTokens,
|
ContextWindow: contextWindow,
|
||||||
Provider: provider,
|
Provider: provider,
|
||||||
Sessions: sessionsManager,
|
Sessions: sessionsManager,
|
||||||
ContextBuilder: contextBuilder,
|
ContextBuilder: contextBuilder,
|
||||||
|
|
@ -223,3 +225,29 @@ func expandHome(path string) string {
|
||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveContextWindow returns the actual context window size for a model.
|
||||||
|
// This is used for summarization thresholds and should reflect the model's
|
||||||
|
// real capacity rather than the output max_tokens setting.
|
||||||
|
func resolveContextWindow(model string, maxTokens int) int {
|
||||||
|
m := strings.ToLower(model)
|
||||||
|
switch {
|
||||||
|
case strings.Contains(m, "claude"):
|
||||||
|
return 200000
|
||||||
|
case strings.Contains(m, "gpt-5"), strings.Contains(m, "gpt-4"):
|
||||||
|
return 128000
|
||||||
|
case strings.Contains(m, "gemini"):
|
||||||
|
return 1000000
|
||||||
|
case strings.Contains(m, "deepseek"):
|
||||||
|
return 64000
|
||||||
|
case strings.Contains(m, "llama"):
|
||||||
|
return 128000
|
||||||
|
case strings.Contains(m, "qwen"):
|
||||||
|
return 32000
|
||||||
|
default:
|
||||||
|
if maxTokens > 32000 {
|
||||||
|
return maxTokens
|
||||||
|
}
|
||||||
|
return 32000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ func registerSharedTools(
|
||||||
} else if searchTool != nil {
|
} else if searchTool != nil {
|
||||||
agent.Tools.Register(searchTool)
|
agent.Tools.Register(searchTool)
|
||||||
}
|
}
|
||||||
fetchTool, err := tools.NewWebFetchToolWithProxy(50000, cfg.Tools.Web.Proxy, cfg.Tools.Web.FetchLimitBytes)
|
fetchTool, err := tools.NewWebFetchToolWithProxy(20000, cfg.Tools.Web.Proxy, cfg.Tools.Web.FetchLimitBytes)
|
||||||
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 {
|
||||||
|
|
@ -661,6 +661,7 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
) (string, int, error) {
|
) (string, int, error) {
|
||||||
iteration := 0
|
iteration := 0
|
||||||
var finalContent string
|
var finalContent string
|
||||||
|
var totalInputTokens, totalOutputTokens int
|
||||||
|
|
||||||
for iteration < agent.MaxIterations {
|
for iteration < agent.MaxIterations {
|
||||||
iteration++
|
iteration++
|
||||||
|
|
@ -805,6 +806,11 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
|
|
||||||
go al.handleReasoning(ctx, response.Reasoning, opts.Channel, al.targetReasoningChannelID(opts.Channel))
|
go al.handleReasoning(ctx, response.Reasoning, opts.Channel, al.targetReasoningChannelID(opts.Channel))
|
||||||
|
|
||||||
|
if response.Usage != nil {
|
||||||
|
totalInputTokens += response.Usage.PromptTokens
|
||||||
|
totalOutputTokens += response.Usage.CompletionTokens
|
||||||
|
}
|
||||||
|
|
||||||
logger.DebugCF("agent", "LLM response",
|
logger.DebugCF("agent", "LLM response",
|
||||||
map[string]any{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
|
|
@ -968,6 +974,17 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if totalInputTokens > 0 || totalOutputTokens > 0 {
|
||||||
|
logger.InfoCF("agent", "Session token usage",
|
||||||
|
map[string]any{
|
||||||
|
"agent_id": agent.ID,
|
||||||
|
"iterations": iteration,
|
||||||
|
"input_tokens": totalInputTokens,
|
||||||
|
"output_tokens": totalOutputTokens,
|
||||||
|
"total_tokens": totalInputTokens + totalOutputTokens,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return finalContent, iteration, nil
|
return finalContent, iteration, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,9 @@ func DefaultConfig() *Config {
|
||||||
RestrictToWorkspace: true,
|
RestrictToWorkspace: true,
|
||||||
Provider: "",
|
Provider: "",
|
||||||
Model: "",
|
Model: "",
|
||||||
MaxTokens: 32768,
|
MaxTokens: 8192,
|
||||||
Temperature: nil, // nil means use provider default
|
Temperature: nil, // nil means use provider default
|
||||||
MaxToolIterations: 50,
|
MaxToolIterations: 15,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Bindings: []AgentBinding{},
|
Bindings: []AgentBinding{},
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,7 @@ func buildParams(
|
||||||
|
|
||||||
func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam {
|
func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam {
|
||||||
result := make([]anthropic.ToolUnionParam, 0, len(tools))
|
result := make([]anthropic.ToolUnionParam, 0, len(tools))
|
||||||
for _, t := range tools {
|
for i, t := range tools {
|
||||||
tool := anthropic.ToolParam{
|
tool := anthropic.ToolParam{
|
||||||
Name: t.Function.Name,
|
Name: t.Function.Name,
|
||||||
InputSchema: anthropic.ToolInputSchemaParam{
|
InputSchema: anthropic.ToolInputSchemaParam{
|
||||||
|
|
@ -256,6 +256,9 @@ func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam {
|
||||||
}
|
}
|
||||||
tool.InputSchema.Required = required
|
tool.InputSchema.Required = required
|
||||||
}
|
}
|
||||||
|
if i == len(tools)-1 {
|
||||||
|
tool.CacheControl = anthropic.NewCacheControlEphemeralParam()
|
||||||
|
}
|
||||||
result = append(result, anthropic.ToolUnionParam{OfTool: &tool})
|
result = append(result, anthropic.ToolUnionParam{OfTool: &tool})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,8 @@ func (t *ReadFileTool) Parameters() map[string]any {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxReadFileChars = 10000
|
||||||
|
|
||||||
func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
path, ok := args["path"].(string)
|
path, ok := args["path"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -127,7 +129,12 @@ func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrorResult(err.Error())
|
return ErrorResult(err.Error())
|
||||||
}
|
}
|
||||||
return NewToolResult(string(content))
|
|
||||||
|
text := string(content)
|
||||||
|
if len(text) > maxReadFileChars {
|
||||||
|
text = text[:maxReadFileChars] + fmt.Sprintf("\n\n... (truncated, showing %d of %d chars. Use exec with head/tail/sed for specific sections)", maxReadFileChars, len(content))
|
||||||
|
}
|
||||||
|
return NewToolResult(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
type WriteFileTool struct {
|
type WriteFileTool struct {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ const (
|
||||||
perplexityTimeout = 30 * time.Second // Perplexity (LLM-based, slower)
|
perplexityTimeout = 30 * time.Second // Perplexity (LLM-based, slower)
|
||||||
fetchTimeout = 60 * time.Second // WebFetchTool
|
fetchTimeout = 60 * time.Second // WebFetchTool
|
||||||
|
|
||||||
defaultMaxChars = 50000
|
defaultMaxChars = 20000
|
||||||
maxRedirects = 5
|
maxRedirects = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue