module LLM

Defined in:

llm/acp/client.cr
llm/adapter.cr
llm/cache.cr
llm/general/client.cr
llm/native_tool_calling.cr
llm/ollama/ollama.cr
llm/prompt.cr

Constant Summary

ACP_DEFAULT_MAX_TOKENS = 128000

Bundling budget for ACP agent providers ("acp:gemini", "acp:codex", "acp:claude", …). These proxy to large-context frontier models whose exact id we don't control, so they aren't keys in MODEL_TOKEN_LIMITS. Falling through to the generic 4000- token global default would shard a project into many tiny bundles and break the cross-file context the bundle analyzer relies on to resolve route prefixes (a false-negative source). 128k is a safe lower bound across the current ACP agents.

AGENT_PROMPT = "You are OWASP Noir's endpoint discovery agent.\nYour goal is to discover all API endpoints from source code with high confidence.\n\nAllowed actions:\n- list_directory(path: string = \".\", max_depth: integer = 3)\n- read_file(path: string)\n- grep(pattern: string, path: string = \".\", file_pattern: string = \"*\")\n- semantic_search(query: string)\n- finalize(endpoints: array, summary: string, confidence: integer 0..100)\n\nRules:\n- First inspect project structure with list_directory or grep.\n- Never assume endpoint existence from filenames only.\n- Read source files before finalizing.\n- Resolve the FULL request path: follow route prefixes, router/blueprint groups, controller base paths, and mount points back to their declaration before recording a URL.\n- Emit a separate endpoint for each HTTP method a route handles, and capture query/json/form/header/cookie/path parameters.\n- Include only endpoints the code actually serves; ignore URLs found only in comments, docs, tests, or outbound third-party calls. Do not fabricate endpoints.\n- Use finalize only when confident enough.\n- Keep endpoint method to [GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD].\n- Use param_type in [query, json, form, header, cookie, path]."
AGENT_SHARED_RULES = "Output only JSON. No markdown fences. Use only the defined action names. Do not guess endpoints without reading code."
AGENT_STEP_FORMAT = "{\n \"type\": \"json_schema\",\n \"json_schema\": {\n \"name\": \"agent_next_action\",\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"action\": {\n \"type\": \"string\",\n \"enum\": [\"list_directory\", \"read_file\", \"grep\", \"semantic_search\", \"finalize\"]\n },\n \"args\": {\n \"type\": \"object\",\n \"additionalProperties\": true\n }\n },\n \"required\": [\"action\", \"args\"],\n \"additionalProperties\": false\n },\n \"strict\": true\n }\n}"
AGENT_TOOLS = "[\n {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"list_directory\",\n \"description\": \"Return project directory structure up to max_depth.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"path\": {\n \"type\": \"string\",\n \"default\": \".\"\n },\n \"max_depth\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 6,\n \"default\": 3\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"read_file\",\n \"description\": \"Read file content for endpoint extraction.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"path\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\"path\"],\n \"additionalProperties\": false\n }\n }\n },\n {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"grep\",\n \"description\": \"Regex search in codebase.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"pattern\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\",\n \"default\": \".\"\n },\n \"file_pattern\": {\n \"type\": \"string\",\n \"default\": \"*\"\n }\n },\n \"required\": [\"pattern\"],\n \"additionalProperties\": false\n }\n }\n },\n {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"semantic_search\",\n \"description\": \"Semantic-like search for code using natural language query.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"query\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\"query\"],\n \"additionalProperties\": false\n }\n }\n },\n {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"finalize\",\n \"description\": \"Finalize extraction and return endpoint list.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"endpoints\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"url\": { \"type\": \"string\" },\n \"path\": { \"type\": \"string\" },\n \"method\": { \"type\": \"string\" },\n \"file\": { \"type\": \"string\" },\n \"line\": { \"type\": \"integer\" },\n \"params\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"param_type\": { \"type\": \"string\" },\n \"value\": { \"type\": \"string\" }\n },\n \"required\": [\"name\"],\n \"additionalProperties\": true\n }\n }\n },\n \"required\": [\"method\"],\n \"additionalProperties\": true\n }\n },\n \"summary\": { \"type\": \"string\" },\n \"confidence\": { \"type\": \"integer\" }\n },\n \"required\": [\"endpoints\"],\n \"additionalProperties\": true\n }\n }\n }\n]"
ANALYZE_FORMAT = "{\n \"type\": \"json_schema\",\n \"json_schema\": {\n \"name\": \"analyze_endpoints\",\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"endpoints\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"url\": {\n \"type\": \"string\"\n },\n \"method\": {\n \"type\": \"string\"\n },\n \"params\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"param_type\": {\n \"type\": \"string\"\n },\n \"value\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\"name\", \"param_type\", \"value\"],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\"url\", \"method\", \"params\"],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\"endpoints\"],\n \"additionalProperties\": false\n },\n \"strict\": true\n }\n}"
ANALYZE_PROMPT = "Analyze the provided source code to extract details about the endpoints and their parameters.\n\n#{ENDPOINT_GUIDANCE}\n\nOutput format:\n- The \"method\" field should strictly use one of these values: \"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"OPTIONS\", \"HEAD\".\n- The \"param_type\" must strictly use one of these values: \"query\", \"json\", \"form\", \"header\", \"cookie\", \"path\".\n- Do not include any explanations, comments, or additional text.\n- Output only the JSON result.\n- Return the result strictly in valid JSON format according to the schema provided below.\n\nInput Code:"
BUNDLE_ANALYZE_PROMPT = "Analyze the following bundle of source code files to extract details about the endpoints and their parameters.\n\n#{ENDPOINT_GUIDANCE}\n- A route prefix may be declared in one file and consumed in another within the same bundle; cross-reference files to resolve the full path.\n\nOutput format:\n- The \"method\" field should strictly use one of these values: \"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"OPTIONS\", \"HEAD\".\n- The \"param_type\" must strictly use one of these values: \"query\", \"json\", \"form\", \"header\", \"cookie\", \"path\".\n- Include endpoints from ALL files in the bundle.\n- Do not include any explanations, comments, or additional text.\n- Output only the JSON result.\n- Return the result strictly in valid JSON format according to the schema provided below.\n\nBundle of files:"
ENDPOINT_GUIDANCE = "Accuracy rules:\n- Report the FULL request path a client calls. Resolve and prepend every route prefix, router/blueprint group, controller base path, or mount point (e.g. group \"/api/v1\" + route \"/users\" => \"/api/v1/users\").\n- Emit a SEPARATE endpoint object for each HTTP method a route handles; never merge methods.\n- Capture every parameter with the correct param_type: query, json (request body), form, header, cookie, or path (a URL placeholder such as {id} or :id).\n- Include only endpoints THIS code defines or serves. Ignore URLs that appear only in comments, documentation, string examples, tests, or outbound calls to third-party services.\n- Do not invent, guess, or pad endpoints. If the code defines none, return an empty endpoints list."

Shared accuracy guidance injected into the endpoint-extraction prompts. Centralised so the classic, bundle, and agentic paths all apply the same FN/FP rules:

  • Full-path resolution kills the biggest false-negative source (sub-routes whose prefix lives in a parent router/controller).
  • One-object-per-method stops verb collapsing.
  • The "only code that actually serves it" rule kills the biggest false-positive source (example/comment/test/outbound URLs).
FILTER_FORMAT = "{\n \"type\": \"json_schema\",\n \"json_schema\": {\n \"name\": \"filter_files\",\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"files\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\"files\"],\n \"additionalProperties\": false\n },\n \"strict\": true\n }\n}"
FILTER_PROMPT = "Analyze the following list of file paths and identify which files are likely to define or serve endpoints, including API endpoints, web pages, route/controller definitions, or static resources.\n\nGuidelines:\n- Focus only on individual files.\n- Do not include directories.\n- Favor recall over precision: when a file might define routes, controllers, handlers, or served content, include it. Missing a route-bearing file loses endpoints permanently.\n- Exclude only files that clearly cannot serve endpoints (lock files, build artifacts, images, binaries).\n- Do not include any explanations, comments, or additional text.\n- Output only the JSON result.\n- Return the result strictly in valid JSON format according to the schema provided below.\n\nInput Files:"
MODEL_TOKEN_LIMITS = {"openai" => {"gpt-3.5-turbo" => 16385, "gpt-3.5-turbo-16k" => 16385, "gpt-4" => 8192, "gpt-4-32k" => 32768, "gpt-4-turbo" => 128000, "gpt-4-turbo-preview" => 128000, "gpt-4-vision-preview" => 128000, "gpt-4-1106-preview" => 128000, "gpt-4-0125-preview" => 128000, "gpt-4o" => 128000, "gpt-4o-mini" => 128000, "o1-preview" => 128000, "o1-mini" => 128000, "o3-mini" => 200000, "gpt-5" => 1000000, "gpt-5-pro" => 1000000, "gpt-5-mini" => 1000000, "gpt-5-nano" => 1000000, "gpt-5-codex" => 1000000, "gpt-5.1" => 1000000, "gpt-5.1-codex" => 1000000, "gpt-5.1-codex-max" => 1000000, "gpt-5.1-codex-mini" => 1000000, "gpt-5.2-codex" => 1000000, "gpt-5.3-codex" => 1000000, "gpt-5.4" => 1000000, "gpt-5.4-pro" => 1000000, "gpt-5.4-mini" => 1000000, "gpt-5.4-nano" => 1000000, "gpt-5.5" => 1000000, "gpt-5.5-pro" => 1000000, "gpt-5.5-mini" => 1000000, "gpt-5.5-nano" => 1000000, "default" => 8000}, "xai" => {"grok-1" => 8192, "grok-2" => 131072, "grok-2-mini" => 131072, "grok-beta" => 131072, "grok-3" => 1000000, "grok-4" => 2000000, "grok-4-fast-reasoning" => 2000000, "grok-4-fast-non-reasoning" => 2000000, "grok-4.1-fast" => 2000000, "grok-4.20" => 2000000, "grok-4.3" => 2000000, "grok-code-fast" => 2000000, "grok-code-fast-1" => 2000000, "grok-build-0.1" => 256000, "default" => 8000}, "anthropic" => {"claude-3-opus" => 200000, "claude-3-sonnet" => 200000, "claude-3-haiku" => 200000, "claude-3-5-sonnet" => 200000, "claude-3-5-haiku" => 200000, "claude-2" => 100000, "claude-2.0" => 100000, "claude-2.1" => 200000, "claude-instant-1.2" => 100000, "claude-sonnet-4" => 1000000, "claude-sonnet-4-5" => 1000000, "claude-sonnet-4-6" => 1000000, "claude-haiku-4-5" => 200000, "claude-opus-4" => 200000, "claude-opus-4-1" => 200000, "claude-opus-4.1" => 200000, "claude-opus-4-5" => 200000, "claude-opus-4-6" => 200000, "claude-opus-4-7" => 200000, "claude-opus-4-8" => 1000000, "claude-fable-5" => 1000000, "claude-mythos-5" => 1000000, "default" => 100000}, "azure" => {"gpt-3.5-turbo" => 16385, "gpt-3.5-turbo-16k" => 16385, "gpt-4" => 8192, "gpt-4-32k" => 32768, "gpt-4-turbo" => 128000, "gpt-4-turbo-preview" => 128000, "gpt-4-vision-preview" => 128000, "gpt-4o" => 128000, "gpt-4o-mini" => 128000, "o1-preview" => 128000, "o1-mini" => 128000, "gpt-4.1" => 1000000, "gpt-5" => 1000000, "gpt-5.1-codex-max" => 1000000, "gpt-5.4" => 1000000, "gpt-5.4-mini" => 1000000, "gpt-5.5" => 1000000, "gpt-5.5-pro" => 1000000, "gpt-5.5-mini" => 1000000, "default" => 8000}, "github" => {"gpt-4o" => 64000, "gpt-4o-mini" => 64000, "Phi-3.5-mini-instruct" => 128000, "Phi-3.5-MoE-instruct" => 128000, "Meta-Llama-3.1-405B-Instruct" => 128000, "Meta-Llama-3.1-70B-Instruct" => 128000, "Meta-Llama-3.1-8B-Instruct" => 128000, "Mistral-large" => 128000, "Mistral-large-2407" => 128000, "Mistral-Nemo" => 128000, "Mistral-small" => 32768, "AI21-Jamba-1.5-Large" => 256000, "AI21-Jamba-1.5-Mini" => 256000, "Cohere-command-r" => 128000, "Cohere-command-r-plus" => 128000, "default" => 8000}, "ollama" => {"llama3" => 128000, "llama3.1" => 128000, "llama3.2" => 128000, "llama3.3" => 128000, "llama4" => 512000, "llama4-maverick" => 256000, "phi2" => 2048, "phi3" => 128000, "phi3.5" => 128000, "phi4" => 16384, "phi4-mini" => 128000, "gemma" => 8192, "gemma2" => 8192, "gemma3" => 128000, "gemma4" => 128000, "mistral" => 32768, "mixtral" => 32768, "codellama" => 100000, "deepseek-coder" => 100000, "qwen2" => 128000, "qwen2.5" => 128000, "qwen3" => 256000, "qwen3-coder" => 256000, "deepseek-v3" => 128000, "deepseek-v3.1" => 128000, "deepseek-v3.2" => 128000, "deepseek-v4-pro" => 1000000, "deepseek-v4-flash" => 1000000, "deepseek-r1" => 128000, "gpt-oss" => 128000, "gpt-oss-120b" => 128000, "gpt-oss-20b" => 128000, "default" => 4000}, "google" => {"gemini-1.5-pro" => 2097152, "gemini-1.5-flash" => 1048576, "gemini-1.0-pro" => 32760, "gemini-pro" => 32760, "gemini-pro-vision" => 16384, "gemini-2.0-flash-exp" => 1048576, "gemini-2.5-pro" => 2000000, "gemini-2.5-flash" => 1048576, "gemini-2.5-flash-lite" => 1048576, "gemini-3-pro-preview" => 2000000, "gemini-3.1-pro-preview" => 2000000, "gemini-3-flash-preview" => 1048576, "gemini-3.1-flash-preview" => 1048576, "gemini-3.1-flash-lite-preview" => 1048576, "gemini-3.5-flash" => 1048576, "default" => 32760}, "cohere" => {"command-r" => 256000, "command-r-plus" => 256000, "command" => 4096, "command-light" => 4096, "command-nightly" => 8192, "command-light-nightly" => 8192, "default" => 4096}, "vllm" => {"default" => 4000}, "lmstudio" => {"gpt-oss" => 128000, "gpt-oss-120b" => 128000, "gpt-oss-20b" => 128000, "default" => 4000}, "openrouter" => {"default" => 128000}, "default" => 4000}

Map of LLM providers and their models to their max token limits This helps determine how many files can be bundled together

SHARED_RULES = "Output only JSON. No explanations. method in [GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD]. param_type in [query, json, form, header, cookie, path]."
SYSTEM_AGENT = "#{AGENT_SHARED_RULES} You are OWASP Noir Advanced Endpoint Discovery Agent. Use iterative tool actions until enough evidence is collected, then finalize."
SYSTEM_ANALYZE = "#{SHARED_RULES} Given source code, return JSON with endpoints: [{url, method, params:[{name, param_type, value}]}]. Report full request paths (resolve route prefixes), one object per HTTP method, and never fabricate endpoints."
SYSTEM_BUNDLE = "#{SHARED_RULES} Given a bundle of files, include endpoints from ALL files; return the same JSON schema. Report full request paths (resolve route prefixes), one object per HTTP method, and never fabricate endpoints."
SYSTEM_FILTER = "#{SHARED_RULES} Given a list of file paths, return JSON with property files: string[] of files that may define or serve endpoints (no directories). Favor recall: when unsure, include the file."

Class Method Summary

Class Method Detail

def self.acp_max_tokens?(provider : String) : Int32 | Nil #

Returns the ACP bundling budget for an acp:<agent> provider, or nil for non-ACP providers. Kept as a standalone pure function so it stays testable even though .get_max_tokens is monkeypatched in the analyzer specs.


[View source]
def self.bundle_files(files : Array(Tuple(String, String)), max_tokens : Int32, safety_margin : Float64 = 0.8) : Array(Tuple(String, Int32)) #

Create a bundle of files that fits within token limits Returns the bundle content and the estimated token count


[View source]
def self.clean_token?(value : String, max_length : Int32) : Bool #

Shared shape check for LLM-supplied endpoint URLs and parameter names. A served path or an identifier never carries raw whitespace, control characters, or markdown backticks, and stays within a sane length bound — their presence means the model captured prose or a code fragment instead. Both the AI analyzer (identification) and the LLM optimizer (correction) call this so a hallucinated value can't ride through either phase. Token-specific rules (placeholder words for URLs) stay at the call site.


[View source]
def self.estimate_tokens(text : String) : Int32 #

Estimate the number of tokens in a string This is a rough estimate using 1 token ≈ 4 characters for English text


[View source]
def self.get_max_tokens(provider : String, model : String) : Int32 #

Get the maximum token limit for a given provider and model


[View source]