221 lines
6.7 KiB
Go
221 lines
6.7 KiB
Go
// Command zeropipe renders a deployment's shipping pipeline onto the deployment's zero-cost pair,
|
|
// so the chain can be smoked without buying anything. It is a port of the platform's own renderer
|
|
// (platform/internal/runner/bankapply_live_test.go, zeroCostPipeline) rather than the sed over
|
|
// `model:` the stand recipe gives: the gates carry models too, and on a pipeline-c1 template the
|
|
// terminologist and the classifier resolve to a paid provider with every stage already zeroed.
|
|
//
|
|
// It also answers the question a rendered file cannot answer about itself: -check counts, in both
|
|
// the shipping file and the rendered one, the models this deployment would have to pay for. A
|
|
// silent renderer and a clean pipeline look the same until the paid count beside the zero is
|
|
// printed too.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type modelsFile struct {
|
|
Providers map[string]struct {
|
|
Kind string `yaml:"kind"`
|
|
} `yaml:"providers"`
|
|
Models map[string]struct {
|
|
Provider string `yaml:"provider"`
|
|
} `yaml:"models"`
|
|
}
|
|
|
|
func main() {
|
|
pipelinePath := flag.String("pipeline", "", "the deployment's shipping pipeline")
|
|
modelsPath := flag.String("models", "", "the models registry the book points at")
|
|
out := flag.String("out", "", "where to write the rendered zero-cost pipeline")
|
|
flag.Parse()
|
|
if *pipelinePath == "" || *modelsPath == "" || *out == "" {
|
|
die("usage: zeropipe -pipeline <c1.yaml> -models <models.yaml> -out <zero.yaml>")
|
|
}
|
|
|
|
models := readModels(*modelsPath)
|
|
free, paid := partition(models)
|
|
if free == "" {
|
|
die("this deployment's %s declares no provider of kind `local`: the probe would have to buy its calls", *modelsPath)
|
|
}
|
|
|
|
raw, err := os.ReadFile(*pipelinePath)
|
|
if err != nil {
|
|
die("the pipeline cannot be read (%s): %v", *pipelinePath, err)
|
|
}
|
|
var pipe map[string]any
|
|
if err := yaml.Unmarshal(raw, &pipe); err != nil {
|
|
die("%v", err)
|
|
}
|
|
|
|
stages, ok := pipe["stages"].([]any)
|
|
if !ok || len(stages) == 0 {
|
|
die("the pipeline declares no stages: %s", *pipelinePath)
|
|
}
|
|
for _, s := range stages {
|
|
stage, ok := s.(map[string]any)
|
|
if !ok {
|
|
die("a stage of %s is not a mapping", *pipelinePath)
|
|
}
|
|
stage["model"] = free
|
|
delete(stage, "escalate_to")
|
|
delete(stage, "label_models")
|
|
}
|
|
// The gates carry models of their own, and both of them: `classify_model` falls back to `model`
|
|
// only when it is empty, so a pipeline that sets it would resolve a paid model here with `model`
|
|
// already zeroed.
|
|
if gates, ok := pipe["gates"].(map[string]any); ok {
|
|
for _, g := range gates {
|
|
gate, ok := g.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if _, has := gate["model"]; has {
|
|
gate["model"] = free
|
|
}
|
|
if _, has := gate["classify_model"]; has {
|
|
gate["classify_model"] = free
|
|
}
|
|
delete(gate, "escalate_to")
|
|
}
|
|
}
|
|
if esc, ok := pipe["escalation"].(map[string]any); ok {
|
|
esc["budget_usd"] = 0
|
|
delete(esc, "chains")
|
|
}
|
|
// The bank contour's one deployment-provided input is resolved against the directory of the
|
|
// pipeline file, and this render is about to move: absolutise it, and refuse rather than let the
|
|
// engine die at its write-path guard with a message that reads like somebody else's defect.
|
|
if mining, ok := pipe["mining"].(map[string]any); ok {
|
|
if rel, ok := mining["contrast_path"].(string); ok && rel != "" {
|
|
abs := rel
|
|
if !filepath.IsAbs(abs) {
|
|
abs = filepath.Join(filepath.Dir(*pipelinePath), abs)
|
|
}
|
|
if _, err := os.Stat(abs); err != nil {
|
|
die("the pipeline enables the bank contour and names %s, which is not on this host", abs)
|
|
}
|
|
mining["contrast_path"] = abs
|
|
}
|
|
}
|
|
|
|
rendered, err := yaml.Marshal(pipe)
|
|
if err != nil {
|
|
die("%v", err)
|
|
}
|
|
if err := os.WriteFile(*out, rendered, 0o644); err != nil {
|
|
die("%v", err)
|
|
}
|
|
|
|
var shipping map[string]any
|
|
if err := yaml.Unmarshal(raw, &shipping); err != nil {
|
|
die("%v", err)
|
|
}
|
|
var check map[string]any
|
|
if err := yaml.Unmarshal(rendered, &check); err != nil {
|
|
die("the rendered pipeline does not parse back: %v", err)
|
|
}
|
|
shippingHits := hits(shipping, paid)
|
|
renderedHits := hits(check, paid)
|
|
fmt.Printf("free model: %s\n", free)
|
|
fmt.Printf("paid models this deployment declares: %d (%s)\n", len(paid), strings.Join(paid, " "))
|
|
fmt.Printf("paid models REACHABLE in %s: %d\n", filepath.Base(*pipelinePath), total(shippingHits))
|
|
for _, name := range sorted(shippingHits) {
|
|
fmt.Printf(" %s: %d\n", name, shippingHits[name])
|
|
}
|
|
fmt.Printf("paid models REACHABLE in %s: %d\n", filepath.Base(*out), total(renderedHits))
|
|
for _, name := range sorted(renderedHits) {
|
|
fmt.Printf(" %s: %d\n", name, renderedHits[name])
|
|
}
|
|
if total(renderedHits) != 0 {
|
|
die("the rendered pipeline still names a paid model: the probe would buy its calls")
|
|
}
|
|
if total(shippingHits) == 0 {
|
|
die("the shipping pipeline names no paid model either: this check proves nothing, and the "+
|
|
"zero in the render is the instrument staying silent rather than the render being clean (%s)", *pipelinePath)
|
|
}
|
|
}
|
|
|
|
func readModels(path string) modelsFile {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
die("the models file cannot be read (%s): %v", path, err)
|
|
}
|
|
var m modelsFile
|
|
if err := yaml.Unmarshal(raw, &m); err != nil {
|
|
die("%v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// partition names the cheapest truth about this registry: which model costs nothing because its
|
|
// provider runs on this host, and which ones a call would be billed for.
|
|
func partition(m modelsFile) (free string, paid []string) {
|
|
for name, model := range m.Models {
|
|
if m.Providers[model.Provider].Kind == "local" {
|
|
if free == "" || name < free {
|
|
free = name
|
|
}
|
|
continue
|
|
}
|
|
paid = append(paid, name)
|
|
}
|
|
sort.Strings(paid)
|
|
return free, paid
|
|
}
|
|
|
|
// hits counts paid models where they are REACHABLE — as values in the parsed configuration, not as
|
|
// text. Counting text would count the comments, and a rendered file has none: the render would look
|
|
// clean the moment the comments were dropped, whether or not a paid model still stood in a gate.
|
|
func hits(node any, names []string) map[string]int {
|
|
out := map[string]int{}
|
|
var walk func(any)
|
|
walk = func(n any) {
|
|
switch v := n.(type) {
|
|
case map[string]any:
|
|
for _, child := range v {
|
|
walk(child)
|
|
}
|
|
case []any:
|
|
for _, child := range v {
|
|
walk(child)
|
|
}
|
|
case string:
|
|
for _, name := range names {
|
|
if v == name {
|
|
out[name]++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
walk(node)
|
|
return out
|
|
}
|
|
|
|
func total(m map[string]int) int {
|
|
sum := 0
|
|
for _, v := range m {
|
|
sum += v
|
|
}
|
|
return sum
|
|
}
|
|
|
|
func sorted(m map[string]int) []string {
|
|
out := make([]string, 0, len(m))
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func die(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
|
os.Exit(1)
|
|
}
|