Skip to content

Commit 67a29d9

Browse files
maresbvineethsai7
andauthored
fix: raise default max_tokens from 4000 to 8192, add --llm-max-tokens CLI flag (#51)
* fix: raise default max_tokens from 4000 to 8192 The default max_tokens of 4000 for LLMAnalyzer (and 8000 for MetaAnalyzer) frequently causes truncated JSON responses. In a batch of 200 skills, 30.5% of scans produced truncated output at the 4000-token limit; raising to 32768 at the call site eliminated all truncation. Set the default to 8192 — the Gemini API default for maxOutputTokens, and safely within the output ceiling of all major providers. Fixes #50 Made-with: Cursor * feat: add --llm-max-tokens CLI flag Expose max_tokens as a CLI argument so users can tune the output token budget without dropping to the Python SDK. Threaded through to both LLMAnalyzer and MetaAnalyzer via the centralized analyzer_factory. Made-with: Cursor * feat: add max_output_tokens to scan policy Add llm_analysis.max_output_tokens as a policy-level knob alongside the existing input budget fields. The CLI --llm-max-tokens flag overrides the policy value. Both the analyzer factory and _build_meta_analyzer now read from the policy when no CLI override is given. Update docs: custom-policy-configuration, policy-quick-reference, cli-command-reference, and llm-analyzer architecture. Made-with: Cursor * fix: add max_output_tokens to TUI, use is-not-None checks, add round-trip tests - Add max_output_tokens Input widget to policy TUI (configure-policy) so users can set the output token budget interactively - Replace `or` with `is not None` in analyzer_factory and cli for correct handling of explicit zero values - Add TestMaxOutputTokensPolicy with YAML round-trip, _to_dict, and preset coverage tests * fix: add max_output_tokens to all presets, expand test coverage - Explicitly set max_output_tokens: 8192 in strict_policy.yaml and permissive_policy.yaml (previously only inherited via deep merge) - Add tests: custom YAML override, CLI-beats-policy in build_analyzers, policy-used-when-CLI-is-None, MetaAnalyzer forwarding from CLI, MetaAnalyzer fallback to policy --------- Co-authored-by: vnarajal <vnarajal@cisco.com>
1 parent 3e0930f commit 67a29d9

18 files changed

Lines changed: 231 additions & 15 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ if not result.is_safe:
231231
| `--use-llm` | Enable LLM analyzer (requires API key) |
232232
| `--llm-provider` | LLM provider for CLI routing: `anthropic` or `openai` |
233233
| `--llm-consensus-runs N` | Run LLM analysis `N` times and keep majority-agreed findings |
234+
| `--llm-max-tokens N` | Maximum output tokens for LLM responses (default: 8192) |
234235
| `--use-virustotal` | Enable VirusTotal binary scanner |
235236
| `--vt-api-key KEY` | Provide VirusTotal API key directly (optional) |
236237
| `--vt-upload-files` | Upload unknown binaries to VirusTotal (optional) |

docs/architecture/analyzers/llm-analyzer.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ export AWS_REGION=us-east-1
281281
- Retries transient failures (`429`/timeouts/network issues) with exponential backoff
282282
- Supports async execution (`analyze_async`) and optional consensus passes
283283
- Applies prompt budget gates from policy (`llm_analysis.*`) and emits `LLM_CONTEXT_BUDGET_EXCEEDED` when content is skipped
284+
- Output token limit is controlled by `llm_analysis.max_output_tokens` in scan policy (default 8192), overridable via `--llm-max-tokens` CLI flag
284285

285286
## Error Handling
286287

docs/reference/cli-command-reference.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ usage: cli.py scan [-h] [--format {summary,json,markdown,table,sarif,html}]
104104
[--aidefense-api-key AIDEFENSE_API_KEY]
105105
[--aidefense-api-url AIDEFENSE_API_URL]
106106
[--llm-provider {anthropic,openai}]
107-
[--llm-consensus-runs N] [--use-trigger] [--enable-meta]
107+
[--llm-consensus-runs N] [--llm-max-tokens N]
108+
[--use-trigger] [--enable-meta]
108109
[--policy PRESET_OR_PATH] [--lenient] [--custom-rules PATH]
109110
[--taxonomy PATH] [--threat-mapping PATH]
110111
skill_directory
@@ -162,6 +163,8 @@ options:
162163
Run LLM analysis N times and keep only findings with
163164
majority agreement (reduces false positives, increases
164165
cost)
166+
--llm-max-tokens N Maximum output tokens for LLM responses (default:
167+
8192). Raise if scans produce truncated JSON.
165168
--use-trigger Enable trigger specificity analysis
166169
--enable-meta Enable meta-analysis FP filtering (2+ analyzers)
167170
--policy PRESET_OR_PATH
@@ -203,7 +206,8 @@ usage: cli.py scan-all [-h] [--recursive] [--check-overlap]
203206
[--aidefense-api-key AIDEFENSE_API_KEY]
204207
[--aidefense-api-url AIDEFENSE_API_URL]
205208
[--llm-provider {anthropic,openai}]
206-
[--llm-consensus-runs N] [--use-trigger]
209+
[--llm-consensus-runs N] [--llm-max-tokens N]
210+
[--use-trigger]
207211
[--enable-meta] [--policy PRESET_OR_PATH] [--lenient]
208212
[--custom-rules PATH] [--taxonomy PATH]
209213
[--threat-mapping PATH]
@@ -264,6 +268,8 @@ options:
264268
Run LLM analysis N times and keep only findings with
265269
majority agreement (reduces false positives, increases
266270
cost)
271+
--llm-max-tokens N Maximum output tokens for LLM responses (default:
272+
8192). Raise if scans produce truncated JSON.
267273
--use-trigger Enable trigger specificity analysis
268274
--enable-meta Enable meta-analysis FP filtering (2+ analyzers)
269275
--policy PRESET_OR_PATH

docs/reference/policy-quick-reference.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ Controls LLM context budget thresholds for LLM and meta analyzers. Content withi
253253
| max_code_file_chars | int | 15000 | Maximum character length for a single code file sent to the LLM |
254254
| max_referenced_file_chars | int | 10000 | Maximum character length for a single referenced file sent to the LLM |
255255
| max_total_prompt_chars | int | 100000 | Maximum total characters across the entire LLM prompt |
256-
| meta_budget_multiplier | float | 3.0 | Multiplier applied to all limits above for the meta analyzer (e.g. 3x = 60K instruction, 45K/file, 300K total) |
256+
| max_output_tokens | int | 8192 | Maximum output tokens for LLM responses (both LLM analyzer and meta-analyzer) |
257+
| meta_budget_multiplier | float | 3.0 | Multiplier applied to all input limits above for the meta analyzer (e.g. 3x = 60K instruction, 45K/file, 300K total) |
257258

258259
</details>
259260

docs/user-guide/custom-policy-configuration.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,12 +518,14 @@ llm_analysis:
518518
max_code_file_chars: 15000 # Max chars per individual code file
519519
max_referenced_file_chars: 10000 # Max chars per referenced file
520520
max_total_prompt_chars: 100000 # Total prompt budget across all files
521+
max_output_tokens: 8192 # Max tokens for LLM responses
521522
meta_budget_multiplier: 3.0 # Meta-analyzer multiplies above limits by this factor
522523
```
523524
524525
**Impact:**
525526
- Files or instruction bodies exceeding these limits are skipped entirely (no truncation) and a budget-skip metadata entry is attached to the scan result.
526-
- The meta-analyzer applies `meta_budget_multiplier` on top of the base limits. With the defaults, the meta-analyzer gets 60K instruction, 45K per file, and 300K total.
527+
- `max_output_tokens` controls the output token budget for both the LLM analyzer and meta-analyzer. Raise this if scans produce truncated JSON (`LLM_ANALYSIS_FAILED` findings). The CLI flag `--llm-max-tokens` overrides this value.
528+
- The meta-analyzer applies `meta_budget_multiplier` on top of the base input limits. With the defaults, the meta-analyzer gets 60K instruction, 45K per file, and 300K total.
527529
- Increase these values for skills with large codebases or extensive instructions. Decrease them to reduce LLM API costs.
528530
529531
</details>

skill_scanner/cli/cli.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def _build_analyzers(policy: ScanPolicy, args: argparse.Namespace, status: Calla
118118
use_trigger=getattr(args, "use_trigger", False),
119119
llm_provider=getattr(args, "llm_provider", None),
120120
llm_consensus_runs=getattr(args, "llm_consensus_runs", 1),
121+
llm_max_tokens=getattr(args, "llm_max_tokens", None),
121122
)
122123

123124
# Emit status messages for the optional analyzers that were activated.
@@ -138,7 +139,13 @@ def _build_analyzers(policy: ScanPolicy, args: argparse.Namespace, status: Calla
138139
return analyzers
139140

140141

141-
def _build_meta_analyzer(args: argparse.Namespace, analyzer_count: int, status: Callable[[str], None], policy=None):
142+
def _build_meta_analyzer(
143+
args: argparse.Namespace,
144+
analyzer_count: int,
145+
status: Callable[[str], None],
146+
policy=None,
147+
max_tokens: int | None = None,
148+
):
142149
"""Optionally build a MetaAnalyzer if ``--enable-meta`` is set."""
143150
if not getattr(args, "enable_meta", False):
144151
return None
@@ -157,13 +164,19 @@ def _build_meta_analyzer(args: argparse.Namespace, analyzer_count: int, status:
157164
meta_model = os.getenv("SKILL_SCANNER_META_LLM_MODEL") or os.getenv("SKILL_SCANNER_LLM_MODEL")
158165
meta_base_url = os.getenv("SKILL_SCANNER_META_LLM_BASE_URL") or os.getenv("SKILL_SCANNER_LLM_BASE_URL")
159166
meta_api_version = os.getenv("SKILL_SCANNER_META_LLM_API_VERSION") or os.getenv("SKILL_SCANNER_LLM_API_VERSION")
160-
meta = MetaAnalyzer(
167+
kwargs: dict = dict(
161168
model=meta_model,
162169
api_key=meta_api_key,
163170
base_url=meta_base_url,
164171
api_version=meta_api_version,
165172
policy=policy,
166173
)
174+
effective_max_tokens = (
175+
max_tokens if max_tokens is not None else (policy.llm_analysis.max_output_tokens if policy else None)
176+
)
177+
if effective_max_tokens is not None:
178+
kwargs["max_tokens"] = effective_max_tokens
179+
meta = MetaAnalyzer(**kwargs)
167180
status("Using Meta-Analyzer for false positive filtering and finding prioritization")
168181
return meta
169182
except Exception as e:
@@ -320,7 +333,8 @@ def scan_command(args: argparse.Namespace) -> int:
320333

321334
policy = _load_policy(args)
322335
analyzers = _build_analyzers(policy, args, status)
323-
meta_analyzer = _build_meta_analyzer(args, len(analyzers), status, policy=policy)
336+
llm_max_tokens = getattr(args, "llm_max_tokens", None)
337+
meta_analyzer = _build_meta_analyzer(args, len(analyzers), status, policy=policy, max_tokens=llm_max_tokens)
324338

325339
scanner = SkillScanner(analyzers=analyzers, policy=policy)
326340
lenient = getattr(args, "lenient", False)
@@ -405,7 +419,8 @@ def scan_all_command(args: argparse.Namespace) -> int:
405419

406420
policy = _load_policy(args)
407421
analyzers = _build_analyzers(policy, args, status)
408-
meta_analyzer = _build_meta_analyzer(args, len(analyzers), status, policy=policy)
422+
llm_max_tokens = getattr(args, "llm_max_tokens", None)
423+
meta_analyzer = _build_meta_analyzer(args, len(analyzers), status, policy=policy, max_tokens=llm_max_tokens)
409424

410425
scanner = SkillScanner(analyzers=analyzers, policy=policy)
411426

@@ -718,6 +733,13 @@ def _add_common_scan_flags(parser: argparse.ArgumentParser) -> None:
718733
metavar="N",
719734
help="Run LLM analysis N times and keep only findings with majority agreement (reduces false positives, increases cost)",
720735
)
736+
parser.add_argument(
737+
"--llm-max-tokens",
738+
type=int,
739+
default=None,
740+
metavar="N",
741+
help="Maximum output tokens for LLM responses (default: 8192). Raise if scans produce truncated JSON.",
742+
)
721743
parser.add_argument("--use-trigger", action="store_true", help="Enable trigger specificity analysis")
722744
parser.add_argument("--enable-meta", action="store_true", help="Enable meta-analysis FP filtering (2+ analyzers)")
723745
parser.add_argument(

skill_scanner/cli/policy_tui.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,9 @@ def compose(self) -> ComposeResult:
481481
with Horizontal(classes="field-row"):
482482
yield Label("Max total prompt (chars)")
483483
yield Input(value="100000", id="llm-max-total-prompt", type="integer")
484+
with Horizontal(classes="field-row"):
485+
yield Label("Max output tokens")
486+
yield Input(value="8192", id="llm-max-output-tokens", type="integer")
484487
with Horizontal(classes="field-row"):
485488
yield Label("Meta budget multiplier")
486489
yield Input(value="3.0", id="llm-meta-multiplier")
@@ -578,6 +581,7 @@ def _sync_form_from_policy(self) -> None:
578581
self.query_one("#llm-max-code-file", Input).value = str(p.llm_analysis.max_code_file_chars)
579582
self.query_one("#llm-max-ref-file", Input).value = str(p.llm_analysis.max_referenced_file_chars)
580583
self.query_one("#llm-max-total-prompt", Input).value = str(p.llm_analysis.max_total_prompt_chars)
584+
self.query_one("#llm-max-output-tokens", Input).value = str(p.llm_analysis.max_output_tokens)
581585
self.query_one("#llm-meta-multiplier", Input).value = str(p.llm_analysis.meta_budget_multiplier)
582586

583587
def _sync_policy_from_form(self) -> None:
@@ -697,6 +701,10 @@ def _sync_policy_from_form(self) -> None:
697701
p.llm_analysis.max_total_prompt_chars = int(self.query_one("#llm-max-total-prompt", Input).value)
698702
except ValueError:
699703
pass
704+
try:
705+
p.llm_analysis.max_output_tokens = int(self.query_one("#llm-max-output-tokens", Input).value)
706+
except ValueError:
707+
pass
700708
try:
701709
p.llm_analysis.meta_budget_multiplier = float(self.query_one("#llm-meta-multiplier", Input).value)
702710
except ValueError:

skill_scanner/config/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class Config:
3838
llm_model: str = "claude-3-5-sonnet-20241022"
3939
llm_base_url: str | None = None
4040
llm_api_version: str | None = None
41-
llm_max_tokens: int = 4000
41+
llm_max_tokens: int = 8192
4242
llm_temperature: float = 0.0
4343
llm_rate_limit_delay: float = 2.0
4444
llm_max_retries: int = 3

skill_scanner/core/analyzer_factory.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def build_analyzers(
9090
aidefense_api_url: str | None = None,
9191
use_trigger: bool = False,
9292
llm_consensus_runs: int = 1,
93+
llm_max_tokens: int | None = None,
9394
) -> list[BaseAnalyzer]:
9495
"""Build the full analyzer list (core + optional).
9596
@@ -100,6 +101,11 @@ def build_analyzers(
100101
hook, and eval scripts so that every entry point uses the exact same
101102
construction logic.
102103
104+
Args:
105+
llm_max_tokens: Override the default ``max_tokens`` for the
106+
:class:`LLMAnalyzer`. When *None* the analyzer's own
107+
default (8192) is used.
108+
103109
Returns:
104110
A list of analyzer instances ready to be passed to
105111
:class:`SkillScanner`.
@@ -124,10 +130,18 @@ def build_analyzers(
124130
key = llm_api_key or os.getenv("SKILL_SCANNER_LLM_API_KEY")
125131
base_url = llm_base_url or os.getenv("SKILL_SCANNER_LLM_BASE_URL")
126132
api_version = llm_api_version or os.getenv("SKILL_SCANNER_LLM_API_VERSION")
133+
extra_kwargs: dict = {}
134+
effective_max_tokens = (
135+
llm_max_tokens if llm_max_tokens is not None else policy.llm_analysis.max_output_tokens
136+
)
137+
if effective_max_tokens is not None:
138+
extra_kwargs["max_tokens"] = effective_max_tokens
127139
if llm_provider and not llm_model and not os.getenv("SKILL_SCANNER_LLM_MODEL"):
128-
llm = LLMAnalyzer(provider=llm_provider, policy=policy)
140+
llm = LLMAnalyzer(provider=llm_provider, policy=policy, **extra_kwargs)
129141
else:
130-
llm = LLMAnalyzer(model=model, api_key=key, base_url=base_url, api_version=api_version, policy=policy)
142+
llm = LLMAnalyzer(
143+
model=model, api_key=key, base_url=base_url, api_version=api_version, policy=policy, **extra_kwargs
144+
)
131145
if llm_consensus_runs > 1:
132146
llm.consensus_runs = llm_consensus_runs
133147
analyzers.append(llm)

skill_scanner/core/analyzers/llm_analyzer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def __init__(
116116
self,
117117
model: str | None = None,
118118
api_key: str | None = None,
119-
max_tokens: int = 4000,
119+
max_tokens: int = 8192,
120120
temperature: float = 0.0,
121121
max_retries: int = 3,
122122
rate_limit_delay: float = 2.0,

0 commit comments

Comments
 (0)