Jiva v0.3.50 — Unified Config, Vision Detection & Code-Mode Improvements
Release Date: July 14, 2026
Summary
Model-agnostic rate limiting and low-output-budget handling (motivated by
Sarvam-105B's 40 req/min and 4096-token limits, but applicable to any
similarly-constrained provider), a real bug fix where the CLI silently
dropped defaultMaxTokens/reasoningEffortStrategy config, richer metadata
recorded on code-mode conversations, unified config storage across CLI and
HTTP interfaces, and auto-detection of vision-capable models during setup.
New Features
Model-agnostic proactive rate limiting
ModelClientConfig.maxRequestsPerMinute — when set, ModelClient tracks its
own request timestamps in a trailing 60s window and waits before sending
a request that would exceed the limit, instead of only reacting to 429s
after they happen. Set for any provider with a known hard rate ceiling; the
Sarvam preset in the setup wizard now sets this to 40 automatically.
429 handling also now checks the standard Retry-After HTTP header (RFC
6585) first, falling back to parsing provider-specific wording out of the
error body (Groq's "Please try again in 8.53s.") only when the header is
absent.
Skeleton-first workflow for low-output-budget models
When the reasoning model's defaultMaxTokens is ≤ 8192, code mode's system
prompt now mandates (not just recommends for large files) scaffolding
every new file as ### section-<name> / ### endsection blocks and filling
them in one at a time — the model is told its exact token budget explicitly.
edit_file gained a use_regex mode to make this reliable: old_string is
compiled as a regex and matched against the file, so a whole section can be
replaced by matching its markers without needing to reproduce the section's
current (stub) content exactly.
Code-mode conversation metadata
ConversationMetadata (unified into storage/types.ts — previously
duplicated with a second, drifting copy in conversation-manager.ts) gains:
{
mcpServers?: string[] // MCP servers enabled for this code task
maxIterations?: number // iteration budget configured for this task
harness?: string // e.g. jiva-core's 'evaluator', or a UI feature
// like Jivam's 'deep-run' — free-form, not a fixed enum
}saveConversation()/autoSave() take an optional codeMeta parameter;
CodeAgent passes its own mcpServerNames/maxIterations/harness, and
previously-saved values persist across subsequent auto-saves that don't
repeat them. The workspace directory a code task ran in was already captured
via the existing workspace field.
Unified config storage across all interfaces
Jiva's CLI config previously lived at a platform-specific location via the
conf package's default resolution (~/Library/Preferences/jiva-nodejs on
macOS, %APPDATA%/jiva-nodejs/Config on Windows, ~/.config/jiva-nodejs on
Linux) — a different file entirely from what the local (non-cloud) HTTP
interface's LocalStorageProvider already used for config
(~/.jiva/config.json, alongside conversations and logs that already lived
there). This meant CLI and local-HTTP-mode configuration were silently out of
sync with each other.
ConfigManager now pins conf's storage to ~/.jiva via the cwd option,
so both interfaces read/write the exact same file.
migrateLegacyConfigIfNeeded() runs once, the first time ConfigManager is
constructed (both interfaces trigger this on startup): if ~/.jiva/config.json
doesn't exist yet but a legacy platform-specific config does, it's copied over.
If something unexpectedly already exists at the new path by the time of the
write (a race, or a stray file), it's backed up to config-backup.json first
rather than clobbered. Once ~/.jiva/config.json exists, it's treated as
authoritative and migration is skipped on all subsequent runs. The CLI prints
the migration result on the next jiva invocation; the HTTP interface logs it
via logger.warn on startup.
Auto-detect vision capability during setup
v0.3.49 added hasVision support for reasoning/tool-calling models, but the
CLI setup wizard never asked for it — the flag could only be set by hand-
editing config.json.
src/utils/vision-detection.ts matches a model name against known vision-
capable model families (Llama 4 Maverick/Scout, Qwen, Kimi, GLM, Claude,
GPT-4+/GPT-5/o-series, Gemini, Pixtral, Grok, Phi multimodal) and returns a
short reason when matched. This only drives the default answer to a y/n
prompt — never authoritative, always overridable.
setup-wizard.ts now asks "Does this model support vision (image input)?"
right after the model name is entered, for both the reasoning and tool-calling
model setup flows, defaulting to the detection result and showing the matched
reason (e.g. "Llama 4 Maverick supports vision"). A Vision: Yes/No line is
also shown in jiva config --show.
Vision-capable reasoning/tool-calling models (no separate multimodal model needed)
Some providers (e.g. Groq's llama-4-maverick, Krutrim's vision-enabled
models) offer a single model that handles both reasoning and vision, making
the separate dedicated multimodal model configuration and its image-captioning
detour unnecessary and wasteful for those setups.
A new hasVision config flag (ModelConfigSchema + ModelClientConfig) lets
any model instance declare native vision support regardless of its type. When
set:
ModelClient.supportsVision()reportstrueModelOrchestratorroutes image content directly to that model instead of
running it through the dedicated multimodal model's caption-then-forward
pipelinehasMultimodalSupport()/getMultimodalModel()account for it too
Priority order when a request contains images: dedicated multimodal model
(unchanged legacy behavior) > reasoning model with hasVision > tool-calling
model with hasVision > legacy passthrough. Existing setups with a configured
multimodal model are unaffected.
DualAgent and CodeAgent need no changes — both delegate all model calls
through ModelOrchestrator.chat()/chatWithFallback(), so fixing routing at
the orchestrator is sufficient for both to pick up native vision support.
Bug Fixes
Manager double system-message crashes strict providers
Symptom: 400 System message must be at the beginning on providers that
reject anything but a single leading system message (e.g. Krutrim's Qwen3.6),
whenever a directive was configured.
Root cause: ManagerAgent.getSystemMessages() (src/core/manager-agent.ts)
appended the directive as a second developer-role message, which becomes a
second system message after role conversion. Krutrim's gpt-oss-120b
happened to tolerate two system messages; Qwen3.6 didn't.
Fix: The directive is now merged into the single system message instead
of appended separately. Safe for all providers — Manager never sends tools,
so Harmony's developer-role tool-injection path never depended on the
directive being a separate message.
CodeAgent gives up after 4 empty responses despite ample maxIterations
Symptom: Code mode would stop with [No response content] after just
~10 iterations even with maxIterations set to 100, following a run of
Empty response with no tool calls warnings. Manually retrying the same
request usually succeeded.
Root cause: In-loop context compaction was gated on the response having
tool calls. Once the model starts returning genuinely empty responses (no
content, no tool calls), that gate can never be true again — so context can
never shrink back down, and the same bloated prompt gets resent on every
recovery-nudge retry, guaranteeing the same failure each time. The
empty-response retry cap (4), not the iteration budget, was the real limiter.
Fix:
- Removed the tool-calls requirement from the in-loop compaction trigger
(src/code/agent.ts) — compaction now runs whenever prompt tokens exceed
the threshold, regardless of whether the last response had tool calls. - Added
finishReasontoModelResponse(src/models/base.ts,
src/models/model-client.ts), captured from the API's own
choices[0].finish_reason(previously discarded). When an empty response
hasfinishReason: 'length', that's direct evidence the model ran out of
completion-token budget mid-"thought" — CodeAgent now forces an immediate
compaction in that case (bypassing the normal token threshold) instead of
just adding another text nudge, which was only adding more prompt weight
and making the same budget problem worse on the next attempt.
CLI silently dropped defaultMaxTokens and reasoningEffortStrategy
Symptom: A reasoning model configured with defaultMaxTokens: 4096
(required for Sarvam-105B) had no effect when used via jiva chat — the
request went out with no max_tokens cap at all, letting Sarvam apply its
own internal default and risking the model exhausting its completion budget
on its reasoning chain before producing visible output (the same failure
shape as the empty-response bug fixed in v0.3.49, via a different path).
Root cause: All four createModelClient() call sites in
src/interfaces/cli/index.ts (reasoning ×2, tool-calling ×2) never passed
defaultMaxTokens or reasoningEffortStrategy from the loaded config,
despite both being present in the config schema and set by the setup
wizard.
Fix: Both fields now passed through at all four sites, along with the
new maxRequestsPerMinute.
Conversation titles always fell back to the first-message snippet
Symptom: Conversation titles were never the LLM-generated 3–5 word
summary they were supposed to be. Saved conversations in
~/.jiva/conversations/ showed one of two wrong shapes:
- The fallback snippet — the first ~40 chars of the first user message
plus...(e.g.make a beautiful looking game of tetris...), or - Raw model "thinking" leaked into the title — e.g.
\nOkay, the user wants a short title for their mess....
Root cause: ConversationManager.generateTitle() (in
src/core/conversation-manager.ts) asked the orchestrator for a title with
maxTokens: 20. The orchestrator routes tool/image-less calls to the
reasoning model, which (for models like zai-org/GLM-5.2) emits an
inline …</think> chain before the answer. 20 tokens is barely
enough for the thinking alone, so the model routinely exhausted its budget
mid-thought — the response came back as \nOkay, the user wants a short title for their mess… with no closing </think> and no actual
title.
The cleanup pipeline then failed in two ways:
extractAssistantMessage()(insrc/models/harmony.ts) strips Harmony
control tokens (<|call|>,<|channel|>, …) but not ``
blocks, so the thinking text survived intoresponse.content.generateTitle()'s own cleanup (trim, de-quote, de-punctuate, truncate
to 60 chars) didn't remove the thinking either, and its garbage guard
(!title || length < 3 || includes('untitled')) didn't catch 60-char
leaked thinking — so it was returned verbatim. When the API returned
the thinking out-of-band (inreasoning_content, leavingcontent
empty), the guard did trip and the method fell back to the first
40 chars — producing the snippet-shaped titles.
Fix:
- Added
stripThinkingContent(), which removes both properly-closed
…</think>blocks and truncated/unclosed<think…runs (the
model ran out of budget before the closing tag). It's applied to
response.contentbefore any other cleanup. - After stripping, the last non-empty line is taken as the title (the
model sometimes emits preamble before the actual title). - Raised
maxTokensfrom 20 → 200 so a reasoning model has room to
finish its thinking chain and emit the title. - Set
reasoningEffort: 'low'on the title call to minimise thinking for
what is a trivial classification task.
The existing first-message fallback is retained as the last resort when
the model still returns nothing usable.
Files Changed
| File | Change |
|---|---|
src/utils/errors.ts |
ModelError.retryAfterMs |
src/utils/vision-detection.ts |
New — detectVisionCapability() matches model names against known vision-capable families |
src/models/model-client.ts |
Proactive rate limiter; Retry-After header parsing; getDefaultMaxTokens(); supportsVision() honors hasVision |
src/models/base.ts |
Added finishReason to ModelResponse (captured from API's choices[0].finish_reason) |
src/models/orchestrator.ts |
Image routing accounts for hasVision on reasoning/tool-calling models |
src/core/config.ts |
maxRequestsPerMinute field on ModelConfigSchema; migrateLegacyConfigIfNeeded(); config storage unified to ~/.jiva |
src/interfaces/cli/setup-wizard.ts |
Sarvam preset sets maxRequestsPerMinute: 40; vision capability prompt after model name entry |
src/interfaces/cli/index.ts |
Fixed missing defaultMaxTokens/reasoningEffortStrategy passthrough (×4 sites); harness wiring for --harness evaluator; vision prompt wiring |
src/interfaces/http/index.ts |
Register config migration on startup; register benchmark routes |
src/interfaces/http/session-manager.ts |
maxRequestsPerMinute passthrough |
src/code/agent.ts |
Low-output-budget skeleton-first system prompt; harness config field; codeMeta on save calls |
src/code/tools/edit.ts |
use_regex mode for edit_file |
src/core/manager-agent.ts |
Merged directive into single system message instead of appending a second one |
src/core/conversation-manager.ts |
ConversationMetadata/CodeConversationMeta now re-exported from storage/types.ts instead of duplicated; generateTitle() strips reasoning `` blocks, raises maxTokens 20→200, sets `reasoningEffort: 'low'`; new `stripThinkingContent()` helper |
src/storage/types.ts |
Canonical ConversationMetadata gains mcpServers/maxIterations/harness; new CodeConversationMeta |
Upgrade
npm install -g jiva-core@0.3.50No breaking changes. All existing configs continue to work without
modification. If you're on Sarvam and haven't re-run jiva config since
before this release, doing so will pick up the new maxRequestsPerMinute
default.