-
Notifications
You must be signed in to change notification settings - Fork 2
Add proactive context window management to prevent AI API errors #708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
86c2009
8a5e5d5
72fb8d9
a907fab
ee03ef9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -48,16 +48,21 @@ export interface ContextCalculation { | |||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Estimate tokens in a text string | ||||||||||||
| * Uses 4 characters per token as a rough estimate | ||||||||||||
| * This is conservative - actual token count may be slightly lower | ||||||||||||
| * Estimate tokens in a text string. | ||||||||||||
| * Uses ~4 chars/token for Latin text, ~2 chars/token when significant | ||||||||||||
| * non-ASCII / CJK content is detected (CJK characters often tokenize to 1-2 tokens each). | ||||||||||||
| */ | ||||||||||||
| export function estimateTokens(text: string): number { | ||||||||||||
| if (!text) return 0; | ||||||||||||
|
|
||||||||||||
| // GPT-style tokenization: ~4 characters per token | ||||||||||||
| // This is conservative to avoid underestimating | ||||||||||||
| return Math.ceil(text.length / 4); | ||||||||||||
| // Detect non-ASCII heavy content (CJK, emoji, etc.) | ||||||||||||
| // CJK Unified Ideographs, Hiragana, Katakana, Hangul, etc. | ||||||||||||
| const nonAsciiCount = (text.match(/[^\x00-\x7F]/g) || []).length; | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Check if the file exists and read the content around line 60
fd "ai-context-calculator.ts" --type fRepository: 2witstudios/PageSpace Length of output: 117 🏁 Script executed: # Once we confirm the file exists, read it to verify the exact content at line 60
cat -n packages/lib/src/monitoring/ai-context-calculator.ts | sed -n '55,65p'Repository: 2witstudios/PageSpace Length of output: 636 🏁 Script executed: # Check for linter configuration in the packages/lib directory
find packages/lib -maxdepth 3 -name "*.json" -o -name "*.mjs" -o -name "biome.json" -o -name ".eslintrc*" | head -20Repository: 2witstudios/PageSpace Length of output: 150 🏁 Script executed: # Check if Biome is configured at the root or in packages
find . -maxdepth 2 -name "biome.json" -o -name "biome.jsonc"Repository: 2witstudios/PageSpace Length of output: 77 🏁 Script executed: # Read the biome.json configuration
cat biome.jsonRepository: 2witstudios/PageSpace Length of output: 1839 🏁 Script executed: # Check the suggested fix in the review comment - verify if the logic is equivalent
python3 << 'EOF'
# Test if the suggested fix produces the same results as the original
# Original: text.match(/[^\x00-\x7F]/g) matches all non-ASCII characters
# Suggested: reduce with codePointAt check for > 0x7f
test_strings = [
"hello", # All ASCII
"hello🌍", # ASCII + emoji
"你好", # Chinese characters
"café", # ASCII + accented char
"مرحبا", # Arabic
]
for test_str in test_strings:
# Simulate original regex behavior
original_count = len([c for c in test_str if ord(c) > 0x7f])
# Simulate suggested fix behavior
suggested_count = 0
for char in test_str:
code_point = ord(char)
if code_point > 0x7f:
suggested_count += 1
print(f"String: '{test_str}' | Original: {original_count} | Suggested: {suggested_count} | Match: {original_count == suggested_count}")
EOFRepository: 2witstudios/PageSpace Length of output: 357 🏁 Script executed: # Check if noControlCharactersInRegex is part of Biome's recommended rules
# and verify if it would be active with the current config
web_search "Biome noControlCharactersInRegex rule"Repository: 2witstudios/PageSpace Length of output: 115 🏁 Script executed: # Search for other similar patterns that might have the same issue
rg -n '\\x00|\\x[0-9A-Fa-f]{2}' --type=ts --type=tsx packages/Repository: 2witstudios/PageSpace Length of output: 92 🏁 Script executed: # Verify the suggested fix doesn't have any edge cases with surrogate pairs or multi-byte chars
python3 << 'EOF'
# Test edge cases with the suggested fix
test_cases = [
("hello", 0), # All ASCII
("café", 1), # Single accented character
("🌍", 1), # Single emoji (surrogate pair in JS)
("你好世界", 4), # Chinese characters
("hello🌍world", 1), # Mixed ASCII and emoji
("\x00\x01\x7f", 0), # Control characters at edge of range
("", 0), # Empty string
]
for test_str, expected in test_cases:
# Suggested fix logic
count = 0
for char in test_str:
code_point = ord(char)
if code_point > 0x7f:
count += 1
status = "✓" if count == expected else "✗"
print(f"{status} String: {repr(test_str)} | Expected: {expected} | Got: {count}")
EOFRepository: 2witstudios/PageSpace Length of output: 355 🏁 Script executed: # Search for control character patterns in the codebase using ts file type only
rg -n '\\x00|\\x[0-9A-Fa-f]{2}' --type=ts packages/Repository: 2witstudios/PageSpace Length of output: 2413 🏁 Script executed: # Verify Biome rule documentation is available and check if recommended includes the suspicious rule
python3 << 'EOF'
# Check Biome's default rule settings for noControlCharactersInRegex
# Based on the biome.json, the "suspicious" category doesn't explicitly disable the rule,
# which means it uses the recommended default
biome_config = {
"suspicious": {
"noArrayIndexKey": "off",
"noExplicitAny": "off",
"noImplicitAnyLet": "off",
"noDoubleEquals": "off",
"noGlobalIsNan": "off",
"noAssignInExpressions": "off"
# noControlCharactersInRegex is NOT listed, so it uses the default (recommended = true for suspicious)
}
}
print("Analysis of biome.json configuration:")
print("- Linter is enabled: true")
print("- 'recommended' is enabled globally for linter rules")
print("- 'suspicious' category does NOT explicitly disable 'noControlCharactersInRegex'")
print("- Therefore, the rule IS active by default")
print("\nThe regex pattern /[^\\x00-\\x7F]/ contains control character escapes,")
print("which would trigger the 'noControlCharactersInRegex' rule in Biome.")
EOFRepository: 2witstudios/PageSpace Length of output: 443 Replace the regex pattern to resolve the The pattern Suggested fix- const nonAsciiCount = (text.match(/[^\x00-\x7F]/g) || []).length;
+ const nonAsciiCount = [...text].reduce((count, char) => {
+ const codePoint = char.codePointAt(0) ?? 0;
+ return count + (codePoint > 0x7f ? 1 : 0);
+ }, 0);📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome (2.4.4)[error] 60-60: Unexpected control character in a regular expression. (lint/suspicious/noControlCharactersInRegex) 🤖 Prompt for AI Agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Investigated — the regex The regex approach is also slightly more conservative for surrogate pairs (emoji), since each surrogate half counts separately, inflating
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Your point about the conservative behavior with surrogate pairs is excellent—counting each surrogate half separately does inflate I've noted this for future reference. Thanks for the clarification! ✏️ Learnings added
🧠 Learnings used |
||||||||||||
| const nonAsciiRatio = nonAsciiCount / text.length; | ||||||||||||
|
|
||||||||||||
| // Use 2 chars/token when >20% non-ASCII (CJK-heavy), else 4 chars/token | ||||||||||||
| const charsPerToken = nonAsciiRatio > 0.2 ? 2 : 4; | ||||||||||||
| return Math.ceil(text.length / charsPerToken); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
|
|
@@ -137,6 +142,42 @@ export function getContextWindowSize(model: string, provider?: string): number { | |||||||||||
| const providerLower = provider?.toLowerCase() || ''; | ||||||||||||
| const modelLower = model.toLowerCase(); | ||||||||||||
|
|
||||||||||||
| // OpenRouter must be checked first — its models contain names like 'claude', 'gpt', 'gemini' | ||||||||||||
| // that would otherwise match the provider-specific branches below. | ||||||||||||
| if (providerLower === 'openrouter') { | ||||||||||||
| // Claude models via OpenRouter | ||||||||||||
| if (modelLower.includes('claude')) return 200_000; | ||||||||||||
| // Gemini models via OpenRouter | ||||||||||||
| if (modelLower.includes('gemini-2.5')) return 1_000_000; | ||||||||||||
| if (modelLower.includes('gemini-2.0') || modelLower.includes('gemini-1.5')) return 1_000_000; | ||||||||||||
| // GPT models via OpenRouter | ||||||||||||
| if (modelLower.includes('gpt-5.2')) { | ||||||||||||
| return modelLower.includes('mini') || modelLower.includes('nano') ? 256_000 : 400_000; | ||||||||||||
| } | ||||||||||||
| if (modelLower.includes('gpt-5.1')) return 400_000; | ||||||||||||
| if (modelLower.includes('gpt-5')) { | ||||||||||||
| return modelLower.includes('mini') || modelLower.includes('nano') ? 128_000 : 272_000; | ||||||||||||
| } | ||||||||||||
| if (modelLower.includes('gpt-4o') || modelLower.includes('gpt-4-turbo')) return 128_000; | ||||||||||||
| // Grok models via OpenRouter | ||||||||||||
| if (modelLower.includes('grok-4-fast')) return 2_000_000; | ||||||||||||
| if (modelLower.includes('grok')) return 128_000; | ||||||||||||
| // DeepSeek models - commonly 64k or 128k | ||||||||||||
| if (modelLower.includes('deepseek-r1') || modelLower.includes('deepseek-v3')) return 128_000; | ||||||||||||
| if (modelLower.includes('deepseek')) return 64_000; | ||||||||||||
| // Qwen models | ||||||||||||
| if (modelLower.includes('qwen-2.5') || modelLower.includes('qwq')) return 128_000; | ||||||||||||
| if (modelLower.includes('qwen')) return 32_000; | ||||||||||||
| // Llama models | ||||||||||||
| if (modelLower.includes('llama-3') || modelLower.includes('llama3')) return 128_000; | ||||||||||||
| if (modelLower.includes('llama')) return 32_000; | ||||||||||||
| // Mistral models | ||||||||||||
| if (modelLower.includes('mistral-large') || modelLower.includes('mistral-nemo')) return 128_000; | ||||||||||||
| if (modelLower.includes('mistral')) return 32_000; | ||||||||||||
| // OpenRouter platform hard cap is 400k for many endpoints - use 200k as safe default | ||||||||||||
| return 200_000; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // OpenAI models | ||||||||||||
| if (providerLower === 'openai' || modelLower.includes('gpt')) { | ||||||||||||
| // GPT-5.2 models (400k/256k context) | ||||||||||||
|
|
@@ -217,8 +258,8 @@ export function getContextWindowSize(model: string, provider?: string): number { | |||||||||||
| return 128_000; // Default for older MiniMax models | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // OpenRouter or unknown | ||||||||||||
| return 200_000; // Conservative default | ||||||||||||
| // Unknown provider/model - conservative default | ||||||||||||
| return 200_000; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
|
|
||||||||||||
Uh oh!
There was an error while loading. Please reload this page.