@@ -25,6 +25,10 @@ interface PlanData {
2525 average_ms ?: number ;
2626 uptime_percent ?: number ;
2727 } ;
28+ has_open_weight ?: boolean ;
29+ has_closed_source ?: boolean ;
30+ has_cli ?: boolean ;
31+ has_ide ?: boolean ;
2832}
2933
3034interface Props {
@@ -48,17 +52,6 @@ interface ScoredPlan {
4852 reasons : string [ ] ;
4953}
5054
51- const OPEN_WEIGHT_PATTERNS = [
52- 'deepseek' , 'mistral' , 'llama' , 'qwen' , 'ollama' , 'glm' , 'kimi' , 'minimax' ,
53- ] ;
54-
55- const CLOSED_SOURCE_PATTERNS = [
56- 'gpt' , 'claude' , 'gemini' , 'copilot' ,
57- ] ;
58-
59- const CLI_PATTERNS = [ 'cli' , 'claude-code' , 'claudes-code' , 'opencode' , 'terminal' , 'command-line' , 'gemini-cli' , 'aider' , 'codex' ] ;
60- const IDE_PATTERNS = [ 'cursor' , 'vscode' , 'vs code' , 'jetbrains' , 'intellij' , 'pycharm' , 'webstorm' , 'idea' , 'edlide' , 'kiro' ] ;
61-
6255function isUnlimited ( val : number | string | null | undefined ) : boolean {
6356 if ( val == null ) return false ;
6457 if ( typeof val === 'string' ) return val . toLowerCase ( ) . includes ( 'unlimited' ) ;
@@ -89,10 +82,16 @@ function scorePlan(plan: PlanData, state: FormState): ScoredPlan {
8982 score += Math . round ( headroom * 25 ) ;
9083 if ( headroom >= 1 ) reasons . push ( `${ ( tpm / 1000 ) . toFixed ( 0 ) } k tokens/min — fits your usage` ) ;
9184 else reasons . push ( `${ ( tpm / 1000 ) . toFixed ( 0 ) } k tokens/min — may be tight` ) ;
85+ } else {
86+ score += 25 ; // Unknown/dynamic limit, assume it fits to avoid penalizing
87+ reasons . push ( 'Dynamic token limits' ) ;
9288 }
9389 if ( cw != null ) {
9490 score += Math . min ( 15 , Math . round ( ( cw / 200_000 ) * 15 ) ) ;
9591 if ( cw >= 128_000 ) reasons . push ( `${ ( cw / 1000 ) . toFixed ( 0 ) } k context window` ) ;
92+ } else {
93+ score += 15 ;
94+ reasons . push ( 'Dynamic context window' ) ;
9695 }
9796 } else {
9897 const rpm = plan . limits . requests_per_minute ;
@@ -103,6 +102,9 @@ function scorePlan(plan: PlanData, state: FormState): ScoredPlan {
103102 score += Math . round ( headroom * 25 ) ;
104103 if ( headroom >= 1 ) reasons . push ( `${ rpm } requests/min — fits your usage` ) ;
105104 else reasons . push ( `${ rpm } requests/min — may be tight` ) ;
105+ } else {
106+ score += 25 ; // Dynamic limits
107+ reasons . push ( `Dynamic request limits` ) ;
106108 }
107109 if ( isUnlimited ( daily ) ) {
108110 score += 15 ;
@@ -112,6 +114,9 @@ function scorePlan(plan: PlanData, state: FormState): ScoredPlan {
112114 const headroom = Math . min ( 1 , daily / Math . max ( dayTotal , 1 ) ) ;
113115 score += Math . round ( headroom * 15 ) ;
114116 if ( headroom >= 1 ) reasons . push ( `${ daily } messages/day — enough for you` ) ;
117+ } else if ( daily == null ) {
118+ score += 15 ;
119+ reasons . push ( 'Dynamic daily messages' ) ;
115120 }
116121 }
117122 }
@@ -120,12 +125,10 @@ function scorePlan(plan: PlanData, state: FormState): ScoredPlan {
120125 // Tool type (0-30 pts) — CLI vs IDE
121126 if ( state . tool ) {
122127 maxScore += 30 ;
123- const toolStr = plan . tools_compatible . join ( ' ' ) . toLowerCase ( ) ;
124- const patterns = state . tool === 'cli' ? CLI_PATTERNS : IDE_PATTERNS ;
125128 const label = state . tool === 'cli' ? 'CLI' : 'IDE' ;
129+ const hasMatch = state . tool === 'cli' ? plan . has_cli : plan . has_ide ;
126130
127- const matches = patterns . filter ( p => toolStr . includes ( p ) ) . length ;
128- if ( matches > 0 ) {
131+ if ( hasMatch ) {
129132 score += 30 ;
130133 reasons . push ( `Compatible with ${ label } ` ) ;
131134 } else {
@@ -136,12 +139,10 @@ function scorePlan(plan: PlanData, state: FormState): ScoredPlan {
136139 // Model preference (0-30 pts) — open weight vs closed source
137140 if ( state . modelPref ) {
138141 maxScore += 30 ;
139- const modelStr = plan . models . join ( ' ' ) . toLowerCase ( ) ;
140- const patterns = state . modelPref === 'open_weight' ? OPEN_WEIGHT_PATTERNS : CLOSED_SOURCE_PATTERNS ;
141142 const label = state . modelPref === 'open_weight' ? 'open-weight' : 'closed-source' ;
143+ const hasMatch = state . modelPref === 'open_weight' ? plan . has_open_weight : plan . has_closed_source ;
142144
143- const matched = patterns . filter ( p => modelStr . includes ( p ) ) ;
144- if ( matched . length > 0 ) {
145+ if ( hasMatch ) {
145146 score += 30 ;
146147 reasons . push ( `Has ${ label } models` ) ;
147148 } else {
@@ -207,7 +208,8 @@ export default function RecommendationWizard({ plans }: Props) {
207208
208209 { /* Row 1: Usage Intensity */ }
209210 < div className = "space-y-3" >
210- < h3 className = "font-semibold text-sm" > Usage Intensity (5 hrs/day)</ h3 >
211+ < h3 className = "font-semibold text-sm" > Average Usage Intensity (5 hrs/day)</ h3 >
212+ < p className = "text-xs text-base-content/60" > Input your *average* expected usage for a 5-hour window, not absolute burst limits.</ p >
211213 < div className = "grid grid-cols-2 gap-3" >
212214 { ( [
213215 { value : 'token_avg' as UsageIntensity , label : 'Token Average' , placeholder : 'e.g. 50000' , icon : '🔢' } ,
0 commit comments