Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bin/opik-logger-darwin-amd64
Binary file not shown.
Binary file modified bin/opik-logger-darwin-arm64
Binary file not shown.
Binary file modified bin/opik-logger-linux-amd64
Binary file not shown.
Binary file modified bin/opik-logger-windows-amd64.exe
Binary file not shown.
51 changes: 44 additions & 7 deletions src/attribution.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,28 @@ func ExtractAttribution(entries []TranscriptEntry) *Attribution {
func BuildSkillsSnapshot(allEntries []TranscriptEntry) map[string]interface{} {
listing := extractSkillEvents(allEntries)
loaded := extractLoadedSkills(allEntries)
menuTokens := extractSkillMenuTokens(allEntries)
if len(listing) == 0 && len(loaded) == 0 && menuTokens == 0 {
attachmentTokens := extractSkillMenuTokens(allEntries)
if len(listing) == 0 && len(loaded) == 0 && attachmentTokens == 0 {
return nil
}

// menu_tokens is the always-on cost of the skill menu: summed per skill
// from each skill's name + frontmatter description (what /context's
// /skills view attributes per row). Bundled (in-binary) skills can't be
// read from disk, so they contribute 0 — bundled_count surfaces how much
// of /context's number we structurally cannot reach.
available := make([]map[string]interface{}, 0, len(listing))
menuTokens := 0
bundledCount := 0
for _, s := range listing {
menuTokens += s.MenuTokens
if s.Source == "bundled" {
bundledCount++
}
e := map[string]interface{}{
"name": s.Name,
"source": s.Source,
"name": s.Name,
"source": s.Source,
"menu_tokens": s.MenuTokens,
}
if s.SHA256 != "" {
e["sha256"] = s.SHA256
Expand All @@ -56,6 +68,10 @@ func BuildSkillsSnapshot(allEntries []TranscriptEntry) map[string]interface{} {
if s.Path != "" {
e["path"] = s.Path
}
if s.CatalogBodyTokens > 0 {
e["catalog_body_tokens"] = s.CatalogBodyTokens
e["catalog_source"] = s.CatalogSource
}
loadedOut = append(loadedOut, e)
}

Expand All @@ -66,6 +82,12 @@ func BuildSkillsSnapshot(allEntries []TranscriptEntry) map[string]interface{} {
"loaded_tokens": loadedTokens,
"available_count": len(available),
"loaded_count": len(loadedOut),
"bundled_count": bundledCount,
// The token estimate of the raw skill_listing attachment in the
// transcript, kept for cross-checking. It differs from menu_tokens:
// the attachment is a partial artifact, whereas menu_tokens is
// reconstructed per skill from on-disk frontmatter.
"menu_tokens_attachment": attachmentTokens,
},
"available": available,
"loaded": loadedOut,
Expand All @@ -91,9 +113,20 @@ func tokEstimate(s string) int {
// "json" → 2.8 (tool_use input, MCP payload)
// "deferred_tools_payload" → 2.5 (pure JSON list of names)
// "tool_result" → 3.0 (mixed text + JSON-ish output)
// "skill_listing_menu" → 3.0 (per-skill `- name: description` block —
// derived from /context's published
// per-skill tokens against the actual
// attachment text: 6368 chars / 2116 tokens)
// "agent_frontmatter" → 3.1 (YAML name/desc + example blocks; verified
// vs /context's "Custom agents" rows)
// "skill_body" → 3.5 (markdown w/ code blocks)
// "memory_file" → 2.4 (.claude/rules/**/*.md + auto-mem MEMORY.md;
// derived from /context's "Memory files"
// rows — denser than skill bodies because
// these files lean on brackets/dashes/code
// conventions that tokenize as separate
// short tokens)
// "assistant_text" → 3.9 (prose with occasional code)
// "skill_listing_menu" → 3.9 (name + short description lines)
// "prose" → 3.9
// "user_prompt" → 4.3 (natural English from a user)
// "" / unknown → 3.6 (overall calibrated median)
Expand Down Expand Up @@ -130,11 +163,15 @@ func charsPerToken(s, contentType string) float64 {
return 2.8
case "deferred_tools_payload":
return 2.5
case "tool_result":
case "memory_file":
return 2.4
case "tool_result", "skill_listing_menu":
return 3.0
case "agent_frontmatter":
return 3.1
case "skill_body":
return 3.5
case "assistant_text", "prose", "skill_listing_menu":
case "assistant_text", "prose":
return 3.9
case "user_prompt":
return 4.3
Expand Down
152 changes: 152 additions & 0 deletions src/cc_builtin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package main

import (
"strconv"
"strings"
)

// ccBuiltinConstants holds the always-on costs Claude Code charges every
// turn that we structurally cannot read from the transcript:
//
// - SystemPromptTokens the bundled default system prompt
// - SystemToolsTokens full JSON schemas for the default tools
// (Read, Edit, Bash, …) — not the names alone
// - SystemToolsDeferredTokens the catalog of deferred tool definitions
// (Cron*, Task*, Web*, Monitor, …) plus any
// schemas Claude Code injects on demand
//
// These are taken from `/context` for a known CC version. They drift with
// each binary release, so the table below should grow over time.
type ccBuiltinConstants struct {
SystemPromptTokens int
SystemToolsTokens int
SystemToolsDeferredTokens int
}

// ccBuiltinByVersion is a small versioned table — keys are exact CC
// CLI versions (the `version` field stamped on every transcript entry).
// When the binary changes its bundled prompt or tool catalog, /context's
// numbers shift; add an entry here with the new ones.
//
// Source: `claude -p "/context"` in a project with NO MCPs connected, on
// the listed CC version + Opus model. The two figures together cover
// >65% of /context's accounted-for tokens.
var ccBuiltinByVersion = map[string]ccBuiltinConstants{
"2.1.150": {
SystemPromptTokens: 8000,
SystemToolsTokens: 17600,
SystemToolsDeferredTokens: 19200,
},
}

// ccBuiltinFor returns the constants for the given CC version. Falls
// through to the highest known version (sorted lexically — fine while
// the table is small) when an exact match isn't found; returns the zero
// value when the table is empty. Callers should check `version != ""` on
// the returned snapshot to distinguish "estimated" from "unknown".
func ccBuiltinFor(version string) (ccBuiltinConstants, string) {
if c, ok := ccBuiltinByVersion[version]; ok {
return c, version
}
// Patch-version fallback: 2.1.151 → use the closest known 2.1.* row.
// Major.Minor match is a reasonable proxy because the system prompt
// and tool catalog rarely change inside a minor.
if best, key := closestKnownVersion(version); key != "" {
return best, key
}
return ccBuiltinConstants{}, ""
}

// closestKnownVersion returns the highest known version that shares the
// same major.minor as want. Returns ("", "") if no such version exists.
func closestKnownVersion(want string) (ccBuiltinConstants, string) {
if want == "" {
return ccBuiltinConstants{}, ""
}
wantMaj, wantMin, _ := splitSemver(want)
if wantMaj < 0 {
return ccBuiltinConstants{}, ""
}
bestKey := ""
var bestVal ccBuiltinConstants
bestPatch := -1
for k, v := range ccBuiltinByVersion {
maj, min, patch := splitSemver(k)
if maj != wantMaj || min != wantMin {
continue
}
if patch > bestPatch {
bestPatch = patch
bestKey = k
bestVal = v
}
}
return bestVal, bestKey
}

func splitSemver(v string) (maj, min, patch int) {
maj, min, patch = -1, -1, -1
parts := strings.SplitN(v, ".", 3)
if len(parts) < 2 {
return
}
var err error
if maj, err = strconv.Atoi(parts[0]); err != nil {
maj = -1
return
}
if min, err = strconv.Atoi(parts[1]); err != nil {
min = -1
return
}
if len(parts) == 3 {
patch, _ = strconv.Atoi(parts[2])
}
return
}

// findCCVersion returns the first non-empty `version` field stamped on
// any transcript entry. Claude Code puts the CLI version on every user
// and assistant entry, so any pass-through tells us the binary that wrote
// the session.
func findCCVersion(entries []TranscriptEntry) string {
for _, e := range entries {
if e.Version != "" {
return e.Version
}
}
return ""
}

// extractCCBuiltinSnapshot returns the `cc.cc_builtin` block —
// approximated, version-keyed costs for the bundled system prompt and
// tool catalog. Marked `estimated: true` so dashboards can distinguish
// these from transcript-derived numbers. Returns nil when the version
// is unknown (better to under-report than ship made-up numbers).
//
// total_tokens splits the same way /context does: always_on is what
// ships in the request envelope every turn (and bills against
// input_tokens / cache_*); deferred_tools is what WOULD cost if loaded
// via ToolSearch, but normally doesn't ship at all. Summing total +
// deferred would over-count by the deferred bucket — same trap we
// addressed in buildContextSnapshot.
func extractCCBuiltinSnapshot(entries []TranscriptEntry) map[string]interface{} {
version := findCCVersion(entries)
consts, matched := ccBuiltinFor(version)
if matched == "" {
return nil
}
alwaysOn := consts.SystemPromptTokens + consts.SystemToolsTokens
return map[string]interface{}{
"summary": map[string]interface{}{
"total_tokens": alwaysOn, // ← matches /context and API billing
"deferred_tokens": consts.SystemToolsDeferredTokens,
"estimated": true,
"cc_version": version,
"matched_table": matched,
"system_prompt": consts.SystemPromptTokens,
"system_tools": consts.SystemToolsTokens,
"deferred_tools": consts.SystemToolsDeferredTokens,
},
}
}
73 changes: 73 additions & 0 deletions src/cc_builtin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package main

import "testing"

func TestFindCCVersion(t *testing.T) {
entries := []TranscriptEntry{
{Type: "queue-operation"},
{Type: "user", Version: "2.1.150"},
{Type: "assistant", Version: "2.1.150"},
}
if got := findCCVersion(entries); got != "2.1.150" {
t.Errorf("findCCVersion = %q, want 2.1.150", got)
}
if got := findCCVersion(nil); got != "" {
t.Errorf("findCCVersion(nil) = %q, want empty", got)
}
}

func TestCCBuiltinFor(t *testing.T) {
// Exact match.
c, key := ccBuiltinFor("2.1.150")
if key != "2.1.150" || c.SystemPromptTokens == 0 {
t.Errorf("exact: got key=%q tokens=%+v", key, c)
}
// Patch fallback inside the same major.minor.
c, key = ccBuiltinFor("2.1.151")
if key == "" || c.SystemPromptTokens == 0 {
t.Errorf("patch fallback: got key=%q tokens=%+v", key, c)
}
// Unknown major.minor → no fallback.
c, key = ccBuiltinFor("3.0.0")
if key != "" || c.SystemPromptTokens != 0 {
t.Errorf("unknown major: expected zero, got key=%q tokens=%+v", key, c)
}
// Empty version.
c, key = ccBuiltinFor("")
if key != "" {
t.Errorf("empty version: expected miss, got %q", key)
}
}

func TestExtractCCBuiltinSnapshotShape(t *testing.T) {
snap := extractCCBuiltinSnapshot([]TranscriptEntry{{Version: "2.1.150"}})
if snap == nil {
t.Fatal("expected snapshot for known version")
}
sum := snap["summary"].(map[string]interface{})
if est, _ := sum["estimated"].(bool); !est {
t.Error("expected estimated:true")
}
if v, _ := sum["cc_version"].(string); v != "2.1.150" {
t.Errorf("cc_version = %q, want 2.1.150", v)
}
// total_tokens excludes deferred (matches /context's visible total
// and API billing). For 2.1.150: system_prompt + system_tools.
total, _ := sum["total_tokens"].(int)
sp, _ := sum["system_prompt"].(int)
st, _ := sum["system_tools"].(int)
if total != sp+st {
t.Errorf("total_tokens = %d, want system_prompt(%d) + system_tools(%d) = %d", total, sp, st, sp+st)
}
deferred, _ := sum["deferred_tokens"].(int)
df, _ := sum["deferred_tools"].(int)
if deferred != df {
t.Errorf("deferred_tokens = %d, want deferred_tools = %d", deferred, df)
}
if extractCCBuiltinSnapshot(nil) != nil {
t.Error("nil entries should yield nil snapshot")
}
if extractCCBuiltinSnapshot([]TranscriptEntry{{Version: "99.99.99"}}) != nil {
t.Error("unknown version should yield nil snapshot")
}
}
Loading
Loading