LLM
Constants
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.
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).
A fenced block anywhere in the reply, with its language tag.
A markdown fence only ever wraps the whole payload, so both patterns
are anchored. The previous gsub("```json", "").gsub("```", "") ran over
the entire response and was wrong in both directions:
- backticks the model quoted from the code it was reading (inside a
snippetordescriptionstring value) were deleted from the data, silently corrupting it; - only the exact lowercase
jsontag was recognised, so```JSONor```javascriptleft the bare language word in front of the JSON —JSON.parsethen failed and the caller returned "", which the analyzer reads as "this code defines no endpoints".
The tag is matched case-insensitively and generically (any language word), since which one the model picks is not something we control.
How many fenced blocks, and how many {, the prose fallbacks try before
giving up; each { try scans to the end of a reply that never closes it.
Floor on how much of an oversized file goes into one part. A provider
budget small enough to make this bind (--ai-max-token 200) cannot fit a
useful request either way; splitting into 1-line parts would just turn one
doomed call into hundreds of them.
Map of LLM providers and their models to their max token limits This helps determine how many files can be bundled together.
The budget sizes the prompt alone, so where output shares or has its own cap it is the input limit: GPT-5 through 5.3 take 272k of their 400k window, and the o-series leaves half of 200k for reasoning. A budget sized at the window ships bundles the API rejects whole. Claude Sonnet 4/4.5 reach 1M only behind a beta header this client never sends.
A dated snapshot or channel id (claude-opus-4-20250514,
gemini-3-pro-preview) resolves to its base entry; a tier variant
(-mini, -fast) has its own window and needs its own entry.
Ceiling on an Ollama-native budget the user did not set. The budget is
sent as num_ctx, which Ollama allocates up front as KV cache: the
128k-256k table values need ~16 GiB or more for an 8B model, and a load
failure loses the whole scan rather than degrading it.
Suffixes that name a snapshot or release channel of the same model, not a variant with its own context window.
Class methods
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.
Create a bundle of files that fits within token limits
Returns one Bundle per request to send
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.
The token budget a run sizes its prompts for — and, for Ollama, the context it asks the server to allocate. An explicit --ai-max-token wins.
Estimate the number of tokens in a string This is a rough estimate using 1 token ≈ 4 characters for English text
Get the maximum token limit for a given provider and model
The JSON object in an LLM reply that carries key, or nil.
A model wraps its JSON in prose ("Here are the endpoints: ...", "Note
/users/{id} too"), puts a code block or a {"thought": ...} before it,
or answers with some other object ({"error": "context length exceeded"}). Only an object holding the expected key is the answer, so
the candidates are tried in turn: the reply, its fenced blocks (```json
ones first), then each balanced top-level {...}. An empty {} reply is
returned as-is — the model saying "nothing here" in the asked-for shape.
The model a request names, stripped of how it was addressed: a router
vendor (anthropic/…), a registry path, an Ollama tag (:8b), a
fine-tune wrapper (ft:…:org::id), a Bedrock region prefix
(us.anthropic.…) and case. Claude's dotted router spelling
(claude-opus-4.7) is folded onto the dashed API id.
The budget to re-split an overflowing bundle at, in noir's estimated tokens. When the provider named both the window and the prompt's real size, their ratio corrects the chars/4 estimate in one step (dense or CJK source runs far over it); otherwise the named window, or half the bundle. Always below the bundle's own size, so each re-split shrinks it.
The complete objects at the head of a "key": [ list that never closes,
or nil: what is left of a reply cut off by the output limit. Stops at
the first item that is not a whole object, so nothing past the cut (or
past the list's end) is taken.
The {label, text} of a bundle that is one part of a split file (the
section split_file_sections wrote), or nil for any other bundle.
Strip the markdown json / code fences that LLM providers sometimes
wrap JSON responses in, and trim surrounding whitespace. Shared by every
provider client so the cleanup rule has a single home. Deliberately no
more than that: picking the JSON out of prose needs to know which key the
caller expects (json_reply), and a client that guessed first would
hand it the wrong object.
The MODEL_TOKEN_LIMITS lookup behind get_max_tokens, split out because
the analyzer specs monkeypatch that method.
A URL names its provider by host only (the path is the gateway's
choice). A custom or gateway URL names no provider at all, but the model
usually still does: gpt-5.5 behind a corporate gateway has the same
window as at OpenAI, so the whole table is searched before falling back
to the 4000-token global default.
Why a reply that did not end normally is unusable, or nil. reason is
OpenAI's finish_reason, Ollama's done_reason or Anthropic's
stop_reason (length / max_tokens for a reply cut off by the output
limit). An empty reply is otherwise silent: the caller would read it as
"no endpoints".
Wraps repo text in a tag pair the text cannot close. The tag comes from a hash of the text itself, so writing the closing tag into a file changes the tag, while the same file still yields the same prompt (and cache entry) on every run — a random nonce would make every request a cache miss.