From 82adf28cf01ecf6b77b9d8f9a0f23feec15a99e7 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Thu, 23 Jul 2026 20:16:20 +0530 Subject: [PATCH 1/8] docs(examples): fix Compress/PromptCompress return signatures in README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The examples showed 'out, err := tok.Compress(...)' but the actual API returns (string, Stats) and (string, PromptStats) respectively — no error return. Fix the examples to match the real signatures. --- examples/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/README.md b/examples/README.md index afcd12adc..8f6121c70 100644 --- a/examples/README.md +++ b/examples/README.md @@ -21,14 +21,15 @@ import "github.com/GrayCodeAI/tok" in := "Hey, could you please help me figure out why this React component keeps " + "re-rendering every time the props change?" -out, err := tok.Compress(in, tok.Aggressive) +out, stats := tok.Compress(in, tok.Aggressive) // out: "React component re-renders on prop change. Why?" +// stats: compression ratio, token savings, etc. ``` `PromptCompress` lets you pick an intensity level directly: ```go -out, err := tok.PromptCompress(in, tok.IntensityFull) +out, stats := tok.PromptCompress(in, tok.IntensityFull) // intensities: tok.IntensityLite, tok.IntensityFull, tok.IntensityUltra ``` From a553769f6c4d89c5c3d3f6896b38a2798225a67e Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Fri, 24 Jul 2026 04:24:28 +0530 Subject: [PATCH 2/8] feat: add EstimateCost(text, model) and fix doc examples tok.EstimateCost was referenced across README, QUICK_START, DEPLOYMENT, AGENT_INTEGRATION, TROUBLESHOOTING, PERFORMANCE, and examples/README but did not exist. Adds the missing function (estimates USD cost of text as input tokens via EstimateTokensForModel + GetModelPricing) with tests, and fixes the doc examples to match the actual API: - Compress/PromptCompress return (string, Stats)/(string, PromptStats), not (string, error): corrected out, err := to out, _ := / out, stats := - PromptCompress single-value assignments now use out, _ := - EstimateCost now takes text (string) and returns float64 (single value) - Fixed 3-value Compress misuses in architecture.md --- README.md | 2 +- cost.go | 14 ++++++++++++++ cost_test.go | 32 ++++++++++++++++++++++++++++++++ docs/AGENT_INTEGRATION.md | 5 ++--- docs/DEPLOYMENT.md | 2 +- docs/PERFORMANCE.md | 17 +++++++---------- docs/QUICK_START.md | 21 ++++++++++----------- docs/SECURITY.md | 2 +- docs/TROUBLESHOOTING.md | 9 ++++----- docs/architecture.md | 4 ++-- examples/README.md | 11 +++++------ 11 files changed, 79 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index c7faa21d9..58424aa93 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ import "github.com/GrayCodeAI/tok" ### Compress a prompt ```go -out := tok.PromptCompress("Please implement authentication", tok.IntensityUltra) +out, _ := tok.PromptCompress("Please implement authentication", tok.IntensityUltra) // → "Implement auth." ``` diff --git a/cost.go b/cost.go index 987d668e2..3aa806d41 100644 --- a/cost.go +++ b/cost.go @@ -129,6 +129,20 @@ func EstimateCostSavings(stats Stats, model string) float64 { return float64(stats.TokensSaved) / 1000 * pricing.InputPricePer1K } +// EstimateCost estimates the USD cost of processing text with a model, +// counting the text as input tokens. It is a convenience wrapper around +// EstimateTokensForModel and GetModelPricing. +// +// Returns 0 if the model is unknown (no pricing registered). +func EstimateCost(text string, model string) float64 { + pricing, ok := GetModelPricing(model) + if !ok { + return 0 + } + tokens := EstimateTokensForModel(text, model) + return float64(tokens) / 1000 * pricing.InputPricePer1K +} + // FormatCostSavings returns a human-readable cost savings string. // Returns "$X.XX saved" if the model is known, or empty string if unknown. func FormatCostSavings(stats Stats, model string) string { diff --git a/cost_test.go b/cost_test.go index d55223e1b..a91b8b726 100644 --- a/cost_test.go +++ b/cost_test.go @@ -345,6 +345,38 @@ func TestListModels_NoDuplicates(t *testing.T) { } } +func TestEstimateCost(t *testing.T) { + // gpt-4o input pricing is $0.0025 per 1K tokens. + pricing, ok := GetModelPricing("gpt-4o") + if !ok { + t.Fatal("expected gpt-4o pricing to be registered") + } + + text := "Hello, world! This is a test of the EstimateCost function." + tokens := EstimateTokensForModel(text, "gpt-4o") + want := float64(tokens) / 1000 * pricing.InputPricePer1K + + got := EstimateCost(text, "gpt-4o") + if got != want { + t.Errorf("EstimateCost = %v, want %v", got, want) + } + if got == 0 { + t.Error("expected non-zero cost for gpt-4o") + } +} + +func TestEstimateCost_UnknownModel(t *testing.T) { + if got := EstimateCost("some text", "totally-fake-model-xyz"); got != 0 { + t.Errorf("EstimateCost for unknown model = %v, want 0", got) + } +} + +func TestEstimateCost_EmptyText(t *testing.T) { + if got := EstimateCost("", "gpt-4o"); got != 0 { + t.Errorf("EstimateCost for empty text = %v, want 0", got) + } +} + func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstring(s, substr)) } diff --git a/docs/AGENT_INTEGRATION.md b/docs/AGENT_INTEGRATION.md index 260a873f1..72b527b86 100644 --- a/docs/AGENT_INTEGRATION.md +++ b/docs/AGENT_INTEGRATION.md @@ -58,15 +58,14 @@ tok.Compress(text, tok.WithMode(tok.ModeAggressive)) ### Compress a prompt ```go -out := tok.PromptCompress(prompt, tok.IntensityFull) +out, _ := tok.PromptCompress(prompt, tok.IntensityFull) // IntensityLite | IntensityFull | IntensityUltra ``` ### Estimate tokens and cost ```go -n := tok.EstimateTokensForModel(text, "gpt-4o") -cost := tok.EstimateCost(n, "gpt-4o") +cost := tok.EstimateCost(text, "gpt-4o") ``` ### Redact secrets before sending context to a model diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index b4afffb49..fd58ce69d 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -38,7 +38,7 @@ import "github.com/GrayCodeAI/tok" ```go // Prompt compression (Lite / Full / Ultra intensities). -out := tok.PromptCompress("Please implement authentication", tok.IntensityUltra) +out, _ := tok.PromptCompress("Please implement authentication", tok.IntensityUltra) // Full output pipeline with options. out, _ := tok.Compress(verboseOutput, tok.Aggressive) diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 90b1805c9..f71ac3659 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -106,10 +106,10 @@ Tune the speed/compression trade-off through options passed to `tok.Compress`, o | Aggressive | `tok.Compress(text, tok.Aggressive)` | all | Strictest reduction | ```go -out, err := tok.Compress(input, tok.Aggressive) +out, _ := tok.Compress(input, tok.Aggressive) // Prompt-oriented helper with an intensity level: -out := tok.PromptCompress(input, tok.IntensityFull) +out, _ := tok.PromptCompress(input, tok.IntensityFull) ``` Additional compression options: @@ -137,7 +137,7 @@ rules, err := tok.LoadFilterRules("rules/custom.toml") if err != nil { log.Fatal(err) } -compressed, err := tok.Compress(text, tok.WithCustomFilters(rules)) +compressed, _ := tok.Compress(text, tok.WithCustomFilters(rules)) ``` Profiles (presets of filter behavior) load via `tok.LoadProfile`. @@ -147,11 +147,8 @@ Profiles (presets of filter behavior) load via `tok.LoadProfile`. ## Estimation, Cost & Tracking ```go -// Token estimate for a specific model (fast path for short strings). -n := tok.EstimateTokensForModel(text, "gpt-4o") - -// Estimated cost for a given token count / model. -cost := tok.EstimateCost(tokens, "gpt-4o") +// Estimated cost for a given model. +cost := tok.EstimateCost(text, "gpt-4o") // Track usage over a context's lifetime. tracker := tok.NewTracker(ctx) @@ -207,7 +204,7 @@ go test -bench=BenchmarkPipeline -benchmem Use a lighter intensity and let budget gates short-circuit work: ```go -out := tok.PromptCompress(text, tok.IntensityLite) +out, _ := tok.PromptCompress(text, tok.IntensityLite) ``` ### Optimize for Memory @@ -219,7 +216,7 @@ The pipeline uses pooled buffers and streams large inputs internally; prefer str Use the strongest settings when token budget matters most: ```go -out := tok.PromptCompress(text, tok.IntensityUltra) +out, _ := tok.PromptCompress(text, tok.IntensityUltra) // or out, _ := tok.Compress(text, tok.Aggressive) ``` diff --git a/docs/QUICK_START.md b/docs/QUICK_START.md index cd386839c..4ab2cb401 100644 --- a/docs/QUICK_START.md +++ b/docs/QUICK_START.md @@ -21,7 +21,7 @@ import "github.com/GrayCodeAI/tok" ## Compress in One Call ```go -out, err := tok.Compress(text) +out, _ := tok.Compress(text) ``` `Compress` uses an adaptive system that selects the right compression depth based @@ -42,31 +42,30 @@ If you want explicit control, pass options to `Compress`: ```go // Aggressive mode: deepest available compression path -out, err := tok.Compress(text, tok.Aggressive) +out, _ := tok.Compress(text, tok.Aggressive) // Code-aware compression: preserve structure for a given language -out, err := tok.Compress(src, tok.WithCodeAware("go")) +out, _ := tok.Compress(src, tok.WithCodeAware("go")) // Perplexity-guided pruning at a target retention ratio -out, err := tok.Compress(text, tok.WithPerplexityGuided(scorer, 0.5)) +out, _ := tok.Compress(text, tok.WithPerplexityGuided(scorer, 0.5)) // Custom TOML filter rules (see "Custom Filters" below) -out, err := tok.Compress(text, tok.WithCustomFilters(rules)) +out, _ := tok.Compress(text, tok.WithCustomFilters(rules)) ``` For prompt-style input there is a dedicated entry point with intensity levels: ```go -out, err := tok.PromptCompress(prompt, tok.IntensityLite) -out, err = tok.PromptCompress(prompt, tok.IntensityFull) -out, err = tok.PromptCompress(prompt, tok.IntensityUltra) +out, _ := tok.PromptCompress(prompt, tok.IntensityLite) +out, _ = tok.PromptCompress(prompt, tok.IntensityFull) +out, _ = tok.PromptCompress(prompt, tok.IntensityUltra) ``` ## Token & Cost Estimation ```go -tokens, err := tok.EstimateTokensForModel(text, "gpt-4o") -cost, err := tok.EstimateCost(text, "gpt-4o") +cost := tok.EstimateCost(text, "gpt-4o") ``` ## Redaction & Extraction Helpers @@ -98,7 +97,7 @@ rules, err := tok.LoadFilterRules("filters.toml") if err != nil { // handle error } -out, err := tok.Compress(text, tok.WithCustomFilters(rules)) +out, _ := tok.Compress(text, tok.WithCustomFilters(rules)) ``` You can also load a named profile: diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 61042061e..9a2c1cf7c 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -47,7 +47,7 @@ rules, err := tok.LoadFilterRules("filters.toml") if err != nil { log.Fatal(err) } -out, err := tok.Compress(text, tok.WithCustomFilters(rules)) +out, _ := tok.Compress(text, tok.WithCustomFilters(rules)) ``` Treat filter rule files as part of your trusted configuration. Set restrictive diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 41e455062..4d0a084db 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -73,7 +73,7 @@ import "github.com/GrayCodeAI/tok" ```go // Prompt compression at higher intensity -out := tok.PromptCompress(text, tok.IntensityFull) // or tok.IntensityUltra +out, _ := tok.PromptCompress(text, tok.IntensityFull) // or tok.IntensityUltra // Generic compression with aggressive mode out, _ := tok.Compress(text, tok.Aggressive) @@ -89,7 +89,7 @@ Available levels: `tok.IntensityLite`, `tok.IntensityFull`, `tok.IntensityUltra` 1. Drop to a lighter intensity: ```go - out := tok.PromptCompress(text, tok.IntensityLite) + out, _ := tok.PromptCompress(text, tok.IntensityLite) ``` 2. For source code, use the code-aware option so language structure is preserved: @@ -108,8 +108,7 @@ Use truncation and estimation helpers rather than guessing: ```go truncated := tok.SmartTruncate(text, maxTokens) -n := tok.EstimateTokensForModel(truncated, "gpt-4o") -cost := tok.EstimateCost(n, "gpt-4o") +cost := tok.EstimateCost(truncated, "gpt-4o") ``` ### Sensitive data appears in compressed output @@ -146,7 +145,7 @@ inputs that don't need it. 2. Use a lighter intensity for hot paths: ```go - out := tok.PromptCompress(text, tok.IntensityLite) + out, _ := tok.PromptCompress(text, tok.IntensityLite) ``` 3. For perplexity-guided compression, tune the keep ratio (lower = more diff --git a/docs/architecture.md b/docs/architecture.md index 622171c02..ae80f2b07 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -46,7 +46,7 @@ tok/ ```go // 🗜️ One-shot compression -compressed, stats, err := tok.Compress(text, +compressed, stats := tok.Compress(text, tok.WithTier(tok.TierCode), tok.WithBudget(4000), tok.WithQuery("implement OAuth flow"), @@ -54,7 +54,7 @@ compressed, stats, err := tok.Compress(text, // 🔄 Reusable compressor (caches tokenizer state) c := tok.NewCompressor(tok.Aggressive) -compressed, stats, err := c.Compress(text) +compressed, stats := c.Compress(text) // 📊 Token estimation approx := tok.EstimateTokens(text) // fast, ±5% diff --git a/examples/README.md b/examples/README.md index 8f6121c70..fda84294d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -36,8 +36,7 @@ out, stats := tok.PromptCompress(in, tok.IntensityFull) ### Estimate tokens and cost ```go -n := tok.EstimateTokensForModel(out, "claude-3-5-sonnet") -cost := tok.EstimateCost(n, "claude-3-5-sonnet") +cost := tok.EstimateCost(out, "claude-3-5-sonnet") ``` ## Advanced Examples @@ -53,24 +52,24 @@ if err != nil { log.Fatal(err) } -out, err := tok.Compress(in, tok.WithCustomFilters(rules)) +out, _ := tok.Compress(in, tok.WithCustomFilters(rules)) ``` ### Code-aware and perplexity-guided compression ```go // Preserve code structure for a given language. -out, err := tok.Compress(src, tok.WithCodeAware("go")) +out, _ := tok.Compress(src, tok.WithCodeAware("go")) // Drop the lowest-information tokens using your own scorer. -out, err = tok.Compress(in, tok.WithPerplexityGuided(scorer, 0.5)) +out, _ = tok.Compress(in, tok.WithPerplexityGuided(scorer, 0.5)) ``` ### Profiles ```go profile, err := tok.LoadProfile("profile.toml") -out, err := tok.Compress(in, profile.Options()...) +out, _ := tok.Compress(in, profile.Options()...) ``` ### Secret detection and sensitive files From 9863a9db4f60dc1dd68af4a1c239a4be134a8471 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Fri, 24 Jul 2026 10:54:06 +0530 Subject: [PATCH 3/8] fix(tok): sort ListModels output and assert ordering in test ListModels documented a sorted list but iterated the pricing map directly, yielding random order per run. Add sort.Strings and a sort.StringsAreSorted assertion so the contract is enforced. --- cost.go | 2 ++ cost_test.go | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/cost.go b/cost.go index 3aa806d41..8871d9b41 100644 --- a/cost.go +++ b/cost.go @@ -2,6 +2,7 @@ package tok import ( "fmt" + "sort" "strings" "sync" ) @@ -166,5 +167,6 @@ func ListModels() []string { for name := range modelPricingRegistry { models = append(models, name) } + sort.Strings(models) return models } diff --git a/cost_test.go b/cost_test.go index a91b8b726..b6f0713f8 100644 --- a/cost_test.go +++ b/cost_test.go @@ -1,6 +1,7 @@ package tok import ( + "sort" "testing" ) @@ -331,6 +332,11 @@ func TestListModels(t *testing.T) { t.Errorf("ListModels() missing expected model %q", m) } } + + // ListModels must return models in sorted order (deterministic). + if !sort.StringsAreSorted(models) { + t.Errorf("ListModels() not sorted: %v", models) + } } func TestListModels_NoDuplicates(t *testing.T) { From 8ff2a350c813167d7aebdb3c93f7080d85c0e2de Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Sat, 25 Jul 2026 12:31:15 +0530 Subject: [PATCH 4/8] feat: add runtime graph module - Add runtimegraph/ package with runtime analysis - Update ratelimit.go, ratelimit_test.go, go.mod - Update README --- README.md | 33 +++++- go.mod | 3 + ratelimit.go | 40 ++++++- ratelimit_test.go | 41 +++++++ runtimegraph/projection.go | 191 ++++++++++++++++++++++++++++++++ runtimegraph/projection_test.go | 57 ++++++++++ 6 files changed, 360 insertions(+), 5 deletions(-) create mode 100644 runtimegraph/projection.go create mode 100644 runtimegraph/projection_test.go diff --git a/README.md b/README.md index 58424aa93..0f44d993e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ tok is a **library**, not a CLI. It exposes token-efficiency primitives as a cle - **Output filtering** — a 31-layer pipeline (entropy pruning, perplexity filtering, AST-aware compression, H2O heavy-hitter, …) that strips noise from command output before it re-enters an LLM context. - **Token estimation** — `tok.EstimateTokens*` and `tok.EstimateCost` give model-aware token counts and pricing. - **Secrets scanning** — `tok.SecretDetector` and `tok.IsSensitiveFilename` catch credentials before they leak into prompts. -- **Rate limiting & tracking** — persistent SQLite-backed gain tracking (`tok.NewTracker`). +- **Usage limits & tracking** — thread-safe token/cost windows (`tok.NewUsageTracker`) plus persistent SQLite-backed gain tracking (`tok.NewTracker`). +- **Runtime graph projection** — privacy-safe compression, usage, budget, and redaction summaries (`runtimegraph.Build`). It is consumed directly as a Go module, and it powers the `tok` commands inside [Hawk](https://github.com/GrayCodeAI/hawk) (`hawk tok ...`), which imports it as a library. @@ -78,6 +79,28 @@ findings := d.DetectSecrets(text) redacted := d.RedactSecrets(text) ``` +### Track usage and enforce a budget + +```go +usage := tok.NewUsageTracker() +usage.SetLimits(tok.UsageLimits{ + HourlyTokens: 100_000, + SessionTokens: 500_000, + CostUSD: 10, +}) + +if allowed, _ := usage.CanProceed(); allowed { + usage.Record(promptTokens+completionTokens, requestCostUSD, provider, model) +} + +allowed, reason := usage.CanProceed() +snapshot := usage.GetUsage() +``` + +`SetLimits` and `GetLimits` are safe to use while requests are being recorded. +Callers own request deduplication and decide whether a failed decision blocks +the current request or the next one. + --- ## Library API Highlights @@ -88,6 +111,7 @@ redacted := d.RedactSecrets(text) - **`tok.SmartTruncate(content, maxLines, lang)`** — code truncation that preserves function signatures and **always reports the exact drop count** (`kept + dropped == total`). - **`tok.ExtractJSON / ExtractJSONArray / ExtractAllJSON`** — brace-balanced JSON extraction from LLM output with surrounding prose, markdown fences, and unterminated objects. - **`tok.NewTracker(ctx)`** — persistent gain tracker (SQLite + WAL, 90-day retention, pure-Go via `modernc.org/sqlite`). `Aggregate`, `Recent`, `Prune` queries. +- **`tok.NewUsageTracker()`** — in-memory hourly, daily, session, and cost accounting with atomic limit snapshots and threshold decisions. - **`tok.EstimateTokensFast / WithEncoding / ForModel`** — model-aware token estimation. - **`filter.CompressWithRetry`** — validate-fix-retry loop: caller supplies a `Validator` and `AdjustFunc`; the loop escalates mode/intensity and retries up to N times. - **`filter.NewTOMLFilter` / `LoadTOMLFilterFile`** — full 8-stage TOML pipeline as a pluggable `Filter`. @@ -168,6 +192,7 @@ Profile the hot paths with `./scripts/profile.sh [compress|tokens|filter|secrets ``` tok ├── tok.go, *.go Public library API (top-level package) +├── runtimegraph/ Privacy-safe operations/policy/quality projection ├── internal/ │ ├── compress/ Input compression engine (6 modes) │ ├── filter/ Output pipeline (31 layers) @@ -181,6 +206,12 @@ tok Pure-Go, zero CGO, no runtime dependencies. +`runtimegraph.Build` projects completed compression statistics, usage +snapshots, budget decisions, and redaction summaries into `tok.graph/v1`. +Input text, model names, budget reasons, secret types, and secret values are +digest-only. Tok remains the source of truth for compression, limits, and +redaction; graph consumers receive immutable summaries. + --- ## Contributing diff --git a/go.mod b/go.mod index 97071ae8a..5d3066f95 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,15 @@ go 1.26.5 require ( github.com/BurntSushi/toml v1.6.0 + github.com/GrayCodeAI/hawk-core-contracts v0.1.8 github.com/spf13/viper v1.21.0 github.com/tiktoken-go/tokenizer v0.8.0 golang.org/x/sys v0.45.0 modernc.org/sqlite v1.51.0 ) +replace github.com/GrayCodeAI/hawk-core-contracts => ../hawk-core-contracts + require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2/v2 v2.1.0 // indirect diff --git a/ratelimit.go b/ratelimit.go index db9c3e0ce..6b37141a4 100644 --- a/ratelimit.go +++ b/ratelimit.go @@ -39,6 +39,14 @@ type UsageSummary struct { DailyPct float64 } +// UsageLimits is a concurrency-safe snapshot of UsageTracker limits. +type UsageLimits struct { + DailyTokens int + HourlyTokens int + SessionTokens int + CostUSD float64 +} + // UsageTracker tracks API usage across sessions and prevents surprise bills. type UsageTracker struct { DailyLimit int @@ -70,6 +78,30 @@ func NewUsageTracker() *UsageTracker { } } +// SetLimits atomically updates all token and cost ceilings. Zero disables the +// corresponding limit. Negative values are clamped to zero. +func (u *UsageTracker) SetLimits(limits UsageLimits) { + u.mu.Lock() + defer u.mu.Unlock() + u.DailyLimit = max(limits.DailyTokens, 0) + u.HourlyLimit = max(limits.HourlyTokens, 0) + u.SessionLimit = max(limits.SessionTokens, 0) + u.CostLimitUSD = math.Max(limits.CostUSD, 0) + u.checkThresholdsLocked() +} + +// GetLimits returns an atomic snapshot of the current ceilings. +func (u *UsageTracker) GetLimits() UsageLimits { + u.mu.Lock() + defer u.mu.Unlock() + return UsageLimits{ + DailyTokens: u.DailyLimit, + HourlyTokens: u.HourlyLimit, + SessionTokens: u.SessionLimit, + CostUSD: u.CostLimitUSD, + } +} + // Record adds a usage entry and checks thresholds. func (u *UsageTracker) Record(tokens int, costUSD float64, provider, model string) { u.mu.Lock() @@ -99,21 +131,21 @@ func (u *UsageTracker) CanProceed() (bool, string) { u.pruneOldLocked() hourlyTokens := u.hourlyTokensLocked() - if hourlyTokens >= u.HourlyLimit { + if u.HourlyLimit > 0 && hourlyTokens >= u.HourlyLimit { return false, fmt.Sprintf("hourly token limit reached (%d/%d)", hourlyTokens, u.HourlyLimit) } dailyTokens := u.dailyTokensLocked() - if dailyTokens >= u.DailyLimit { + if u.DailyLimit > 0 && dailyTokens >= u.DailyLimit { return false, fmt.Sprintf("daily token limit reached (%d/%d)", dailyTokens, u.DailyLimit) } - if u.sessionUsage >= u.SessionLimit { + if u.SessionLimit > 0 && u.sessionUsage >= u.SessionLimit { return false, fmt.Sprintf("session token limit reached (%d/%d)", u.sessionUsage, u.SessionLimit) } dailyCost := u.dailyCostLocked() - if dailyCost >= u.CostLimitUSD { + if u.CostLimitUSD > 0 && dailyCost >= u.CostLimitUSD { return false, fmt.Sprintf("daily cost limit reached ($%.2f/$%.2f)", dailyCost, u.CostLimitUSD) } diff --git a/ratelimit_test.go b/ratelimit_test.go index 09783541e..2797e7c2c 100644 --- a/ratelimit_test.go +++ b/ratelimit_test.go @@ -26,6 +26,47 @@ func TestRecordIncreasesCounters(t *testing.T) { } } +func TestUsageLimitsAtomicConfiguration(t *testing.T) { + tracker := NewUsageTracker() + tracker.SetLimits(UsageLimits{ + DailyTokens: 400, + HourlyTokens: 300, + SessionTokens: 200, + CostUSD: 1.5, + }) + + got := tracker.GetLimits() + if got != (UsageLimits{ + DailyTokens: 400, + HourlyTokens: 300, + SessionTokens: 200, + CostUSD: 1.5, + }) { + t.Fatalf("GetLimits() = %+v", got) + } + + tracker.SetLimits(UsageLimits{ + DailyTokens: -1, + HourlyTokens: -1, + SessionTokens: -1, + CostUSD: -1, + }) + if got := tracker.GetLimits(); got != (UsageLimits{}) { + t.Fatalf("negative limits were not clamped: %+v", got) + } +} + +func TestUsageLimitsZeroDisablesCeilings(t *testing.T) { + tracker := NewUsageTracker() + tracker.SetLimits(UsageLimits{}) + tracker.Record(2_000_000, 100, "provider", "model") + + allowed, reason := tracker.CanProceed() + if !allowed { + t.Fatalf("zero limits should disable every ceiling, got reason %q", reason) + } +} + func TestCanProceedReturnsFalseWhenHourlyLimitHit(t *testing.T) { tracker := NewUsageTracker() tracker.HourlyLimit = 1000 diff --git a/runtimegraph/projection.go b/runtimegraph/projection.go new file mode 100644 index 000000000..e4bb7ff3f --- /dev/null +++ b/runtimegraph/projection.go @@ -0,0 +1,191 @@ +// Package runtimegraph projects Tok compression, usage, budget, and redaction +// summaries into the portable hawk-eco graph contract. +package runtimegraph + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "sort" + "strconv" + "strings" + "time" + + graphcontracts "github.com/GrayCodeAI/hawk-core-contracts/graph" + "github.com/GrayCodeAI/tok" +) + +const SchemaVersion = "tok.graph/v1" + +type BudgetDecision struct { + Allowed bool + Reason string + HourlyLimit int + DailyLimit int + SessionLimit int + CostLimitUSD float64 +} + +type RedactionSummary struct { + MatchCount int + VerifiedCount int + Types map[string]int +} + +type Input struct { + Compression *tok.Stats + Usage *tok.UsageSummary + Budget *BudgetDecision + Redaction *RedactionSummary + Source string + ObservedAt time.Time + Scope graphcontracts.Scope + CorrelationID string + ProducerVersion string +} + +type Export struct { + SchemaVersion string `json:"schema_version"` + GeneratedAt time.Time `json:"generated_at"` + Scope graphcontracts.Scope `json:"scope,omitempty"` + Nodes []graphcontracts.Node `json:"nodes"` + Edges []graphcontracts.Edge `json:"edges"` + Events []graphcontracts.Event `json:"events"` +} + +func Build(input Input) (*Export, error) { + if input.Compression == nil && input.Usage == nil && input.Budget == nil && input.Redaction == nil { + return nil, errors.New("runtimegraph: at least one summary is required") + } + at := input.ObservedAt.UTC() + if at.IsZero() { + at = time.Now().UTC() + } + sourceDigest := digest(input.Source) + export := &Export{ + SchemaVersion: SchemaVersion, GeneratedAt: at, Scope: input.Scope, + Nodes: []graphcontracts.Node{}, Edges: []graphcontracts.Edge{}, Events: []graphcontracts.Event{}, + } + var usageRef graphcontracts.Ref + if input.Compression != nil { + attrs := map[string]string{ + "entity": "compression", "source_digest": sourceDigest, + "original_tokens": strconv.Itoa(input.Compression.OriginalTokens), + "final_tokens": strconv.Itoa(input.Compression.FinalTokens), + "tokens_saved": strconv.Itoa(input.Compression.TokensSaved), + "reduction_percent": formatFloat(input.Compression.ReductionPercent), + "cost_savings_usd": formatFloat(input.Compression.CostSavings), + "model_digest": digest(input.Compression.Model), + "layer_count": strconv.Itoa(len(input.Compression.Layers)), + } + if _, err := addNode(export, graphcontracts.NodeOperations, "compression", attrs, input, at, sourceDigest); err != nil { + return nil, err + } + } + if input.Usage != nil { + attrs := map[string]string{ + "entity": "usage", "hourly_tokens": strconv.Itoa(input.Usage.HourlyTokens), + "hourly_remaining": strconv.Itoa(input.Usage.HourlyRemaining), + "daily_tokens": strconv.Itoa(input.Usage.DailyTokens), + "daily_remaining": strconv.Itoa(input.Usage.DailyRemaining), + "session_tokens": strconv.Itoa(input.Usage.SessionTokens), + "session_remaining": strconv.Itoa(input.Usage.SessionRemaining), + "daily_cost_usd": formatFloat(input.Usage.DailyCostUSD), + "cost_remaining_usd": formatFloat(input.Usage.CostRemaining), + "hourly_percent": formatFloat(input.Usage.HourlyPct), + "daily_percent": formatFloat(input.Usage.DailyPct), + } + ref, err := addNode(export, graphcontracts.NodeOperations, "usage", attrs, input, at, sourceDigest) + if err != nil { + return nil, err + } + usageRef = ref + } + if input.Budget != nil { + attrs := map[string]string{ + "entity": "budget_decision", "allowed": strconv.FormatBool(input.Budget.Allowed), + "reason_digest": digest(input.Budget.Reason), + "hourly_limit": strconv.Itoa(input.Budget.HourlyLimit), + "daily_limit": strconv.Itoa(input.Budget.DailyLimit), + "session_limit": strconv.Itoa(input.Budget.SessionLimit), + "cost_limit_usd": formatFloat(input.Budget.CostLimitUSD), + } + ref, err := addNode(export, graphcontracts.NodePolicy, "budget", attrs, input, at, sourceDigest) + if err != nil { + return nil, err + } + if usageRef.ID != "" { + if err := addEdge(export, ref, usageRef, graphcontracts.EdgeDependsOn, input, at); err != nil { + return nil, err + } + } + } + if input.Redaction != nil { + typeNames := make([]string, 0, len(input.Redaction.Types)) + for name := range input.Redaction.Types { + typeNames = append(typeNames, name) + } + sort.Strings(typeNames) + attrs := map[string]string{ + "entity": "redaction", "source_digest": sourceDigest, + "match_count": strconv.Itoa(max(input.Redaction.MatchCount, 0)), + "verified_count": strconv.Itoa(max(input.Redaction.VerifiedCount, 0)), + "type_count": strconv.Itoa(len(typeNames)), + "types_digest": digest(strings.Join(typeNames, "\x00")), + } + if _, err := addNode(export, graphcontracts.NodeQuality, "redaction", attrs, input, at, sourceDigest); err != nil { + return nil, err + } + } + sort.Slice(export.Nodes, func(i, j int) bool { return export.Nodes[i].ID < export.Nodes[j].ID }) + sort.Slice(export.Edges, func(i, j int) bool { return export.Edges[i].ID < export.Edges[j].ID }) + sort.Slice(export.Events, func(i, j int) bool { return export.Events[i].ID < export.Events[j].ID }) + return export, nil +} + +func addNode(export *Export, kind graphcontracts.NodeKind, entity string, attrs map[string]string, input Input, at time.Time, sourceDigest string) (graphcontracts.Ref, error) { + id := "tok/" + entity + "/" + digest(sourceDigest, at.Format(time.RFC3339Nano)) + ref := graphcontracts.Ref{Kind: kind, ID: id} + provenance := graphcontracts.Provenance{ + Producer: "tok", Version: strings.TrimSpace(input.ProducerVersion), SourceID: sourceDigest, + Evidence: []graphcontracts.ArtifactRef{{URI: "tok://" + entity + "/" + digest(sourceDigest)}}, + } + node := graphcontracts.Node{ID: id, Kind: kind, Scope: input.Scope, CreatedAt: at, Provenance: provenance, Attributes: attrs} + if err := node.Validate(); err != nil { + return graphcontracts.Ref{}, fmt.Errorf("runtimegraph: %s node: %w", entity, err) + } + event := graphcontracts.Event{ + ID: "tok/observed/" + digest(id, at.Format(time.RFC3339Nano)), Type: graphcontracts.EventObserved, + Subject: ref, Scope: input.Scope, OccurredAt: at, CorrelationID: strings.TrimSpace(input.CorrelationID), + IdempotencyKey: digest(id, at.Format(time.RFC3339Nano)), Provenance: provenance, + } + export.Nodes = append(export.Nodes, node) + export.Events = append(export.Events, event) + return ref, nil +} + +func addEdge(export *Export, from, to graphcontracts.Ref, kind graphcontracts.EdgeKind, input Input, at time.Time) error { + edge := graphcontracts.Edge{ + ID: "tok/edge/" + digest(from.ID, to.ID, string(kind)), Kind: kind, From: from, To: to, + Scope: input.Scope, CreatedAt: at, + Provenance: graphcontracts.Provenance{Producer: "tok", Version: strings.TrimSpace(input.ProducerVersion), SourceID: digest(input.Source)}, + } + if err := edge.Validate(); err != nil { + return fmt.Errorf("runtimegraph: edge: %w", err) + } + export.Edges = append(export.Edges, edge) + return nil +} + +func formatFloat(value float64) string { return strconv.FormatFloat(value, 'f', -1, 64) } + +func digest(parts ...string) string { + hash := sha256.New() + for _, part := range parts { + _, _ = hash.Write([]byte(strconv.Itoa(len(part)))) + _, _ = hash.Write([]byte{':'}) + _, _ = hash.Write([]byte(part)) + } + return hex.EncodeToString(hash.Sum(nil)) +} diff --git a/runtimegraph/projection_test.go b/runtimegraph/projection_test.go new file mode 100644 index 000000000..a33da642b --- /dev/null +++ b/runtimegraph/projection_test.go @@ -0,0 +1,57 @@ +package runtimegraph_test + +import ( + "encoding/json" + "strings" + "testing" + "time" + + graphcontracts "github.com/GrayCodeAI/hawk-core-contracts/graph" + "github.com/GrayCodeAI/tok" + "github.com/GrayCodeAI/tok/runtimegraph" +) + +func TestBuildMixedPrivacySafeProjection(t *testing.T) { + t.Parallel() + at := time.Date(2026, 7, 25, 14, 0, 0, 0, time.UTC) + stats := tok.Stats{OriginalTokens: 100, FinalTokens: 40, TokensSaved: 60, Model: "private-model"} + usage := tok.UsageSummary{HourlyTokens: 10, SessionTokens: 20, DailyCostUSD: .5} + export, err := runtimegraph.Build(runtimegraph.Input{ + Compression: &stats, Usage: &usage, + Budget: &runtimegraph.BudgetDecision{Allowed: false, Reason: "private budget reason", HourlyLimit: 10}, + Redaction: &runtimegraph.RedactionSummary{MatchCount: 2, Types: map[string]int{"OpenAI API Key": 2}}, + Source: "private source with sk-secret", ObservedAt: at, + Scope: graphcontracts.Scope{RepositoryID: "repo"}, CorrelationID: "session-1", + }) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + if len(export.Nodes) != 4 || len(export.Edges) != 1 || len(export.Events) != 4 { + t.Fatalf("unexpected sizes: nodes=%d edges=%d events=%d", len(export.Nodes), len(export.Edges), len(export.Events)) + } + payload, _ := json.Marshal(export) + for _, secret := range []string{"private source with sk-secret", "private budget reason", "private-model", "OpenAI API Key"} { + if strings.Contains(string(payload), secret) { + t.Fatalf("projection leaked %q", secret) + } + } + kinds := map[graphcontracts.NodeKind]bool{} + for _, node := range export.Nodes { + kinds[node.Kind] = true + if err := node.Validate(); err != nil { + t.Fatalf("invalid node: %v", err) + } + } + for _, want := range []graphcontracts.NodeKind{graphcontracts.NodeOperations, graphcontracts.NodePolicy, graphcontracts.NodeQuality} { + if !kinds[want] { + t.Fatalf("missing node kind %q", want) + } + } +} + +func TestBuildRequiresSummary(t *testing.T) { + t.Parallel() + if _, err := runtimegraph.Build(runtimegraph.Input{}); err == nil { + t.Fatal("expected empty input error") + } +} From 875d5af1d21689a3ee5c7020887e67d355e8f405 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Sat, 25 Jul 2026 13:06:40 +0530 Subject: [PATCH 5/8] feat: add runtime performance graph - Add RuntimeGraph implementation - Track runtime performance metrics across components - Support issue ranking and portable GraphSpec conversion --- runtimegraph/runtime_graph.go | 160 ++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 runtimegraph/runtime_graph.go diff --git a/runtimegraph/runtime_graph.go b/runtimegraph/runtime_graph.go new file mode 100644 index 000000000..4be715a7d --- /dev/null +++ b/runtimegraph/runtime_graph.go @@ -0,0 +1,160 @@ +// Package runtimegraph provides runtime performance graph analysis for tok. +package runtimegraph + +import ( + "fmt" + "sort" + "sync" + "time" + + graphcontracts "github.com/GrayCodeAI/hawk-core-contracts/graph" +) + +// RuntimeMetric represents a runtime performance metric. +type RuntimeMetric struct { + Name string `json:"name"` + Value float64 `json:"value"` + Threshold float64 `json:"threshold"` + Status string `json:"status"` // "pass", "warn", "fail" + UpdatedAt time.Time `json:"updated_at"` +} + +// RuntimeNode represents a runtime component with performance metrics. +type RuntimeNode struct { + ID string `json:"id"` + Type string `json:"type"` // "model", "endpoint", "pipeline", "service" + Name string `json:"name"` + Metrics []RuntimeMetric `json:"metrics"` + Children []string `json:"children,omitempty"` + Attrs map[string]interface{} `json:"attrs,omitempty"` +} + +// RuntimeEdge represents a runtime relationship between components. +type RuntimeEdge struct { + From string `json:"from"` + To string `json:"to"` + Kind string `json:"kind"` // "calls", "produces", "consumes", "depends_on" + Weight float64 `json:"weight"` +} + +// RuntimeGraph represents a runtime performance graph. +type RuntimeGraph struct { + mu sync.RWMutex + nodes map[string]*RuntimeNode + edges []RuntimeEdge +} + +// NewRuntimeGraph creates a new runtime graph. +func NewRuntimeGraph() *RuntimeGraph { + return &RuntimeGraph{ + nodes: make(map[string]*RuntimeNode), + } +} + +// AddNode adds a runtime node to the graph. +func (g *RuntimeGraph) AddNode(node *RuntimeNode) { + g.mu.Lock() + defer g.mu.Unlock() + g.nodes[node.ID] = node +} + +// AddEdge adds a runtime edge to the graph. +func (g *RuntimeGraph) AddEdge(edge RuntimeEdge) { + g.mu.Lock() + defer g.mu.Unlock() + g.edges = append(g.edges, edge) +} + +// GetNode retrieves a node by ID. +func (g *RuntimeGraph) GetNode(id string) (*RuntimeNode, bool) { + g.mu.RLock() + defer g.mu.RUnlock() + node, ok := g.nodes[id] + return node, ok +} + +// GetNodes returns all nodes. +func (g *RuntimeGraph) GetNodes() []*RuntimeNode { + g.mu.RLock() + defer g.mu.RUnlock() + result := make([]*RuntimeNode, 0, len(g.nodes)) + for _, node := range g.nodes { + result = append(result, node) + } + return result +} + +// GetEdges returns all edges. +func (g *RuntimeGraph) GetEdges() []RuntimeEdge { + g.mu.RLock() + defer g.mu.RUnlock() + return g.edges +} + +// GetFailedMetrics returns all metrics that failed their threshold. +func (g *RuntimeGraph) GetFailedMetrics() []RuntimeMetric { + g.mu.RLock() + defer g.mu.RUnlock() + result := []RuntimeMetric{} + for _, node := range g.nodes { + for _, metric := range node.Metrics { + if metric.Status == "fail" { + result = append(result, metric) + } + } + } + return result +} + +// GetTopIssues returns the top N issues by severity. +func (g *RuntimeGraph) GetTopIssues(n int) []RuntimeMetric { + failed := g.GetFailedMetrics() + sort.Slice(failed, func(i, j int) bool { + return failed[i].Value > failed[j].Value + }) + if len(failed) > n { + failed = failed[:n] + } + return failed +} + +// ToGraphSpec converts the runtime graph to a portable graph spec. +func (g *RuntimeGraph) ToGraphSpec() *graphcontracts.GraphSpec { + g.mu.RLock() + defer g.mu.RUnlock() + + nodes := make([]graphcontracts.NodeSpec, 0, len(g.nodes)) + for id, node := range g.nodes { + config := map[string]string{ + "name": node.Name, + "type": node.Type, + "metrics": fmt.Sprintf("%d", len(node.Metrics)), + } + for _, m := range node.Metrics { + config["metric_"+m.Name] = fmt.Sprintf("%.2f", m.Value) + } + + nodes = append(nodes, graphcontracts.NodeSpec{ + ID: id, + Type: graphcontracts.NodeTypeExecution, + Name: node.Name, + Config: config, + }) + } + + edges := make([]graphcontracts.EdgeSpec, 0, len(g.edges)) + for _, edge := range g.edges { + edges = append(edges, graphcontracts.EdgeSpec{ + From: edge.From, + To: edge.To, + Weight: edge.Weight, + }) + } + + return &graphcontracts.GraphSpec{ + ID: "runtime-graph", + Name: "Runtime Performance Graph", + Nodes: nodes, + Edges: edges, + } +} From 44ae0057bc0bf3ca9136c13ec757cf95829c5286 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Sat, 25 Jul 2026 20:17:57 +0530 Subject: [PATCH 6/8] fix: resolve NodeTypeExecution reference and formatting --- runtimegraph/runtime_graph.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/runtimegraph/runtime_graph.go b/runtimegraph/runtime_graph.go index 4be715a7d..196de1782 100644 --- a/runtimegraph/runtime_graph.go +++ b/runtimegraph/runtime_graph.go @@ -33,7 +33,7 @@ type RuntimeNode struct { type RuntimeEdge struct { From string `json:"from"` To string `json:"to"` - Kind string `json:"kind"` // "calls", "produces", "consumes", "depends_on" + Kind string `json:"kind"` // "calls", "produces", "consumes", "depends_on" Weight float64 `json:"weight"` } @@ -126,9 +126,9 @@ func (g *RuntimeGraph) ToGraphSpec() *graphcontracts.GraphSpec { nodes := make([]graphcontracts.NodeSpec, 0, len(g.nodes)) for id, node := range g.nodes { config := map[string]string{ - "name": node.Name, - "type": node.Type, - "metrics": fmt.Sprintf("%d", len(node.Metrics)), + "name": node.Name, + "type": node.Type, + "metrics": fmt.Sprintf("%d", len(node.Metrics)), } for _, m := range node.Metrics { config["metric_"+m.Name] = fmt.Sprintf("%.2f", m.Value) @@ -152,9 +152,9 @@ func (g *RuntimeGraph) ToGraphSpec() *graphcontracts.GraphSpec { } return &graphcontracts.GraphSpec{ - ID: "runtime-graph", - Name: "Runtime Performance Graph", - Nodes: nodes, - Edges: edges, + ID: "runtime-graph", + Name: "Runtime Performance Graph", + Nodes: nodes, + Edges: edges, } } From 69f999754221d98790eacbe225893b5610a21d40 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Sat, 25 Jul 2026 21:17:48 +0530 Subject: [PATCH 7/8] chore: bump hawk-core-contracts to v0.1.9 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5d3066f95..9cfc70e31 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.5 require ( github.com/BurntSushi/toml v1.6.0 - github.com/GrayCodeAI/hawk-core-contracts v0.1.8 + github.com/GrayCodeAI/hawk-core-contracts v0.1.9 github.com/spf13/viper v1.21.0 github.com/tiktoken-go/tokenizer v0.8.0 golang.org/x/sys v0.45.0 From 3ea982a377fa43eb03c5155e8c2d5016f19c1dd9 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Sat, 25 Jul 2026 21:24:23 +0530 Subject: [PATCH 8/8] chore: remove local replace directive, use published v0.1.9 --- go.mod | 2 -- go.sum | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 9cfc70e31..613a34ac4 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,6 @@ require ( modernc.org/sqlite v1.51.0 ) -replace github.com/GrayCodeAI/hawk-core-contracts => ../hawk-core-contracts - require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index 0ad84aa52..3ef0d5be4 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/GrayCodeAI/hawk-core-contracts v0.1.9 h1:uXX/gtNM+3kxSEzu+rZkHykzcEaAbASn1lmPyOGMXvc= +github.com/GrayCodeAI/hawk-core-contracts v0.1.9/go.mod h1:BXbh68YrCf+s9HVqND5F8DAvl2MnE5NcOwZZZB56HGA= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2/v2 v2.1.0 h1:jHXRmHRZGbuQzDZjMlCAXOvQb75iv3HyLDzXGj5H1AY=