perf(pricing): memoize normalizeModelName#67
Merged
alexgreensh merged 1 commit intoJun 16, 2026
Conversation
normalizeModelName runs a ~60-branch linear scan of String.includes checks to map a model id to a pricing key, and it sits on hot paths: once per usage bucket and again inside calculateCost during session parsing, plus per turn and across the savings / quality / dashboard accumulation loops. A 30-day audit calls it many thousands of times, almost always on a small set of repeating model-id strings. Add a module-level Map cache keyed on the raw model id so repeat lookups are O(1). The matching logic is unchanged (extracted verbatim into a private computeNormalizedModelName); output is verified byte-identical across ~70 model strings including provider prefixes, every GPT-5/Gemini variant, and null cases. The cache is bounded (cap 4096) and cleared by resetPricingCache().
alexgreensh
added a commit
that referenced
this pull request
Jun 16, 2026
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alexgreensh
added a commit
that referenced
this pull request
Jun 16, 2026
Torture-room follow-up to PRs #62-#67. The O_NOFOLLOW symlink-refusal hardening landed in continuity.ts and smart-compact.ts but two sibling write paths in checkpoint-policy.ts were missed (4-agent convergence in the gauntlet): - persistState() policy-state write (was bare fs.writeFileSync) - appendCheckpointEvent() telemetry append (was openSync('a')) Changes: - Extract the duplicated writeFileNoFollow() into a single openclaw/src/fs-utils.ts (was copy-pasted in continuity.ts + smart-compact.ts) and add appendFileNoFollow(). - Apply O_NOFOLLOW to persistState and appendCheckpointEvent. - Make appendCheckpointManifest best-effort so an O_NOFOLLOW ELOOP can't crash a checkpoint capture (an unregistered artifact is a harmless orphan). Verified: tsc clean, runtime smoke test confirms write/append correctness, 0600 mode, and ELOOP symlink refusal with target file left intact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alexgreensh
added a commit
that referenced
this pull request
Jun 16, 2026
Merges contributor PRs #62-#67 (openclaw port): negative/non-finite token clamp, streamed first-prompt read, bounded session maps, O_NOFOLLOW checkpoint hardening, single-read costly-prompts, memoized model normalization. Plus a torture-room follow-up completing O_NOFOLLOW coverage in checkpoint-policy.ts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner
|
Merged in v5.11.13 — clean null-aware memoization (nice use of .has() over a falsy check). Thanks! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
normalizeModelName()maps a model id (e.g.anthropic/claude-sonnet-4-...) to a pricing key by running up to ~60 sequentialString.includes()checks (Anthropic -> GPT-5 family -> GPT-4 -> reasoning -> Gemini -> DeepSeek -> Qwen -> ...). It sits on hot paths and is called many thousands of times per audit:parseSession, and again insidecalculateCost;parseSessionTurns;Almost every call is on a small, repeating set of model-id strings, so the same ~60-branch scan is redone over and over.
What
Memoize the result.
normalizeModelName()now consults a module-levelMapcache keyed on the raw model id; repeat lookups become O(1) instead of a linear scan. The matching logic is unchanged.How
computeNormalizedModelName(modelId).normalizeModelName()checks the cache withMap.has()(not a falsy check, so a cachednull— for empty or<...>ids — is distinguished from a miss), computes on a miss, and stores the result._normalizeCache.clear()into the existingresetPricingCache(), mirroring the file's existing_mergedPricingcache idiom.Validation
npm run build(tsc) passes;dist/regenerated and committed in lockstep withsrc.anthropic/,openrouter/openai/,anthropic:), mixed case, every GPT-5 / GPT-4 / Gemini / DeepSeek / Qwen / Mistral / Grok / local variant, and null-producing inputs ("","<...>").resetPricingCache()confirmed to clear the cache without changing results.normalizeModelNameis a pure function of its argument (no dependency on pricing config or env), so caching is safe regardless of config reload..d.tssignature change;computeNormalizedModelNameis private.Scope
Narrow by design. A parallel
normalizeModelNameexists inopencode/src/pricing.tswithout this cache; mirroring it there is a possible follow-up (lower urgency — OpenCode reprices per session, not per turn).