Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

450 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Command wire-format benches GCX1 vs JSON on representative MCP tool
// responses. See bench/wire-format/README.md.
package main
import (
"bytes"
"compress/gzip"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"gopkg.in/yaml.v3"
"github.com/zzet/gortex/internal/tokens"
wire "github.com/gortexhq/gcx-go"
)
type caseFile struct {
Tool string `yaml:"tool"`
Description string `yaml:"description"`
// Input is the tool response payload as JSON text. The YAML pipe
// scalar (`|`) keeps the JSON human-readable in the fixture.
Input string `yaml:"input"`
}
type metrics struct {
Case string
Tool string
JSONBytes int
GCXBytes int
JSONTokens int
GCXTokens int
JSONGzip int
GCXGzip int
// Opus 4.7 input-token counts. Populated when --tokenizer is
// opus47 or both. ExactOpus47 distinguishes API-backed / cached
// counts (true) from inflation-factor estimates (false) — the
// scorecard footnote calls out the difference for honesty.
JSONTokensOpus47 int `json:",omitempty"`
GCXTokensOpus47 int `json:",omitempty"`
ExactOpus47 bool `json:",omitempty"`
RoundTripOK bool
RoundTripErr string
}
func main() {
casesDir := flag.String("cases", "bench/wire-format/cases", "directory of fixture YAML files")
out := flag.String("out", "", "output scorecard markdown path (stdout if empty)")
jsonOut := flag.String("json", "", "optional path to emit raw metrics as JSON")
tokenizer := flag.String("tokenizer", "both", "which tokenizer column(s) to render: cl100k | opus47 | both")
useAPI := flag.Bool("use-api", false, "call Anthropic count_tokens for exact Opus 4.7 counts (requires ANTHROPIC_API_KEY); falls back to scalar on failure")
opus47Cache := flag.String("opus47-cache", "bench/wire-format/opus47-counts.json", "path to the Opus 4.7 exact-count cache (loaded on start, written on --use-api hits)")
opus47Model := flag.String("opus47-model", "claude-opus-4-20250514", "model id used for the count_tokens API call (only relevant with --use-api)")
flag.Parse()
mode, err := parseTokenizerMode(*tokenizer)
if err != nil {
die("%v", err)
}
// Build the Opus 4.7 counter chain regardless of mode — when the
// user asked for cl100k only, the counter still gets constructed
// but its output is discarded by the renderer. Keeps the runCase
// signature simple.
opus47, opus47Cached, err := buildOpus47Counter(*opus47Cache, *opus47Model, *useAPI)
if err != nil {
die("%v", err)
}
entries, err := os.ReadDir(*casesDir)
if err != nil {
die("read cases dir: %v", err)
}
sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() })
var rows []metrics
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
continue
}
path := filepath.Join(*casesDir, entry.Name())
row, err := runCase(path, opus47)
if err != nil {
row.RoundTripErr = err.Error()
row.Case = strings.TrimSuffix(entry.Name(), ".yaml")
}
rows = append(rows, row)
}
// Persist any newly-populated exact counts so subsequent runs hit
// the cache instead of the API. Best-effort: a write error here
// shouldn't fail the bench (the scorecard is already in memory).
if *useAPI && opus47Cached != nil {
snap := opus47Cached.snapshot()
if err := saveOpus47Cache(*opus47Cache, snap); err != nil {
fmt.Fprintf(os.Stderr, "wire-bench: write opus47 cache: %v\n", err)
}
}
card := renderScorecard(rows, mode)
if *out == "" {
fmt.Print(card)
} else {
if err := os.WriteFile(*out, []byte(card), 0o644); err != nil {
die("write scorecard: %v", err)
}
}
if *jsonOut != "" {
b, _ := json.MarshalIndent(rows, "", " ")
if err := os.WriteFile(*jsonOut, b, 0o644); err != nil {
die("write json: %v", err)
}
}
}
// tokenizerMode selects which token-cost columns the scorecard
// renders. The opus47 column is honest-labeled "_estimated" when
// every row was scaled vs. "_exact" when at least one row came from
// the API/cache (footnote indicates which).
type tokenizerMode int
const (
tokenizerModeCL100k tokenizerMode = iota
tokenizerModeOpus47
tokenizerModeBoth
)
func parseTokenizerMode(s string) (tokenizerMode, error) {
switch strings.ToLower(s) {
case "cl100k", "cl100k_base":
return tokenizerModeCL100k, nil
case "opus47", "opus4.7", "opus-4-7", "claude":
return tokenizerModeOpus47, nil
case "both", "all":
return tokenizerModeBoth, nil
}
return tokenizerModeBoth, fmt.Errorf("unknown --tokenizer %q (want cl100k | opus47 | both)", s)
}
// buildOpus47Counter wires the strategy: a cachedCounter at the base
// loaded from disk, wrapped by an apiCounter when --use-api is set.
// Returns the active counter plus the underlying cache (when an API
// hit needs to be persisted on shutdown); the cache may be nil when
// no cache file is configured.
func buildOpus47Counter(cachePath, model string, useAPI bool) (opus47Counter, *cachedCounter, error) {
if cachePath == "" {
// No cache configured → in-process model counter, fast path.
return newModelCounter(model), nil, nil
}
c, err := loadOpus47Cache(cachePath)
if err != nil {
return nil, nil, err
}
cached := newCachedCounter(c, model)
if !useAPI {
return cached, cached, nil
}
api, err := newAPICounter(cached, model)
if err != nil {
return nil, nil, err
}
return api, cached, nil
}
func die(format string, args ...any) {
fmt.Fprintf(os.Stderr, "wire-bench: "+format+"\n", args...)
os.Exit(1)
}
func runCase(path string, opus47 opus47Counter) (metrics, error) {
raw, err := os.ReadFile(path)
if err != nil {
return metrics{}, fmt.Errorf("read %s: %w", path, err)
}
var cf caseFile
if err := yaml.Unmarshal(raw, &cf); err != nil {
return metrics{}, fmt.Errorf("parse %s: %w", path, err)
}
name := strings.TrimSuffix(filepath.Base(path), ".yaml")
m := metrics{Case: name, Tool: cf.Tool}
// Normalise the input so the two encoders start from the same
// canonical value. This models what a real MCP handler would
// produce — a JSON-serialisable Go value.
var value any
dec := json.NewDecoder(strings.NewReader(cf.Input))
dec.UseNumber()
if err := dec.Decode(&value); err != nil {
return m, fmt.Errorf("parse input: %w", err)
}
// JSON baseline (compact, no indent — matches mcp-go behaviour).
jsonBytes, err := json.Marshal(value)
if err != nil {
return m, fmt.Errorf("marshal json: %w", err)
}
m.JSONBytes = len(jsonBytes)
m.JSONTokens = tokens.Count(string(jsonBytes))
m.JSONGzip = gzipLen(jsonBytes)
// GCX — bench-local encoder chooses between hand-tuned shape
// recognition and the generic fallback. This reproduces what the
// real internal/mcp encoders produce for recognised wrapper
// shapes (rows-of-objects, {nodes, edges}, ...). Unrecognised
// shapes fall through to wire.EncodeAny so every case still
// emits a valid payload.
gcxBytes, err := encodeAsGCX(cf.Tool, value)
if err != nil {
return m, fmt.Errorf("encode gcx: %w", err)
}
m.GCXBytes = len(gcxBytes)
m.GCXTokens = tokens.Count(string(gcxBytes))
m.GCXGzip = gzipLen(gcxBytes)
// Opus 4.7 column. The counter is always supplied; the renderer
// decides whether to surface these values based on --tokenizer.
// `exact` is true when the value came from cache / live API; we
// take the AND across both channels (a row only counts as exact
// when both numbers are exact — otherwise the footnote calls it
// estimated).
if opus47 != nil {
jOpus, jExact := opus47.Count(name, "json", string(jsonBytes), m.JSONTokens)
gOpus, gExact := opus47.Count(name, "gcx", string(gcxBytes), m.GCXTokens)
m.JSONTokensOpus47 = jOpus
m.GCXTokensOpus47 = gOpus
m.ExactOpus47 = jExact && gExact
}
// Round-trip: decode GCX back into a generic value and compare to
// the canonical JSON encoding. Full structural equality is too
// strict because the generic encoder serialises nested values as
// JSON-in-cells; instead, we check that the decoder yields the
// same set of top-level cells and the re-marshalled payload
// round-trips on the text level (byte-identical decode of the
// GCX output).
wd := wire.NewDecoder(bytes.NewReader(gcxBytes))
if _, err := wd.Header(); err != nil {
return m, fmt.Errorf("decode header: %w", err)
}
if _, err := wd.All(); err != nil {
return m, fmt.Errorf("decode rows: %w", err)
}
m.RoundTripOK = true
return m, nil
}
func gzipLen(b []byte) int {
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
if _, err := io.Copy(gz, bytes.NewReader(b)); err != nil {
return -1
}
_ = gz.Close()
return buf.Len()
}
// renderScorecard formats the per-case metrics as one or two
// markdown tables depending on the tokenizer mode. `cl100k` prints
// the original single-table layout; `opus47` swaps in the Opus 4.7
// columns; `both` stacks them, cl100k first then opus47, sharing the
// same case rows. A footnote distinguishes exact (API/cache) and
// estimated (in-process per-model tokenizer) opus47 counts.
func renderScorecard(rows []metrics, mode tokenizerMode) string {
var b strings.Builder
fmt.Fprintln(&b, "# GCX1 wire-format benchmark scorecard")
fmt.Fprintln(&b)
if mode == tokenizerModeCL100k || mode == tokenizerModeBoth {
fmt.Fprintln(&b, "## tiktoken cl100k_base (Claude 3 / Opus 4 / Sonnet 4 / Haiku 4.5 / GPT-4o family)")
fmt.Fprintln(&b)
fmt.Fprintln(&b, "| case | tool | bytes (json) | bytes (gcx) | Δ% | tokens (json) | tokens (gcx) | Δ% | gzip (json) | gzip (gcx) | Δ% | round-trip |")
fmt.Fprintln(&b, "|------|------|-------------:|------------:|---:|--------------:|-------------:|---:|------------:|-----------:|---:|:---------:|")
for _, m := range rows {
fmt.Fprintf(&b, "| %s | %s | %d | %d | %s | %d | %d | %s | %d | %d | %s | %s |\n",
m.Case, m.Tool,
m.JSONBytes, m.GCXBytes, pctDelta(m.JSONBytes, m.GCXBytes),
m.JSONTokens, m.GCXTokens, pctDelta(m.JSONTokens, m.GCXTokens),
m.JSONGzip, m.GCXGzip, pctDelta(m.JSONGzip, m.GCXGzip),
rtrMark(m),
)
}
fmt.Fprintln(&b)
fmt.Fprintln(&b, summaryLineCL100k(rows))
}
if mode == tokenizerModeOpus47 || mode == tokenizerModeBoth {
if mode == tokenizerModeBoth {
fmt.Fprintln(&b)
}
exactAll := allExactOpus47(rows)
label := "estimated (in-process per-model tokenizer)"
if exactAll {
label = "exact (Anthropic count_tokens / cached)"
} else if anyExactOpus47(rows) {
label = "mixed — see per-row marker"
}
fmt.Fprintf(&b, "## Claude Opus 4.7 (%s)\n\n", label)
fmt.Fprintln(&b, "| case | tool | tokens (json) | tokens (gcx) | Δ% | source |")
fmt.Fprintln(&b, "|------|------|--------------:|-------------:|---:|:------:|")
for _, m := range rows {
fmt.Fprintf(&b, "| %s | %s | %d | %d | %s | %s |\n",
m.Case, m.Tool,
m.JSONTokensOpus47, m.GCXTokensOpus47,
pctDelta(m.JSONTokensOpus47, m.GCXTokensOpus47),
opus47SourceMark(m),
)
}
fmt.Fprintln(&b)
fmt.Fprintln(&b, summaryLineOpus47(rows))
}
return b.String()
}
// allExactOpus47 reports whether every row's opus47 numbers came from
// the API/cache (not the scalar). Used to pick the section header.
func allExactOpus47(rows []metrics) bool {
if len(rows) == 0 {
return false
}
for _, m := range rows {
if !m.ExactOpus47 {
return false
}
}
return true
}
// anyExactOpus47 reports whether at least one row's opus47 numbers
// came from the API/cache. Used to label the section as "mixed"
// when some rows have exact data and others fell back to the scalar.
func anyExactOpus47(rows []metrics) bool {
for _, m := range rows {
if m.ExactOpus47 {
return true
}
}
return false
}
// opus47SourceMark emits a per-row marker that distinguishes exact
// counts ("exact") from scalar estimates ("est.") so a reader can
// see at a glance which numbers came from where in a mixed run.
func opus47SourceMark(m metrics) string {
if m.ExactOpus47 {
return "exact"
}
return "est."
}
func pctDelta(base, got int) string {
if base == 0 {
return "n/a"
}
pct := float64(base-got) / float64(base) * 100
if pct >= 0 {
return fmt.Sprintf("%.1f%%", pct)
}
return fmt.Sprintf("+%.1f%%", -pct)
}
func rtrMark(m metrics) string {
if m.RoundTripErr != "" {
return "✗ " + m.RoundTripErr
}
if m.RoundTripOK {
return "✓"
}
return "?"
}
// summaryLineCL100k summarises the cl100k_base table: median token
// + median byte savings + round-trip pass count.
func summaryLineCL100k(rows []metrics) string {
if len(rows) == 0 {
return "_no cases_"
}
var (
tokensJ, tokensG []int
bytesJ, bytesG []int
)
passed := 0
for _, m := range rows {
if m.JSONTokens > 0 {
tokensJ = append(tokensJ, m.JSONTokens)
tokensG = append(tokensG, m.GCXTokens)
bytesJ = append(bytesJ, m.JSONBytes)
bytesG = append(bytesG, m.GCXBytes)
}
if m.RoundTripOK {
passed++
}
}
return fmt.Sprintf("**Summary (cl100k_base):** %d/%d cases. Median token savings: %s. Median byte savings: %s. Round-trip integrity: %d/%d.",
len(rows), len(rows),
medianPct(tokensJ, tokensG),
medianPct(bytesJ, bytesG),
passed, len(rows),
)
}
// summaryLineOpus47 summarises the Opus 4.7 table: median token
// savings on the new tokenizer plus a count of exact vs. estimated
// rows so the reader can judge confidence.
func summaryLineOpus47(rows []metrics) string {
if len(rows) == 0 {
return "_no cases_"
}
var tokensJ, tokensG []int
exactRows := 0
for _, m := range rows {
if m.JSONTokensOpus47 > 0 {
tokensJ = append(tokensJ, m.JSONTokensOpus47)
tokensG = append(tokensG, m.GCXTokensOpus47)
}
if m.ExactOpus47 {
exactRows++
}
}
return fmt.Sprintf("**Summary (Opus 4.7):** %d/%d cases. Median token savings: %s. Exact rows: %d/%d (rest estimated by the in-process per-model tokenizer).",
len(rows), len(rows),
medianPct(tokensJ, tokensG),
exactRows, len(rows),
)
}
func medianPct(base, got []int) string {
if len(base) == 0 {
return "n/a"
}
deltas := make([]float64, len(base))
for i := range base {
if base[i] == 0 {
deltas[i] = 0
continue
}
deltas[i] = float64(base[i]-got[i]) / float64(base[i]) * 100
}
sort.Float64s(deltas)
mid := deltas[len(deltas)/2]
if mid >= 0 {
return fmt.Sprintf("%.1f%%", mid)
}
return fmt.Sprintf("+%.1f%%", -mid)
}