package core import ( "encoding/json" "fmt" "llmsproxy/internal/config" ) // compileBillingPrices turns a billing profile into the prices table the billing // plugin consumes — by ASKING THE PLUGIN TO DO IT. // // Why the kernel does not compile prices any more (GitCode issue #1, follow-up // direction): "how a rule becomes a price table" is billing semantics, and it // used to live in TWO places — `internal/billing/compile.go` produced source // entries with zero token prices, while `billing.lua`'s priceFor() applied the // source dimension after the model dimension. Each was defensible alone; together // they silently zeroed every request (cost 0, unpriced_reqs frozen). Splitting // the semantics across two languages is what made that possible. // // So the kernel keeps only what it must own — decoding config.yaml and validating // the DSL — and hands the PROFILE plus the source base URLs to the plugin as // plain DATA. The plugin compiles, and the kernel injects the result through the // existing `prices` sidecar (SetState), which also keeps price-vs-ledger // separation intact. // // A plugin sandbox has no filesystem access, so "the plugin reads the config // file" is not achievable by design; passing the decoded profile is the hook / // interface this architecture provides instead. func (c *Core) compileBillingPrices(profile *config.BillingProfile, lenient bool) (map[string]interface{}, []string, error) { if c.plugins == nil { return nil, nil, fmt.Errorf("plugins not loaded") } if profile == nil { return nil, nil, fmt.Errorf("no billing profile") } // Structs go through JSON so the Lua side receives plain data (maps/slices): // pushGoValue is not a struct encoder, and a silent shape mismatch here would // look exactly like "the plugin priced nothing". rawProfile, err := json.Marshal(profile) if err != nil { return nil, nil, err } var prof map[string]interface{} if err := json.Unmarshal(rawProfile, &prof); err != nil { return nil, nil, err } sources := make([]map[string]interface{}, 0, len(c.cfg.Sources)) for _, s := range c.cfg.Sources { sources = append(sources, map[string]interface{}{"name": s.Name, "base_url": s.BaseURL}) } out, err := c.plugins.Call("billing", "compile_prices", map[string]interface{}{ "profile": prof, "sources": sources, "lenient": lenient, }) if err != nil { return nil, nil, err } m, ok := out.(map[string]interface{}) if !ok { return nil, nil, fmt.Errorf("billing plugin returned an unexpected shape: %T", out) } if msg, hasErr := m["error"]; hasErr { if e := fmt.Sprint(msg); e != "" && e != "" { return nil, nil, fmt.Errorf("%s", e) } } prices, ok := m["prices"].(map[string]interface{}) if !ok { return nil, nil, fmt.Errorf("billing plugin returned no prices table") } var unmatched []string if arr, ok := m["unmatched_rules"].([]interface{}); ok { for _, v := range arr { unmatched = append(unmatched, fmt.Sprint(v)) } } return prices, unmatched, nil } // CompileBillingPrices is compileBillingPrices for callers outside the package // (the rules editor validates an edit before persisting it). func (c *Core) CompileBillingPrices(profile *config.BillingProfile, lenient bool) (map[string]interface{}, []string, error) { return c.compileBillingPrices(profile, lenient) }