Some checks failed
Deploy (Gitea) / deploy (push) Failing after 2s
File Reorganization: - Extracted core-context → symbolic-awareness (skill) - Extracted heartbeat → symbolic-events (skill) - Relocated 6 utility fragments, renamed 23 files, deleted system-model.lisp - Renamed gateway-* → channel-*, split gateway-messaging → 4 channel-* files - Renamed defskill/defpackage names to match new file prefixes - Deleted gateway-messaging.org/.lisp, removed core-context filter - Documented self-repair criterion, added AGENTS.md core boundary rule Token Economics (v0.5.0, skills not core): - tokenizer.lisp: count-tokens, model-token-ratio, token-cost, provider-token-cost (11 tests) - cost-tracker.lisp: cost-track-call, cost-session-total, cost-by-provider (6 tests) - token-economics.lisp: prompt-prefix-cached, context-assemble-cached, enforce-token-budget with CONTEXT_MAX_TOKENS env var (9 tests) Bug Fixes: - Fixed DeepSeek 400 (removed malformed tools from cascade) - Fixed UNDEFINED-FUNCTION crash (fboundp guards in think()) - Fixed gate-trace duplication (setf replaces list* in cognitive-verify) - Tightened dexador connect-timeout 10s→5s Test suite: 116/116 (100%)
147 lines
5.5 KiB
Common Lisp
147 lines
5.5 KiB
Common Lisp
(in-package :passepartout)
|
||
|
||
(defparameter *model-token-ratios*
|
||
'((:gpt-4o-mini . 4.0)
|
||
(:gpt-4o . 4.0)
|
||
(:gpt-3.5-turbo . 4.0)
|
||
(:claude-3-5-sonnet . 4.5)
|
||
(:claude-3-opus . 4.5)
|
||
(:claude-3-haiku . 4.5)
|
||
(:deepseek-chat . 4.0)
|
||
(:deepseek-reasoner . 4.0)
|
||
(:llama-3.1-70b . 3.5)
|
||
(:llama-3.1-405b . 3.5)
|
||
(:gemini-2.0-flash . 4.0)
|
||
(:gemini-1.5-pro . 4.0)
|
||
(:openrouter/auto . 4.0))
|
||
"Estimated characters per token for each model family.")
|
||
|
||
(defparameter *default-token-ratio* 4.0
|
||
"Fallback characters-per-token ratio when model is unknown.")
|
||
|
||
(defun model-token-ratio (model-keyword)
|
||
"Returns the estimated characters-per-token for MODEL-KEYWORD.
|
||
Falls back to *DEFAULT-TOKEN-RATIO* for unknown models."
|
||
(or (cdr (assoc model-keyword *model-token-ratios*))
|
||
*default-token-ratio*))
|
||
|
||
(defun count-tokens (text &key model)
|
||
"Returns the estimated token count for TEXT.
|
||
Uses character-count / ratio heuristic calibrated per model family.
|
||
MODEL is a keyword identifying the model (e.g. :gpt-4o-mini)."
|
||
(let ((clean (if (stringp text) text (format nil "~a" text))))
|
||
(ceiling (length clean) (model-token-ratio model))))
|
||
|
||
(defparameter *token-prices*
|
||
'((:gpt-4o-mini . 0.15) ; $0.15/1M input tokens
|
||
(:gpt-4o . 2.50) ; $2.50/1M input tokens
|
||
(:gpt-3.5-turbo . 0.50) ; $0.50/1M input tokens
|
||
(:claude-3-5-sonnet . 3.00) ; $3.00/1M input tokens
|
||
(:claude-3-opus . 15.00) ; $15.00/1M input tokens
|
||
(:claude-3-haiku . 0.25) ; $0.25/1M input tokens
|
||
(:deepseek-chat . 0.27) ; $0.27/1M input tokens
|
||
(:deepseek-reasoner . 0.55) ; $0.55/1M input tokens
|
||
(:llama-3.1-70b . 0.59) ; Groq: $0.59/1M
|
||
(:llama-3.1-405b . 1.30) ; NVIDIA NIM: ~$1.30/1M
|
||
(:gemini-2.0-flash . 0.10) ; $0.10/1M input
|
||
(:gemini-1.5-pro . 1.25)) ; $1.25/1M input
|
||
"Provider pricing in USD per 1M input tokens.
|
||
Prices sourced as of 2026-05. Output tokens cost 2-5× more;
|
||
we bill at input rates as a conservative estimate.")
|
||
|
||
(defun token-cost (model token-count)
|
||
"Returns the estimated cost in USD for TOKEN-COUNT tokens at MODEL's price.
|
||
Returns 0.0 for unknown models."
|
||
(let ((price-per-1m (or (cdr (assoc model *token-prices*)) 0.0)))
|
||
(* (/ price-per-1m 1000000.0) token-count)))
|
||
|
||
(defparameter *provider-default-models*
|
||
'((:deepseek . :deepseek-chat)
|
||
(:openai . :gpt-4o-mini)
|
||
(:anthropic . :claude-3-5-sonnet)
|
||
(:groq . :llama-3.1-70b)
|
||
(:gemini . :gemini-2.0-flash)
|
||
(:nvidia . :llama-3.1-405b)
|
||
(:openrouter . :openrouter/auto))
|
||
"Maps provider keywords to their default model families for cost tracking.")
|
||
|
||
(defun provider-token-cost (provider token-count)
|
||
"Returns the estimated cost in USD for a given PROVIDER and TOKEN-COUNT.
|
||
Uses the provider's default model for pricing."
|
||
(let ((model (cdr (assoc provider *provider-default-models*))))
|
||
(if model
|
||
(token-cost model token-count)
|
||
0.0)))
|
||
|
||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||
(ql:quickload :fiveam :silent t))
|
||
|
||
(defpackage :passepartout-tokenizer-tests
|
||
(:use :cl :fiveam :passepartout)
|
||
(:export #:tokenizer-suite))
|
||
|
||
(in-package :passepartout-tokenizer-tests)
|
||
|
||
(def-suite tokenizer-suite :description "Token counting and cost estimation")
|
||
(in-suite tokenizer-suite)
|
||
|
||
(test test-count-tokens-default
|
||
"Contract 1: count-tokens returns non-zero for a non-empty string."
|
||
(let ((count (count-tokens "hello world")))
|
||
(is (> count 0))
|
||
(is (integerp count))))
|
||
|
||
(test test-count-tokens-known-model
|
||
"Contract 1: count-tokens with a known model returns a count."
|
||
(let ((count (count-tokens "hello world" :model :gpt-4o-mini)))
|
||
(is (> count 0))
|
||
(is (integerp count))))
|
||
|
||
(test test-count-tokens-unknown-model
|
||
"Contract 1: count-tokens with an unknown model falls back to default."
|
||
(let ((count (count-tokens "hello world" :model :unknown-model-xyz)))
|
||
(is (> count 0))
|
||
(is (integerp count))))
|
||
|
||
(test test-count-tokens-empty
|
||
"Contract 1: count-tokens on empty string returns 0."
|
||
(let ((count (count-tokens "")))
|
||
(is (= 0 count))))
|
||
|
||
(test test-model-token-ratio-known
|
||
"Contract 2: known model returns correct ratio."
|
||
(is (= 4.0 (model-token-ratio :gpt-4o-mini)))
|
||
(is (= 4.5 (model-token-ratio :claude-3-5-sonnet)))
|
||
(is (= 3.5 (model-token-ratio :llama-3.1-70b))))
|
||
|
||
(test test-model-token-ratio-unknown
|
||
"Contract 2: unknown model returns default ratio."
|
||
(is (= 4.0 (model-token-ratio :unknown-model-abc))))
|
||
|
||
(test test-token-cost-known
|
||
"Contract 3: token-cost returns a number for known model."
|
||
(let ((cost (token-cost :gpt-4o-mini 1000)))
|
||
(is (numberp cost))
|
||
(is (> cost 0.0))))
|
||
|
||
(test test-token-cost-unknown
|
||
"Contract 3: token-cost returns 0.0 for unknown model."
|
||
(is (= 0.0 (token-cost :no-such-model 1000))))
|
||
|
||
(test test-provider-token-cost
|
||
"Contract: provider-token-cost maps provider to model price."
|
||
(let ((cost (provider-token-cost :deepseek 1000)))
|
||
(is (numberp cost))
|
||
(is (> cost 0.0))))
|
||
|
||
(test test-count-tokens-ratio-sensitivity
|
||
"Contract 1: longer text produces proportionally more tokens."
|
||
(let ((short (count-tokens "hi" :model :gpt-4o-mini))
|
||
(long (count-tokens "this is a much longer piece of text with many words in it" :model :gpt-4o-mini)))
|
||
(is (> long short))))
|
||
|
||
(test test-count-tokens-non-string
|
||
"Contract 1: non-string values are coerced and counted."
|
||
(let ((count (count-tokens 12345)))
|
||
(is (> count 0))))
|