Skip to content

AI Models

Every model call the platform makes — Cortex chat, log analysis, the Warden judge, image transcription (vision) and code fixes — goes to one model, deepseek-v4.1-flash, on NeuralWatt. It is pinned by its own id, never by the provider’s tracking alias nw-flash: an alias moves to a newer model with no deploy here, and the answers move with it.

ServiceWhere the id is setDefault
ai-servicesrc/config/env.ts PRIMARY_MODEL = LLM_MODEL, else legacy GROQ_MODELdeepseek-v4.1-flash
ai-service visionsrc/config/values.attachments.ts llmVisionModel = LLM_VISION_MODELPRIMARY_MODEL. Set to "" to make the deployment decline images
ai-service cheap / strong tierssrc/config/values.models.ts LLM_CHEAP_MODEL, LLM_STRONG_MODELPRIMARY_MODEL — cost is split by lane, not by model
warden (ai-service repo, Dockerfile.warden)src/warden/config.ts WARDEN_JUDGE_MODELthe ai-service LLM_MODEL
analyzersrc/core/providers/NeuralWattProvider.ts DEFAULT_MODEL; LLM_MODEL, else NEURALWATT_MODELdeepseek-v4.1-flash
analyzer Warden judgesrc/warden/config.ts WARDEN_JUDGE_MODELthe analyzer LLM_MODEL
backendByoLlmService byo.platform.model (what Settings shows as “Logrite (built-in)”)deepseek-v4.1-flash

The measurement behind the choice is in the comment on PRIMARY_MODEL (2026-09-24, 5 models × 14 real transcripts × 2 reps on the production findings prompt): deepseek-v4.1-flash 100.0 % correct, 92.9 % run-to-run consistent, 0 false positives; the previous default deepseek-v4-flash 92.9 % / 92.9 %.

Both ai-service (src/core/registry/modelResolver.ts) and the analyzer resolve a role’s model in this order, highest first:

  1. a workspace’s own BYO-LLM credential (ai-service: the request’s provider bundle wins before the registry is consulted);
  2. an env var for the role — LLM_MODEL / GROQ_MODEL (chat), LLM_CHEAP_MODEL, LLM_STRONG_MODEL, WARDEN_JUDGE_MODEL (judge), LLM_ANALYSIS_MODEL, LLM_CODE_FIX_MODEL. Any of these being set, even to the value the registry would give, means the registry gets no vote for that role;
  3. the backend model registry’s rank-0 row for the role (ai_model_role);
  4. the compiled-in default above.

The analyzer’s analysis role is baseline-gated: a registry row that would move the analysis model while it has requires_baseline set is refused (logged REFUSED analysis model change) unless ALLOW_UNVERIFIED_ANALYSIS_MODEL=true, because the 28-language quality harness pins its digests to the model that produced them.

Every estate (prod and dev ai-service, the analyzers, and the demo seeders’ relay provider) uses the same NeuralWatt key (identical sha256 prefix), and that key has 5 concurrent slots. A burst anywhere starves everything: on 2026-10-14 the dev analyzer’s agent-scan judge made ~1,100 calls in 10 minutes and prod Cortex answered “The AI step is unavailable right now” (429, concurrent_budget_exceeded: 5/5 slots in use). See Analyzer → the judge flood. The provider’s x-budget-remaining-usd header read 0.54 that day: check the balance before a demo.

The analyzer now holds itself to 3 of the 5 (analyzer 9ee4eda). providers/providerSlots.ts caps generateCompletion at LLM_MAX_CONCURRENT (default 3, 0 = no cap) per process, serves an interactive call (a custom-rule preview) before queued background work, and treats a 429 as busy, not down: it retries with backoff (Retry-After honoured, 15 s cap) for 20 s (interactive) or 90 s (background), then throws ProviderBusyError. /rules/propose answers that as 503 model_busy with Retry-After: 60, and the custom-rule preview says “the assistant is busy (the AI model is at capacity). Try again in a minute.” instead of “unreachable” (backend CustomRuleService.providerBusy). The warden judge’s chatCompletion path is not capped — a queue in front of a live request’s decision would be worse than a fast refusal.

The flex lane is the same model and weights on NeuralWatt’s discounted best-effort lane, sent as service_tier:"flex". It may queue for capacity; it never changes what writes the answer. Omitting service_tier is the standard lane.

Nothing a request waits for runs on flex. Measured 2026-10-07 from inside the ai-service container on both estates, an 8-token reply took 55–65 s on flex against 3–6 s on standard. Three awaited calls had been filed as background work and were moved off flex in ai-service 3302f72: the rolling conversation summary (the chat turn awaits it once a conversation passes the verbatim window), the hand-off brief (“continue in a new conversation” awaits it) and the upload summary prose (the sync upload response awaits it inside a 12 s budget, so on flex every upload waited 12 s and kept its original text).

Interactive chat never falls back to flex (2026-10-09). For the chat call class the flex rung of ResilientProvider is skipped: a failed standard call throws, and Cortex answers from the facts it already holds with answered:false, unanswered_reason:"provider_unavailable" (see AI service → Cortex close-out). Before, a chat turn could wait ~69 s on flex. Background work keeps the flex lane; the out-of-band Warden judge has its own 120 s budget (WARDEN_JUDGE_ASYNC_TIMEOUT_MS).

Request body fields (ai-service src/core/providers/wireOptions.ts, used by OpenAICompatibleProvider.baseBody):

FieldValue
max_tokensmaxTokens, else 4096
temperaturethe call class’s value, else 0
top_p1
reasoning_effortthe call class’s level
service_tier"flex" only on the flex lane
response_format{"type":"json_schema","json_schema":{"name":…,"schema":…,"strict":true}} when the call passes a schema
seedstill sent when the caller passes one (LLM_SEED, default 42). NeuralWatt documents no seed; nothing relies on it

The analyzer builds the same shape in src/core/providers/neuralWattBody.ts: temperature 0, top_p 1, seed 7, reasoning_effort defaults to low, service_tier:"flex" only when asked, strict json_schema when a schema is passed (else the older json_object hint), max_tokens default 8000.

CALL_POLICY in ai-service src/config/values.models.ts has one row per class of model call. Every call site passes its row instead of literal options. The model is the same for every row.

ClassLanereasoning_effortTemperatureCall sites
chatstandardlow0Agent loop and graph final pass (core/AgentLoop.ts, core/agent/graph.ts), chatEngine/runV1.ts, sync.ts, v2/run.ts, agentRun.ts, emit.ts (synthesis, re-address), activity.ts, v2/uploadAnswer.ts, synthesize/synthesize.ts, fuseTurn.ts, verify/critic/llmCritic.ts, verify/repair.ts, routes/agentBuilder.routes.ts, rolling and hand-off summaries (core/context/rollingSummary.ts, handoffSummary.ts)
routestandardnone0intent/llmBackstop.ts, synthesize/openEndedPlanner.ts, safety/guardrail.ts, utils/dynamicFollowUps.ts
smalltalkstandardnone0.5chatEngine/conversational.ts
judgeSyncstandardlow0Warden inline judge (warden/judge.ts, the default class)
judgeAsyncflexlow0Warden out-of-band judge queue (warden/asyncJudgeQueue.ts)
analysisAsyncflexhigh0Log-analysis findings when no caller is waiting (services/logAnalysis/llmFindings.ts, background = no deadline), compliance rule synthesis (compliance-streams/synthesize/ruleSynthesizer.ts)
analysisSyncstandardhigh0Log-analysis findings when a caller holds the connection open (llmFindings.ts), upload summary prose (execSummary.prose.ts)
visionstandardlow0Image transcription (core/attachments/visionExtract.ts)
codeFixstandardhigh0routes/codeFixes/runCodeFix.ts

In the analyzer, the coding-issue write-up (src/api/routes/describeCodingIssue.ts) runs on the flex lane with reasoning_effort: "low"; the Warden judge runs low, temperature 0, standard lane first. KeyFindings generation (KeyFindingsGenerator, PostGenerationKeyFindingsGenerator, ComplianceKeyFindingsGenerator) sends temperature: 0.

callOptions(cls, answerTokens?) returns the class’s row plus maxTokens = answerTokens + REASONING_TOKEN_ALLOWANCE[reasoning_effort]. Omit answerTokens to take the provider default ceiling. A call site states the answer it needs; the allowance covers the thinking before it:

reasoning_effortAllowance (tokens)
none0
low768
high2048
xhigh4096
max8192
  • PROMPT_VERSION (src/config/values.models.ts, currently 2026-10-07.1) is part of every Cortex answer-cache key. Bump it whenever a prompt changes what an answer can say.
  • ANALYSIS_VERSION (src/config/values.logAnalysis.ts, currently 2026-10-07.1) is part of the upload-reuse key: an identical re-upload is answered from the stored analysis only when that analysis was produced by this version. The reuse key does not contain the model id.

There is no fallback model anywhere. ai-service AIProviderFactory.buildResilient(model) builds [model on the standard lane, the same model on the flex lane] — only for AI_PROVIDER=neuralwatt; any other provider runs with no chain. Usage is metered once per logical call, outside the chain.

ResilientProvider (src/core/providers/ResilientProvider.ts):

  1. calls the standard lane;
  2. on a rate-limit error (429, rate limit, too many requests, over capacity, overloaded, resource_exhausted, quota) waits a jittered 250–600 ms and retries the same lane once (non-streaming calls only);
  3. on a transient error (429/500/502/503/504, timeouts, connection resets, unavailable…) sends the call to the same model on the flex lane (FlexLaneProvider), which notes tier:"flex" on the turn. A streaming call falls back only if nothing was streamed yet.

Vision uses the same two rungs (AIProviderFactory.getVisionProviders()); a BYO workspace gets one rung, its own model, and only if that model can see. The ai-service Warden inline judge runs standard then flex; the out-of-band judge runs flex only. The analyzer’s judge walks JUDGE_TIERS = ['standard', 'flex'] on the same model and retries a 429 after 1 s, 2 s, 4 s.

Deleted env keys — no longer read by any service; remove them from overlays: LLM_FALLBACK_MODEL, LLM_FALLBACK_MODELS, LLM_VISION_FALLBACK_MODELS, WARDEN_JUDGE_LADDER, WARDEN_JUDGE_FALLBACK_MODEL. The analyzer’s Warden judge builds a NeuralWattProvider only; it has no Groq path and does not read GROQ_MODEL (the analyzer’s AI_PROVIDER=groq option for analysis still does).

Interactive paths that already hold measured facts answer with those facts followed by AI_UNAVAILABLE_NOTE (src/core/consistency/aiUnavailable.ts, re-exported from core/chatEngine/emit.ts):

The AI step is unavailable right now; these are the measured facts.

Used by the open-ended synthesis (model: "deterministic-facts"), the upload answer (v2/uploadAnswer.ts, model: "upload-findings") and the activity answer (chatEngine/activity.ts). The upload-answer cache never keeps an answer that carries this note, so the next asking tries the model again.

Vision and the live model catalogue (2026-10-07)

Section titled “Vision and the live model catalogue (2026-10-07)”

Vision is the same model (LLM_VISION_MODEL defaults to PRIMARY_MODEL). Probing showed a non-vision model given an image does not fail — one returned five invented lines — so a cross-model vision ladder could replace a refusal with fabrication.

src/core/providers/modelCatalogue.ts (ai-service and warden) loads the provider’s catalogue at boot and every 6 h:

GET {NEURALWATT_BASE_URL}/models # default base https://api.neuralwatt.com/v1, so …/v1/models
Authorization: Bearer $NEURALWATT_API_KEY
  • Only an answer with scope:"customer" counts. Without a valid key the endpoint still answers 200 with scope:"public"; that is logged MODEL_CATALOGUE_UNAUTHENTICATED and the last good copy is kept. Any other failure also keeps the last good copy. No copy at all = no opinion.
  • Per model it reads metadata.capabilities.vision, metadata.limits.max_images / max_images_total and metadata.reasoning.supported_efforts.
  • modelSupportsVision asks the catalogue first, then falls back to the configured LLM_VISION_MODEL or a known vision-family id pattern.
  • Images attached to a chat turn are clamped to the answering model’s max_images per turn (chatEngine/attachments.ts); the transcripts of every image are still in the context.
  • A configured id (chat, cheap, strong, vision, WARDEN_JUDGE_MODEL) missing from the customer-scope catalogue logs MODEL_NOT_IN_CATALOGUE model=… and turns /health degraded:
{"status":"degraded","reasons":["model deepseek-v4.1-flash not in provider catalogue"], …}

ai-service /health stays HTTP 200 when degraded (503 only when the provider is not configured) and reports ai_provider.catalogue_loaded_at. The warden relay’s /health says ok or degraded with the same reasons. The analyzer runs the same check (src/core/providers/modelCatalogue.ts) over its analysis model and its judge model; its /health turns degraded with reasons and carries modelCatalogue (loaded, models, loadedAt, lastError).

The provider documents no seed, so repeatable answers come from facts and caches, not sampling. src/core/consistency/cortexCache.ts stores the first verifier-clean answer under a key made of everything that can change it, and replays it byte for byte.

Store. Redis via src/shared/redis.ts, reading REDIS_URL, else WARDEN_REDIS_URL (e.g. redis://host:6379/1 — REDIS_URL wins so ai-service and the relay can share one server on different databases). Keys cortex:<namespace>:<sha256>, TTL 24 h. Redis is never on the correctness path: down or unconfigured, a per-process Map (500 entries) answers instead and the turn reports cache:"local".

NamespaceVariableWhat it cachesSets message_end.metadata.cache
agentagentAnswerCache (chatEngine/v2/agentAnswerCache.ts)agent-path answers — see eligibility belowyes
uploaduploadAnswerCache (chatEngine/v2/uploadAnswer.ts)answers about an uploaded file, keyed on the question, the analysis, its facts_digest and the model. An “AI unavailable” fallback is never keptyes
logfindingsfindingsCache (services/logAnalysis/modelPassCall.ts)the log-analysis model pass’s findingsno
open-endedanswerCache (synthesize/answerCache.ts)open-ended answers. Stored only when verification is ok; follow-ups bypass it entirelyyes
deterministicdetCache (intent/dataQueries/answerCache.ts)no-LLM answers (performance, activity, workspace performance)yes
planplanCache (synthesize/openEndedPlanner.ts)the planner’s primitives and window for a questiononly local
routerouteCache (intent/llmBackstop.ts)the LLM backstop’s intent routeonly local

agent, upload and logfindings were added on 2026-10-07; together they make the agent path, upload questions and the log-analysis model pass repeat their first verified result.

Which agent-path answers are stored. Only an answer that is all of:

  • the first turn of a conversation;
  • sent with no attachments;
  • produced with every tool call succeeding;
  • verifier ok, or verification off.

A turn that ran no tool is keyed without the data version, so a data-free answer survives new logs arriving. The knowledge-pack pages injected into the turn are part of the key. A turn that did read data is keyed on the data version of the pinned index, else of the caller workspace’s application-log and trace families (log-analytics, log-observability, log-cloud-platform, trace-analytics-raw — the set the empty-workspace guard reads). Until ai-service bfcf6b0 a Cortex or API-key turn, whose clientId is empty, resolved no patterns at all: every unpinned answer was keyed on none (data_version 140bedbf9c3f = sha256 of none, seen on both estates) and replayed for the rest of the day while new logs arrived.

Key (cortexCacheKey, sha256 over canonical JSON):

  • the canonical question — lower-cased, punctuation and whitespace collapsed;
  • workspace, scope (with |pin: and the pinned index when one is pinned), analysis id;
  • the relative window expression (last_24_hours), never the resolved from/to; a calendar expression (today, this week…) also carries its anchor date in the time zone (today@2026-10-06);
  • timeZone (default UTC);
  • the model id (deterministic-<intent> for the deterministic cache) and PROMPT_VERSION;
  • dataVersion(...).key (src/core/consistency/dataVersion.ts): count and max(@timestamp) per index pattern read, cached 30 s. An unreadable pattern is count: null, never 0, so an outage cannot cache “you have no logs”. The plan and route caches pass an empty data version — they depend on the question alone;
  • extras for the open-ended cache: verify mode, role, experiment key, and a sha256 of any attachment context.

message_end.metadata on every chat turn (messageEndMeta, src/core/consistency/turnMeta.ts):

FieldMeaning
handlerthe branch that answered (falls back to the model label)
modelthe model label the path reported
tierstandard, or flex when the flex lane answered
cachehit, miss or local
facts_digestsha256 of the canonical facts the answer was grounded on, or null
data_versionagent path only: the digest of the workspace data the answer was keyed on

The model sometimes answers in DeepSeek’s own tool-call markup (DSML) instead of the standard tool-call fields. When tools were offered, that markup is parsed into real tool calls; otherwise it is stripped, and the stream filter never shows it to the reader. A turn that offers no tools (the agent loop’s final pass) ends its prompt with a fixed note saying tools are unavailable, so the model answers in prose.

MasterControl ops/testing/cortex_consistency.py asks Cortex the same questions twice against a deployed estate and checks the answers carry the same facts. Stdlib only.

Terminal window
python3 ops/testing/cortex_consistency.py --estate dev [--repeat 2] [--json]
python3 ops/testing/cortex_consistency.py --estate prod --dry-run
FlagDefault
--estaterequireddev (workspace 3103, key from $LOGRITE_DEV_API_KEY) or prod (workspace 252, key from /home/deployd/.logrite-prod-api-key)
--repeat2asks per question; below 2 is refused
--time-zoneAsia/Kolkatachat timeZone
--cache-dir$XDG_CACHE_HOME/logrite or ~/.cache/logritewhere Hadoop_2k.log is kept
--jsonoffmachine-readable report
--dry-runoffprint the plan, no network

Reuse check (setup). It downloads Loghub Hadoop_2k.log (refused unless its sha256 matches) and uploads it twice to POST {ai}/api/v1/log-analytics/{workspace}?shape=batch. The second upload must answer status:"reused" with the same analysisId, datasetId and findings (ignoring the per-batch fileId).

Questions, each asked --repeat times in a fresh conversation through POST {ai}/api/v1/chatv2:

IdQuestion
Q1the Sandbox indexes preset — “List the indexes I can read.”
Q2the recent-errors preset — “Show me the most recent error-level log lines and explain what they say.”
Q3“What do the findings in {dataset} say?” (needs the upload)
Q4“What went wrong in the Hadoop log I just uploaded?” (needs the upload)
Q5“When did the errors peak in {dataset}?” (needs the upload)
Q6the explain-trace preset (NullPointerException example trace)
Q7the worksoft-cortex add-on’s first suggested prompt — skipped unless the add-on is enabled

Compared against repeat 1: handler, model, facts_digest, the “Likely trigger”, every number and every timestamp in the answer, and any error. cache and tier are printed but not compared — the second ask is supposed to be a hit, and flex is the same model.

Status per question:

StatusMeaning
MATCHevery repeat agrees with the first
MISMATCHrepeats disagree on the same data — a failure
MOVEDrepeats differ and were keyed on different data_version values: the workspace data changed between the askings (a collector shipping every minute does this). Not a failure — new data is allowed a new answer
SKIPthe question does not apply (Q7 without the Worksoft add-on)
FAILa step failed — an error answer, or no uploaded dataset to ask about

Exit codes: 0 every repeat matched (or moved with its data); 1 a mismatch, a failed step, or the key appearing in a response; 2 no credential (before any network call).

Backend catalogue and registry (2026-10-07)

Section titled “Backend catalogue and registry (2026-10-07)”
  • service/byollm/ModelCatalog.java — the curated list for the BYO-LLM Settings page and the Warden provider catalog. NeuralWatt: deepseek-v4.1-flash, deepseek-v4.1-flash-flex, deepseek-v4-flash, glm-5.3-flash, qwen3.6-35b, kimi-k2.7-code.
  • ByoLlmService defaults: byo.platform.provider = neuralwatt, byo.platform.model = deepseek-v4.1-flash.
  • config/ModelConsolidationMigrator.java runs once on boot (marker model-consolidation-2026-10); resources/migrations/2026-10-model-consolidation.sql is its idempotent hand-run mirror. On an empty registry it does nothing (BillingCatalogSeeder writes the consolidated catalogue directly). Otherwise it:
    1. upserts ai_model deepseek-v4.1-flash (provider neuralwatt, capabilities jsonMode, toolUse, vision, reasoning, contextWindow 1,048,576), enabled;
    2. upserts its ai_model_rate: token basis, input 150,000, cached input 15,000, output 600,000 micro-USD per million tokens (0.15 / 0.015 / 0.60 USD per M), markup_bps 50000, path standard, effective 2026-10-07;
    3. upserts rank 0 of global roles analysis, chat, chat_cheap, chat_strong, code_fix, judge to the model. A new row gets requires_baseline only for analysis; an existing row keeps its requires_baseline as found, and its quality_baseline_digest is cleared only if the model actually changes. There is no vision role — vision uses the primary model;
    4. disables every global rank above 0 for those roles (the old judge rungs, rank 1 qwen3.6-35b-fast and rank 2 kimi-k2.7-code-fast), kept for the record, no longer served.
  1. Check the provider serves it to this account. The new id must be in the customer-scope catalogue. After the change, /health on ai-service, warden and analyzer must not be degraded, and the logs must not show MODEL_NOT_IN_CATALOGUE. Check its supported_efforts include every reasoning_effort in the call-policy table, and vision if it will read images.

  2. Set the env keys through the console (POST /api/env, see Environment variables), one request per service:

    ServiceKeys
    ai-serviceLLM_MODEL; LLM_VISION_MODEL if it is set explicitly (otherwise it follows LLM_MODEL)
    wardenLLM_MODEL, WARDEN_JUDGE_MODEL
    ingest-shimLLM_MODEL, LLM_VISION_MODEL — it loads the same config module; no model call was found in src/shim/
    analyzerLLM_MODEL; WARDEN_JUDGE_MODEL if set
    Terminal window
    curl -s -X POST http://127.0.0.1:8090/api/env \
    -H 'Content-Type: application/json' \
    -d '{"host":"dev","service":"ai-service","changes":{"LLM_MODEL":"new-model-id"},"apply":true}'

    On "host":"prod" the recreate is refused unless the body also carries "confirm":"DEPLOY-TO-PRODUCTION". The request returns 202 with a job id.

  3. Update the backend catalogue and registry — add the id to ModelCatalog’s NeuralWatt list, and write a migration in the shape of ModelConsolidationMigrator: ai_model row, ai_model_rate row, rank-0 ai_model_role rows. A service with no env override for a role takes the registry row; the analyzer refuses an analysis row with requires_baseline until the baselines are re-measured or ALLOW_UNVERIFIED_ANALYSIS_MODEL=true.

  4. Bump PROMPT_VERSION and ANALYSIS_VERSION if the new model changes what an answer or an analysis says. The open-ended cache key already contains the model id, but the upload-reuse key does not: without an ANALYSIS_VERSION bump an identical re-upload replays the old model’s findings.

  5. Re-run cortex_consistency.py against the estate and expect exit 0.