Files
Nityananda GohainandGitHub 61ad7bb3b3 feat: add signozllmcostprocessor (#797)
## Pull Request

### Summary
Adds llm cost processor

### Related Issues
FIxes https://github.com/SigNoz/engineering-pod/issues/4358

### Resource Impact
**Will this change affect the application's resource usage?**
- [x] Yes
- [ ] No

If **Yes**, describe:
- **Resources impacted:** (CPU, Memory, Disk, Network, etc.)
- **Expected change:** (e.g., +10% CPU, -150MB RAM)
- **Benchmarks:** Provide before/after results and how they were
measured.
2026-05-18 04:33:46 +00:00

107 lines
3.3 KiB
Go

package signozllmpricingprocessor // import "github.com/SigNoz/signoz-otel-collector/processor/signozllmpricingprocessor"
import (
"fmt"
"path"
)
type CacheMode string
const (
CacheModeSubtract CacheMode = "subtract" // cache_read is already counted inside input_tokens (e.g. OpenAI)
CacheModeAdditive CacheMode = "additive" // cache_read is separate from input_tokens (e.g. Anthropic)
)
// Config is the top-level processor configuration.
type Config struct {
// Attrs maps logical token-field names to the actual span attribute keys.
Attrs AttrMapping `mapstructure:"attrs"`
DefaultPricing PricingConfig `mapstructure:"default_pricing"`
// OutputAttrs maps logical cost-field names to the span attribute keys
// where computed costs are written.
OutputAttrs OutputMapping `mapstructure:"output_attrs"`
}
type AttrMapping struct {
Model string `mapstructure:"model"`
In string `mapstructure:"in"`
Out string `mapstructure:"out"`
CacheRead string `mapstructure:"cache_read"`
CacheWrite string `mapstructure:"cache_write"`
}
type PricingConfig struct {
Rules []PricingRule `mapstructure:"rules"`
}
type PricingRule struct {
Name string `mapstructure:"name"`
Pattern []string `mapstructure:"pattern"`
Cache PricingRuleCache `mapstructure:"cache"`
In float64 `mapstructure:"in"`
Out float64 `mapstructure:"out"`
}
type PricingRuleCache struct {
// Mode controls how cache tokens are factored into the cost:
// "subtract" — cache read tokens are already counted inside input_tokens;
// billed_input = input_tokens - cache_read.
// "additive" — cache read/write are separate from input_tokens;
// all four buckets are billed independently.
Mode CacheMode `mapstructure:"mode"`
// Per-million-token prices (USD) for cached reads/writes.
Read float64 `mapstructure:"read"`
Write float64 `mapstructure:"write"`
}
type OutputMapping struct {
In string `mapstructure:"in"`
Out string `mapstructure:"out"`
CacheRead string `mapstructure:"cache_read"`
CacheWrite string `mapstructure:"cache_write"`
Total string `mapstructure:"total"`
}
func (c *CacheMode) String() string {
if c == nil {
return ""
}
return string(*c)
}
func (c *Config) Validate() error {
if c.Attrs.Model == "" {
return fmt.Errorf("attrs.model must not be empty")
}
for i, r := range c.DefaultPricing.Rules {
if len(r.Pattern) == 0 {
return fmt.Errorf("default_pricing.rules[%d]: pattern must not be empty", i)
}
for j, p := range r.Pattern {
if p == "" {
return fmt.Errorf("default_pricing.rules[%d].pattern[%d]: pattern must not be empty", i, j)
}
if _, err := path.Match(p, ""); err != nil {
return fmt.Errorf("default_pricing.rules[%d].pattern[%d]: invalid glob pattern %q: %w", i, j, p, err)
}
}
if r.Cache.Mode != CacheModeSubtract && r.Cache.Mode != CacheModeAdditive {
return fmt.Errorf("default_pricing.rules[%d] (pattern=%v): cache.mode must be %q or %q, got %q",
i, r.Pattern, CacheModeSubtract, CacheModeAdditive, r.Cache.Mode)
}
if r.In < 0 || r.Out < 0 || r.Cache.Read < 0 || r.Cache.Write < 0 {
return fmt.Errorf("default_pricing.rules[%d] (pattern=%v): prices must be non-negative", i, r.Pattern)
}
}
if c.OutputAttrs.Total == "" {
return fmt.Errorf("output_attrs.total must not be empty")
}
return nil
}