项目文件夹

文件
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

230 行
8.1 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
// Per-model, tokenizer-aware token estimation.
//
// Count (in tokens.go) always uses cl100k_base — fine for the
// provider-neutral "how much content is this" metrics. But the LLM
// providers gortex talks to do NOT all tokenize alike: GPT-4o / o-series
// use o200k_base, GPT-4 / GPT-3.5 use cl100k_base, and Claude / DeepSeek
// / Gemini each ship their own tokenizer that is not publicly available
// as a Go BPE.
//
// CountFor closes that gap. It resolves a model id to the closest real
// in-process tiktoken encoding (cl100k_base or o200k_base — both
// bundled offline, no network) and applies a per-family calibration
// ratio that corrects the proxy toward the family's true tokenizer.
// The result is a genuine per-model estimate instead of one cl100k
// number stretched by a single global scalar.
package tokens
import (
"math"
"strings"
"sync"
"github.com/pkoukk/tiktoken-go"
tiktoken_loader "github.com/pkoukk/tiktoken-go-loader"
)
// tiktoken encoding identifiers. Both are bundled by the offline BPE
// loader, so resolving either never touches the network.
const (
encodingCL100K = "cl100k_base"
encodingO200K = "o200k_base"
)
// Per-family calibration ratios. A ratio multiplies the raw BPE count
// from the proxy encoding to approximate the family's real tokenizer.
//
// - OpenAI families map onto their *actual* encoding, so ratio 1.0
// is exact, not an estimate.
// - claudeRatio (cl100k × 1.35) is the median ratio observed when
// sampling gortex's fixtures against Anthropic's count_tokens API;
// per-fixture variance runs ~28-42%, so it is honestly an estimate.
// - deepSeekRatio: DeepSeek's V3 byte-level BPE (128k vocab) is close
// to cl100k but a touch denser on source code.
// - geminiRatio: Gemini's SentencePiece tokenizer (256k vocab) sits
// near o200k on mixed code; 1.0 is the proxy's best single point.
const (
exactRatio = 1.0
claudeRatio = 1.35
deepSeekRatio = 1.05
geminiRatio = 1.0
)
// modelSpec ties a model family to the tiktoken encoding used to count
// it and the calibration ratio applied to that raw count.
type modelSpec struct {
family string
encoding string
ratio float64
}
// defaultSpec is used for an empty or unrecognised model id: plain
// cl100k_base with no correction — the same behaviour as Count.
var defaultSpec = modelSpec{family: "default", encoding: encodingCL100K, ratio: exactRatio}
// specForModel resolves a model id (as written in llm config — e.g.
// "claude-opus-4-7", "gpt-4o", "deepseek-chat", "o4-mini", or a bare
// Claude alias like "sonnet") to its counting spec. Matching is
// case-insensitive and order-sensitive: the o200k OpenAI check runs
// before the cl100k gpt-4 check because "gpt-4o" contains "gpt-4".
func specForModel(model string) modelSpec {
m := strings.ToLower(strings.TrimSpace(model))
switch {
case m == "":
return defaultSpec
case strings.Contains(m, "claude"),
strings.Contains(m, "opus"),
strings.Contains(m, "sonnet"),
strings.Contains(m, "haiku"):
return modelSpec{family: "claude", encoding: encodingCL100K, ratio: claudeRatio}
case strings.Contains(m, "deepseek"):
return modelSpec{family: "deepseek", encoding: encodingCL100K, ratio: deepSeekRatio}
case strings.Contains(m, "gemini"):
return modelSpec{family: "gemini", encoding: encodingO200K, ratio: geminiRatio}
case isOpenAIO200K(m):
return modelSpec{family: "openai-o200k", encoding: encodingO200K, ratio: exactRatio}
case strings.Contains(m, "gpt-4"),
strings.Contains(m, "gpt-3.5"),
strings.Contains(m, "gpt-35"):
return modelSpec{family: "openai-cl100k", encoding: encodingCL100K, ratio: exactRatio}
default:
return defaultSpec
}
}
// isOpenAIO200K reports whether m names an OpenAI model that tokenizes
// with o200k_base: the GPT-4o / GPT-4.1 / GPT-5 lines (including the
// Codex slugs) and the o1 / o3 / o4 reasoning series.
func isOpenAIO200K(m string) bool {
switch {
case strings.Contains(m, "gpt-4o"),
strings.Contains(m, "gpt-4.1"),
strings.Contains(m, "gpt-5"),
strings.Contains(m, "chatgpt-4o"):
return true
case strings.HasPrefix(m, "o1"),
strings.HasPrefix(m, "o3"),
strings.HasPrefix(m, "o4"):
return true
default:
return false
}
}
// CountFor returns the estimated token count of text for the named
// model, using that model's tokenizer family. An empty or unknown
// model id falls back to plain cl100k_base counting (identical to
// Count). When the encoder cannot be loaded it degrades to the
// chars/4 heuristic, exactly like Count.
func CountFor(model, text string) int {
if text == "" {
return 0
}
spec := specForModel(model)
enc, err := encoderFor(spec.encoding)
if err != nil || enc == nil {
return fallbackCount(text)
}
raw := len(enc.EncodeOrdinary(text))
if spec.ratio == exactRatio {
return raw
}
return int(math.Round(float64(raw) * spec.ratio))
}
// CountForInt64 is the int64 convenience wrapper for CountFor — used by
// call sites that store cumulative counts as int64.
func CountForInt64(model, text string) int64 {
return int64(CountFor(model, text))
}
// ScaleFromCL100K converts a token count already measured in cl100k_base
// into the given model's tokenizer-family count, applying the same
// calibration ratio CountFor uses. It lets a caller that only kept a
// provider-neutral cl100k count (the savings ledger) recover a per-model
// figure without re-tokenizing the original text.
//
// For Claude / DeepSeek — families CountFor itself encodes with
// cl100k_base and then scales — this is exact-equivalent to having called
// CountFor on the source. For the OpenAI-o200k / Gemini families CountFor
// re-encodes with o200k_base (ratio 1.0); here we have no o200k count, so
// we approximate it by the cl100k count (the two run within a few percent
// on mixed code). An empty or unknown model returns n unchanged.
func ScaleFromCL100K(model string, n int64) int64 {
if n <= 0 {
return n
}
spec := specForModel(model)
if spec.ratio == exactRatio {
return n
}
return int64(math.Round(float64(n) * spec.ratio))
}
// EncodingForModel returns the tiktoken encoding name (cl100k_base or
// o200k_base) that CountFor uses for the given model id.
func EncodingForModel(model string) string {
return specForModel(model).encoding
}
// ModelFamily returns the tokenizer-family label CountFor resolves the
// model id to ("claude", "openai-o200k", "openai-cl100k", "deepseek",
// "gemini", or "default"). Useful for telemetry and tests.
func ModelFamily(model string) string {
return specForModel(model).family
}
// EstimatorFor returns a reusable counter bound to one model — handy
// for hot loops (benchmarks, recall eval) that count many strings
// against the same model and want to resolve the spec just once.
func EstimatorFor(model string) func(string) int {
spec := specForModel(model)
return func(text string) int {
if text == "" {
return 0
}
enc, err := encoderFor(spec.encoding)
if err != nil || enc == nil {
return fallbackCount(text)
}
raw := len(enc.EncodeOrdinary(text))
if spec.ratio == exactRatio {
return raw
}
return int(math.Round(float64(raw) * spec.ratio))
}
}
// --- encoder cache ----------------------------------------------------
var (
bpeLoaderOnce sync.Once
encoderCache sync.Map // encoding name -> *encoderEntry
)
// encoderEntry lazily loads one tiktoken encoding exactly once.
type encoderEntry struct {
once sync.Once
enc *tiktoken.Tiktoken
err error
}
// encoderFor returns the cached tiktoken encoder for an encoding name,
// loading it (offline, from the bundled BPE assets) on first use. Each
// encoding is loaded at most once; concurrent callers share the
// result.
func encoderFor(name string) (*tiktoken.Tiktoken, error) {
bpeLoaderOnce.Do(func() {
// Offline loader: BPE rank tables are bundled in the binary, so
// resolving an encoding never needs network access — important
// for sealed environments and single-binary distribution.
tiktoken.SetBpeLoader(tiktoken_loader.NewOfflineLoader())
})
v, _ := encoderCache.LoadOrStore(name, &encoderEntry{})
e := v.(*encoderEntry)
e.once.Do(func() {
e.enc, e.err = tiktoken.GetEncoding(name)
})
return e.enc, e.err
}