From 95e76f6a561d568b834fa61d461f9b36423408f8 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 27 Mar 2026 14:31:56 +0800 Subject: [PATCH] feat: enhance token estimation and context limit handling in strategy configurations --- kernel/engine_analysis.go | 33 ++++++++++--------- mcp/client.go | 3 ++ store/strategy.go | 26 ++++++++++++++- .../components/strategy/CoinSourceEditor.tsx | 14 ++++---- .../components/strategy/TokenEstimateBar.tsx | 33 ++++--------------- web/src/i18n/translations.ts | 12 +++---- web/src/pages/StrategyStudioPage.tsx | 13 ++++++++ 7 files changed, 78 insertions(+), 56 deletions(-) diff --git a/kernel/engine_analysis.go b/kernel/engine_analysis.go index b367b1ac..8c08ba0b 100644 --- a/kernel/engine_analysis.go +++ b/kernel/engine_analysis.go @@ -55,24 +55,27 @@ func GetFullDecisionWithStrategy(ctx *Context, mcpClient mcp.AIClient, engine *S engineConfig := engine.GetConfig() engineConfig.ClampLimits() - // Token estimation check — warn or block if exceeding all known model limits + // Token estimation check — block if exceeding the specific model's context limit estimate := engineConfig.EstimateTokens() - allExceed := true - anyWarning := false - for _, ml := range estimate.ModelLimits { - if ml.UsagePct <= 100 { - allExceed = false - } - if ml.UsagePct >= 80 { - anyWarning = true - } + + // Determine context limit for the specific model being used + contextLimit := 131072 // safe default (strictest common limit) + var providerName string + if embedder, ok := mcpClient.(mcp.ClientEmbedder); ok { + base := embedder.BaseClient() + providerName = base.Provider + contextLimit = store.GetContextLimitForClient(base.Provider, base.Model) } - if allExceed && len(estimate.ModelLimits) > 0 { - logger.Errorf("🚫 Token estimate %d exceeds ALL known model context limits — blocking analysis", estimate.Total) - return nil, fmt.Errorf("estimated %d tokens exceeds all known model context limits; reduce coins, timeframes, or K-line count", estimate.Total) + + if estimate.Total > contextLimit { + logger.Errorf("🚫 Token estimate %d exceeds %s context limit %d — blocking analysis", + estimate.Total, providerName, contextLimit) + return nil, fmt.Errorf("estimated %d tokens exceeds model context limit of %d; reduce coins, timeframes, or K-line count", + estimate.Total, contextLimit) } - if anyWarning { - logger.Infof("⚠️ Token estimate %d — approaching context limits for some models", estimate.Total) + if estimate.Total*100/contextLimit >= 80 { + logger.Infof("⚠️ Token estimate %d — approaching %s context limit %d", + estimate.Total, providerName, contextLimit) } // 1. Fetch market data using strategy config diff --git a/mcp/client.go b/mcp/client.go index 047bc0e9..b70ac1cf 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -392,6 +392,9 @@ func (client *Client) String() string { client.Provider, client.Model) } +// BaseClient returns the underlying *Client (satisfies ClientEmbedder interface). +func (c *Client) BaseClient() *Client { return c } + // IsRetryableError determines if error is retryable (network errors, timeouts, etc.) func (client *Client) IsRetryableError(err error) bool { errStr := err.Error() diff --git a/store/strategy.go b/store/strategy.go index ebc2f06c..2ef062bd 100644 --- a/store/strategy.go +++ b/store/strategy.go @@ -12,7 +12,7 @@ import ( // Hard limits to prevent token explosion in AI requests const ( - MaxCandidateCoins = 3 + MaxCandidateCoins = 50 MaxPositions = 3 MaxTimeframes = 4 MinKlineCount = 10 @@ -622,6 +622,30 @@ func GetContextLimit(provider string) int { return 131072 // safe default } +// GetContextLimitForClient returns context limit for a provider+model pair. +// For claw402, the underlying model is inferred from the model name prefix. +func GetContextLimitForClient(provider, model string) int { + if provider == "claw402" { + switch { + case strings.HasPrefix(model, "claude"): + return ModelContextLimits["claude"] + case strings.HasPrefix(model, "gpt"), strings.HasPrefix(model, "o1"), strings.HasPrefix(model, "o3"): + return ModelContextLimits["openai"] + case strings.HasPrefix(model, "gemini"): + return ModelContextLimits["gemini"] + case strings.HasPrefix(model, "grok"): + return ModelContextLimits["grok"] + case strings.HasPrefix(model, "kimi"): + return ModelContextLimits["kimi"] + case strings.HasPrefix(model, "qwen"): + return ModelContextLimits["qwen"] + default: + return ModelContextLimits["deepseek"] + } + } + return GetContextLimit(provider) +} + // EstimateTokens estimates the total token count for a strategy configuration. // This is a pure computation based on config fields — no network calls. func (c *StrategyConfig) EstimateTokens() TokenEstimate { diff --git a/web/src/components/strategy/CoinSourceEditor.tsx b/web/src/components/strategy/CoinSourceEditor.tsx index fd2b7439..14cf1936 100644 --- a/web/src/components/strategy/CoinSourceEditor.tsx +++ b/web/src/components/strategy/CoinSourceEditor.tsx @@ -71,7 +71,7 @@ export function CoinSourceEditor({ return xyzDexAssets.has(base) } - const MAX_STATIC_COINS = 3 + const MAX_STATIC_COINS = 50 const showToast = (msg: string) => { const toast = document.createElement('div') @@ -333,7 +333,7 @@ export function CoinSourceEditor({ onChange({ ...config, ai500_limit: parseInt(val) || 10 }) } disabled={disabled} - options={[1, 2, 3].map(n => ({ value: n, label: String(n) }))} + options={[3, 5, 10, 20, 30, 40, 50].map(n => ({ value: n, label: String(n) }))} className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text" /> @@ -387,7 +387,7 @@ export function CoinSourceEditor({ onChange({ ...config, oi_top_limit: parseInt(val) || 10 }) } disabled={disabled} - options={[1, 2, 3].map(n => ({ value: n, label: String(n) }))} + options={[3, 5, 10, 20, 30, 40, 50].map(n => ({ value: n, label: String(n) }))} className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text" /> @@ -441,7 +441,7 @@ export function CoinSourceEditor({ onChange({ ...config, oi_low_limit: parseInt(val) || 10 }) } disabled={disabled} - options={[1, 2, 3].map(n => ({ value: n, label: String(n) }))} + options={[3, 5, 10, 20, 30, 40, 50].map(n => ({ value: n, label: String(n) }))} className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text" /> @@ -495,7 +495,7 @@ export function CoinSourceEditor({ value={config.ai500_limit || 10} onChange={(val) => !disabled && onChange({ ...config, ai500_limit: parseInt(val) || 10 })} disabled={disabled} - options={[5, 10, 15, 20, 30, 50].map(n => ({ value: n, label: String(n) }))} + options={[3, 5, 10, 20, 30, 40, 50].map(n => ({ value: n, label: String(n) }))} className="px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text" /> @@ -535,7 +535,7 @@ export function CoinSourceEditor({ value={config.oi_top_limit || 10} onChange={(val) => !disabled && onChange({ ...config, oi_top_limit: parseInt(val) || 10 })} disabled={disabled} - options={[5, 10, 15, 20, 30, 50].map(n => ({ value: n, label: String(n) }))} + options={[3, 5, 10, 20, 30, 40, 50].map(n => ({ value: n, label: String(n) }))} className="px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text" /> @@ -575,7 +575,7 @@ export function CoinSourceEditor({ value={config.oi_low_limit || 10} onChange={(val) => !disabled && onChange({ ...config, oi_low_limit: parseInt(val) || 10 })} disabled={disabled} - options={[5, 10, 15, 20, 30, 50].map(n => ({ value: n, label: String(n) }))} + options={[3, 5, 10, 20, 30, 40, 50].map(n => ({ value: n, label: String(n) }))} className="px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text" /> diff --git a/web/src/components/strategy/TokenEstimateBar.tsx b/web/src/components/strategy/TokenEstimateBar.tsx index 9dd33a7e..ec4a5707 100644 --- a/web/src/components/strategy/TokenEstimateBar.tsx +++ b/web/src/components/strategy/TokenEstimateBar.tsx @@ -21,10 +21,10 @@ interface TokenEstimateResult { interface TokenEstimateBarProps { config: StrategyConfig | null language: Language - onOverflowChange?: (overflow: boolean) => void + onTokenCountChange?: (total: number) => void } -export function TokenEstimateBar({ config, language, onOverflowChange }: TokenEstimateBarProps) { +export function TokenEstimateBar({ config, language, onTokenCountChange }: TokenEstimateBarProps) { const [estimate, setEstimate] = useState(null) const [isLoading, setIsLoading] = useState(false) const debounceRef = useRef | null>(null) @@ -52,6 +52,7 @@ export function TokenEstimateBar({ config, language, onOverflowChange }: TokenEs if (response.ok) { const data = await response.json() setEstimate(data) + onTokenCountChange?.(data.total) } } catch { // silently ignore — non-critical UI element @@ -67,15 +68,6 @@ export function TokenEstimateBar({ config, language, onOverflowChange }: TokenEs } }, [config]) - useEffect(() => { - if (!estimate) { - onOverflowChange?.(false) - return - } - const maxPct = estimate.model_limits.reduce((max, ml) => Math.max(max, ml.usage_pct), 0) - onOverflowChange?.(maxPct >= 100) - }, [estimate, onOverflowChange]) - if (!config) return null if (isLoading && !estimate) { @@ -89,14 +81,8 @@ export function TokenEstimateBar({ config, language, onOverflowChange }: TokenEs if (!estimate) return null - // Find the strictest model (smallest context limit = highest usage_pct) - const strictest = estimate.model_limits.reduce( - (max, ml) => (ml.usage_pct > max.usage_pct ? ml : max), - estimate.model_limits[0] - ) - if (!strictest) return null - - const pct = strictest.usage_pct + // Display based on 200K reference + const pct = Math.round(estimate.total * 100 / 200000) const barWidth = Math.min(pct, 100) let barColor = '#0ECB81' // green @@ -109,8 +95,6 @@ export function TokenEstimateBar({ config, language, onOverflowChange }: TokenEs textColor = '#F0B90B' } - const exceedWarning = pct >= 100 ? tr('tokenExceedWarning') : null - return (
@@ -129,15 +113,10 @@ export function TokenEstimateBar({ config, language, onOverflowChange }: TokenEs
- {tr('tokenTooltip')} ({strictest.name} {(strictest.context_limit / 1000).toFixed(0)}K) + {tr('tokenTooltip')} (~{estimate.total.toLocaleString()} / 200K)
- {exceedWarning && ( -

- {exceedWarning} -

- )}
) } diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index fded6020..1db615d2 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -1087,9 +1087,9 @@ export const translations = { generatePromptPreview: 'Click to generate prompt preview', runAiTestHint: 'Click to run AI test', tokenEstimate: 'Token Estimate', - tokenExceedWarning: 'Exceeds context limit. Reduce coins or timeframes.', + tokenExceedWarning: 'Token estimate exceeds 128K. AI requests may fail for some models.', tokenEstimating: 'Estimating...', - tokenTooltip: 'Based on strictest model', + tokenTooltip: 'Based on 200K context', }, // Metric Tooltip @@ -2388,9 +2388,9 @@ export const translations = { generatePromptPreview: '点击生成 Prompt 预览', runAiTestHint: '点击运行 AI 测试', tokenEstimate: 'Token 预估', - tokenExceedWarning: '超出上下文限制,建议减少币种或时间框架', + tokenExceedWarning: 'Token 估算超过 128K,部分模型请求可能失败', tokenEstimating: '预估中...', - tokenTooltip: '基于最严格模型计算', + tokenTooltip: '基于 200K 上下文计算', }, // Metric Tooltip @@ -3491,9 +3491,9 @@ export const translations = { generatePromptPreview: 'Klik untuk generate pratinjau prompt', runAiTestHint: 'Klik untuk menjalankan uji AI', tokenEstimate: 'Estimasi Token', - tokenExceedWarning: 'Melebihi batas konteks. Kurangi koin atau timeframe.', + tokenExceedWarning: 'Estimasi token melebihi 128K. Permintaan AI mungkin gagal untuk beberapa model.', tokenEstimating: 'Mengestimasi...', - tokenTooltip: 'Berdasarkan model paling ketat', + tokenTooltip: 'Berdasarkan konteks 200K', }, // Metric Tooltip diff --git a/web/src/pages/StrategyStudioPage.tsx b/web/src/pages/StrategyStudioPage.tsx index a46c2714..17a9579f 100644 --- a/web/src/pages/StrategyStudioPage.tsx +++ b/web/src/pages/StrategyStudioPage.tsx @@ -38,6 +38,7 @@ import { RiskControlEditor } from '../components/strategy/RiskControlEditor' import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEditor' import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor' import { GridConfigEditor, defaultGridConfig } from '../components/strategy/GridConfigEditor' +import { TokenEstimateBar } from '../components/strategy/TokenEstimateBar' import { DeepVoidBackground } from '../components/common/DeepVoidBackground' import { t } from '../i18n/translations' @@ -52,6 +53,7 @@ export function StrategyStudioPage() { const [editingConfig, setEditingConfig] = useState(null) const [isLoading, setIsLoading] = useState(true) const [isSaving, setIsSaving] = useState(false) + const [estimatedTokens, setEstimatedTokens] = useState(0) const [error, setError] = useState(null) const [hasChanges, setHasChanges] = useState(false) @@ -397,6 +399,10 @@ export function StrategyStudioPage() { // Save strategy const handleSaveStrategy = async () => { if (!token || !selectedStrategy || !editingConfig) return + if (estimatedTokens >= 128000 && currentStrategyType === 'ai_trading') { + notify.warning(tr('tokenExceedWarning')) + // continue with save + } setIsSaving(true) try { // Always sync the config language with the current interface language @@ -826,6 +832,13 @@ export function StrategyStudioPage() { + {/* Token Estimate Bar */} + {currentStrategyType === 'ai_trading' && ( +
+ +
+ )} + {/* Strategy Type Selector */} {editingConfig && (