Changelog¶
All notable changes to this project are documented here. The format follows Keep a Changelog, and versions follow SemVer (pre-1.0: minor versions may include breaking changes).
Unreleased¶
Added¶
prism doctornow reports per-device VRAM usage (used / total / freein MiB) and emits alogging.getLogger("prism.cli")warning when free VRAM is below$PRISM_VRAM_RESERVE_MB(default 1536). The warning names the device (e.g.GPU #0), the livevram_used_mb / vram_total_mb / vram_free_mb, the configured threshold, and the next step (POST /v1/unload). Threshold is read through the newprism.resources.low_vram_threshold_mb()helper, which mirrors the env-read pattern the existingcheck_can_loadguard already uses. No new env var, no new flag.
Fixed¶
prism acp: fixed aNameErrorontimeduring request timeout/cancellation, forwarded structured SSEtool_callsemitted byprism serveto the ACP agent tool-execution loop, properly propagated turn cancellation duringfs/read_text_fileandfs/write_text_fileRPCs instead of recording false tool failures, silenced JSON-RPC response frames for notifications lacking anid, and ensured worker threads for active sessions are cancelled upon EOF.prism acp's direct-engine fallback (used whenprism serveis unreachable) gave an unhelpful rawKeyError/AttributeErrormessage when the session's model was an Ollama model or unresolvable, instead of the clear "start the server" messageprism mcp's equivalent fallback already gives; it now checks the resolved model's backend first. A failure while reading anHTTPError's body (e.g. the connection drops mid-read) is now itself guarded — previously it could escape_run_prompt's error handler entirely and silently drop the response for thatsession/prompt, hanging the client with no reply at all.prism.mcp's lazy_catalog()/_engine_manager()singletons used a plainthreading.Lockfor_state_lock, but_engine_manager()calls_catalog()while already holding it — a non-reentrant lock self-deadlocks on that nested acquire, soprism mcp's direct-engine fallback (used whenprism serveis unreachable) would hang forever the first time it ran. Switched tothreading.RLock. Found while reviewing the equivalent, correctly-RLock'd pattern added forprism acp(Phase 12); no test previously exercised the real_engine_manager()body, sinceTestServerlessFallbackpatchesmcp._managerdirectly.
Changed¶
- Spec invariant I9 added: "ACP agent never touches user files or runs commands." Every read or write requested by the model is mediated through the client editor's
fs/*capabilities. prism.clinow readsPRISM_API_KEY(and, by next phase, the rest of thePRISM_*vars) through a typedEnvConfigdataclass inprism/env_config.py. CLI flags keep precedence over the env. Defaults are unchanged for users; a typo in a var is caught once at startup instead of at first use.tool_choiceis no longer silently rewritten to"auto"for every non-"none"value. The four accepted shapes ("none" | "auto" | "required" | {"type": "function", "function": {"name": ...}}) are honored as OpenAI specifies: the structured form narrowstoolsto the single named tool (a name not in the list is a hard400 invalid_request_errorbefore the engine is touched; the unknown shape is also a400). The previous behaviour silently passed the fulltoolslist to the model even when the caller explicitly named one tool.prism.templates.supports_toolsno longer usesre.search(r"\btools\b", text). The check is now structural: thetoolssymbol must appear inside a Jinja expression ({{ ... tools ... }}) or a Jinja block ({% ... tools ... %}, covering{% if tools %},{% for t in tools %},{% set t = tools %}, etc.). Templates that only mentiontoolsin a{# comment #}or in prose no longer falsely report support (no False-Positives on neighbouring text); the same models as before reach the400 tools_not_supportedpath because real Jinja usage still matches.PRISM_PREFILL_CHUNKnow defaults to1024tokens (0oroffrestores whole-prompt prefill). Without it, one long prompt left most of the GPU memory held after the model was unloaded, and the next model loaded then ran about 25 times slower (qwen2.5-coder-7b after a 4200-token Phi-4-mini prompt: TTFT 83 s and 1.2 tok/s, against 0.34 s and 30 tok/s with the default).- README: a "Prism" badge was added next to the existing version/CI/license badges, linking to the project repository.
- ONNX sampling:
temperatureandtop_pnow take effect. Mostgenai_config.jsonfiles saytop_k: 1, and with that ONNX Runtime GenAI ignores both, so every request was greedy (Phi-4-mini at temperature 1.5: 1 distinct output in 4 runs before, 4 in 4 now). When sampling,top_kis the model's own value if above 1, else 40. - Queue slot is released when the HTTP client disconnects while waiting for the engine lock:
ActiveEngineManager.use_enginenow accepts anis_alivecallback and poll-iterates with a 50 ms tick, so a gone peer no longer holds a slot for the full--queue-timeout(default 300 s). The check usesrecv(1, MSG_PEEK)with the socket temporarily set non-blocking; no new flag, no new environment variable. Disconnects mid-generation are still handled as before. - Chat-template render failure with tools no longer falls back silently to the built-in format (which would drop the caller's tool definitions).
/v1/chat/completionsnow returns400 template_render_failedinstead, naming the model id and the underlying jinja/template exception text. The no-tools path keeps the silent fallback (I7). 503 insufficient_resourcesnow carries a structurederror.holder = {"pid", "model"}when the model-load lock is held by another process, alongside the existing prose inerror.message. Orchestrators (e.g.benchrig --runtime prism) can read the field to decide between wait / drain / smaller-model.
Added¶
- ACP agent now mediates model tool calls through the editor's
fs/*capabilities (read_file,write_file). prism acp: a stdio server speaking the Agent Client Protocol (ACP), so ACP-capable editors (Zed and others) can run a streamed, cancellable chat session against a local, zero-cost Prism model. First milestone:initialize,session/new,session/prompt(plain-text content blocks only) andsession/cancel— no file or terminal access yet. Falls back to an in-process ONNX engine whenprism serveis unreachable, same asprism mcp. New$PRISM_ACP_MODELenv var andprism connect acp [--write] [--test]to configure Zed'sagent_serverssettings. A malformed-but-JSON-valid message (wrong-typedparams/prompt) returns a JSON-RPC error instead of crashing the stdio loop; a failed generation no longer leaves a dangling, unanswered user turn in the session history;initialize'sprotocolVersionrejects booleans and negative numbers instead of echoing them back.prism.catalog.DEFAULT_FALLBACK_MODEL_IDcentralizes the hardcoded model idprism mcpandprism acpfall back to when no local models are installed at all. A broken stdout pipe (the editor closed the connection) is logged and dropped instead of crashing the process; a late failure while reporting a completed answer no longer discards that answer; an internal fault (e.g. a catalog I/O error) is reported as-32603 Internal errorinstead of being mislabeled-32602 Invalid params— now via an explicitAcpRequestErrorcontract rather than guessing from exception type, since a genuine internal bug can raise the sameAttributeError/TypeErrora malformed request raises. A pathologically deep (but syntactically valid) JSON line no longer crashes the process with an uncaughtRecursionError. The lazy_catalog()/_engine_manager()singletons are now guarded by a sharedthreading.RLock, closing a race where two sessions hitting the direct-engine fallback at once could each construct their own engine manager (two engine locks instead of one, risking a concurrent model load). A session's lifecycle can now be traced throughlogging.getLogger("prism.acp")(session creation, prompt accept/start/finish, the direct-engine fallback trigger, failures, and cancellation), mirroringprism/server.py's existingloggingconvention; standardloggingconfiguration applies, no new env var or flag. The module's remaining operator-facing stderr notices (a dropped response when stdout is gone, a dropped malformed stdin line) now also emit the equivalentlogging.warningalongside the existingsys.stderrwrite.prism.mcpgets the equivalentlogging.getLogger("prism.mcp")tracing:tools/calldispatch andcall_prism_server's model resolution, the direct-engine fallback trigger, and its HTTP/internal error paths, atDEBUG/WARNING. Same convention asprism.acp; no behavior change.- Centralised env-var schema (
prism/env_config.py, stdlib only): one frozenEnvConfigdataclass declaring everyPRISM_*variable with its type, default, and__post_init__validator.EnvConfig.from_env(...)builds an instance from a Mapping;EnvConfig.from_env_file(path)parses a hand-rolledKEY=VALUEfile (whitespace,# comment, double/single-quoted values);load_config()runs at CLI / server startup, fails fast on bad values (oneValueErrorlisting every problem), and emits onelogging.warningfor any unknownPRISM_*key. No runtime dependency — nokev, nopydantic-settings, nopython-dotenv..envfile loading is opt-in only:PRISM_ENV_FILE=/path/to/.env(security: never auto-discover a file the operator did not name). - Reasoning separation:
prism.reasoningmodule extracting<think>...</think>blocks intomessage.reasoning_content(and streamingdelta.reasoning_content) for OpenAI-compatible/v1/chat/completionsacross ONNX and Ollama backends, while keeping raw output intact for legacy/v1/completions. - Thread control:
PRISM_THREADSenvironment variable to set intra-op thread count via session_options overlay on ONNX models. - Diagnostics and telemetry:
prism statusdisplays system RAM and swap usage; model loads log memory deltas ("VRAM +N MB, RAM +N MB");prism doctorinspects Windows WSL2.wslconfigand warns ifmemory=orautoMemoryReclaim=gradualare missing. - Request queue limit:
--max-queueCLI flag and$PRISM_MAX_QUEUE(default 8) to bound waiting requests for active ONNX model, immediately returning 503server_busywhen full. - Guarded ONNX model loading:
OnnxGenAiEngine._load_modelacquires the machine lock and checks memory headroom before allocating library resources. Returns HTTP 503insufficient_resources(Retry-After: 30) on/v1/chat/completionsand provides actionable hints in MCP tool completions. - Cross-process model load lock:
prism.machine_lockmodule withflockon~/.prism/load.lock(viastate_dir()inprism.paths) to serialize model loading across processes and prevent concurrent VRAM spikes (timeout configurable viaPRISM_LOAD_TIMEOUT,PRISM_LOAD_LOCK=off). - Resource budget and capacity guard:
prism.resourcesmodule withram_available_mb(),vram_free_mb(),estimate_load_mb(), andcheck_can_load()to guard against VRAM and RAM exhaustion before loading models (configurable viaPRISM_RAM_RESERVE_MB,PRISM_VRAM_RESERVE_MB,PRISM_RESOURCE_CHECK=off). - AI-native SDLC for coding agents (Claude Code, Codex, Cursor, ...):
intent.md,spec.md,plan.mdandREVIEW.mdas committed artifacts,AGENTS.md,sdlc*skills in.agents/skills/,scripts/sdlc_check.py(gate, and--redto prove a new test fails first), an opt-in.githooks/pre-commit, and a CI changelog check on pull requests. Seedocs/sdlc.md. top_kandrepetition_penaltyrequest fields (ONNX and Ollama);frequency_penalty/presence_penaltygo to Ollama, and on ONNX a non-zero value is a400unsupported_parameterinstead of being silently ignored.- Loop guard: an ONNX generation that ends in a repeating token cycle (period up to 64 tokens, over at least 200 tokens and 12 repetitions) is stopped after a warning
in the log and reported as
finish_reason: length.PRISM_LOOP_GUARD=offdisables it. POST /v1/drain: an orchestrator (e.g. a benchmark that needs a different model on the same GPU) can ask the runningprism serveto finish its current request, unload the model, and exit with status 0. Reply is{"drained": bool, "model": str|null, "exit_in_ms": 250}. Idempotent; same auth as/v1/unload. Stdlib-only (os._exit,threading.Timer).POST /v1/unload: drops the ONNX model currently held byprism serveand frees its VRAM/RAM. The body is optional (any JSON object is accepted and ignored), the call is idempotent ({"unloaded": bool, "model": string|null}), and it acquires the engine lock so an in-flight generation finishes before the model is released. Useful between benchmark runs of different large models, so each one starts from cold VRAM. Requires the API key when--api-keyis set; without it, the endpoint is open on loopback like every other route.PRISM_MCP_AUTO_STOP_SEC(a positive float, seconds):prism mcpexits with status 0 after that many seconds without atools/call; the timer resets on every call. Intended for test harnesses that start the MCP process to satisfy editor integrations but never drive it, so the process stops holding its Python/CUDA startup overhead (~1.6 GB VRAM passive) after the run.0or unset = no auto-stop (current behaviour). One-line stderr message on exit names the env var so the cause is obvious in a test log.
Fixed¶
- Tests: WSL config doctor tests in
tests/test_prism_cli.pynow run hermetically on non-WSL Linux environments (is_wsl_systemhonorsPRISM_WSLCONFIG_PATHandPRISM_FORCE_WSL=0). - ONNX engine dropped every token with id 0 (
!in Phi-4: "Wow! Great!" came out as "Wow Great") and did not count it, so a reply cut off atmax_tokenscould be reported asfinish_reason: stop. - Ctrl+C / SIGTERM during an in-flight streamed response used to print a
Tracebackalongside the "Shutting down server…" message (OSError("Bad file descriptor")orValueError("I/O operation on closed file.")from awfile.writeafterwith server:closed the connection underneath the daemon handler thread). The SSE / JSON writers —_begin_sse,_sse,_sse_error,_send_json,_send_cors_headers,do_OPTIONS, and the streaming loops in_generate/_generate_ollama— now swallow that family of I/O errors at debug;_ssere-raisesConnectionResetErrorwhen its write failed so the streaming loops' existing client-disconnect catch aborts generation as before.POST /v1/drain(P12) is its own exit path and is unchanged.
0.2.0 - 2026-09-20¶
Added¶
prism convert MODEL: converts and quantizes a Hugging Face model (or a local folder) to an ONNX Runtime GenAI folder with onnxruntime-genai's own model builder (CUDA or CPU,int4orfp16) and installs it next to pulled models. Optionalconvertextra; it runs as a subprocess, is verified like a pull, is built in a staging folder, and only replaces an existing model with--force.prism doctorreports whether its requirements are installed.stream_options.include_usageon streaming requests: a last chunk with emptychoices,usageand thetelemetryblock (device, timings). Streaming responses carried neither before, so clients had to estimate token counts.-
PRISM_PREFILL_CHUNK(a positive integer of tokens): process the prompt in chunks. ONNX Runtime GenAI's GPU memory otherwise grows by about 1.4 MB per prompt token and is not released; on Phi-4-mini with 4500 prompt tokens the peak fell from 11.7 GB to 6.6 GB with a chunk of 256; on two other models the peak fell by 54% and 5% while time to first token rose by 110% and 33%, so it stays off by default. See Devices & CUDA. -
stopon chat and legacy completions (a string or up to 4 strings): text is cut at the first match, held-back partial matches never leak, and generation aborts. Previously it was ignored. - ONNX
max_tokensis capped to the room left in the model'scontext_length(fromgenai_config.json); a prompt that fills the window is a400 context_length_exceededinstead of a failure inside the engine. - Ollama responses carry
usageand the realfinish_reason(lengthwhen it hitmax_tokens), and honourstream_options.include_usage. -
$OLLAMA_HOSTselects the Ollama daemon. -
prism pull owner/repodownloads one model folder from a repo that keeps several side by side (Microsoft'scuda/…,cpu_and_mobile/…,directml/…), chosen by--epand--quant, or by the new--variant TEXT. Before, such a repo was downloaded whole (every variant) and then rejected for lacking a rootgenai_config.json. Leftover.azDownload*files are skipped. - Aliases
phi-4-mini-reasoning,phi-4-reasoning,phi-3-mini-4kandmistral-7b-instruct-v0.2;scripts/verify_aliases.pynow also requires an.onnxfile and a tokenizer. -
Chat templates for Gemma and Mistral (v0.1/v0.2 and v0.3+), checked against the real Jinja templates of Gemma 2 and 3 and of Mistral v0.2 (Microsoft's ONNX export) and v0.3.
-
prism serve --queue-timeout SEC/$PRISM_QUEUE_TIMEOUT(default 300,0= no limit): a request that cannot get the model in time is answered503 server_busywithRetry-Afterinstead of hanging behind a slow generation. -
Optional
jinjaextra (pip install "prism-local[jinja]"): Prism renders the model's own chat template in Jinja's immutable sandbox instead of only recognising its family, which reproduces details like Qwen2.5's default system prompt. A leading BOS is dropped when the tokenizer adds it. A template that fails falls back to the built-in format with a warning.$PRISM_TEMPLATE=auto|jinja|builtin. Without jinja2 nothing changes. -
Tool calling on
/v1/chat/completions:tools,tool_choice: "none",role: "tool"messages and assistanttool_calls; replies carry OpenAI-shapedtool_callsandfinish_reason: "tool_calls", streaming or not. ONNX models need thejinjaextra and a chat template that takestools(otherwise400 tools_not_supported); their calls are recognised in the output in the Qwen/Hermes, Phi-4-mini, Mistral and Llama 3.1 conventions, and streaming withtoolsis buffered. Ollama models passtoolsto the daemon. Checked end to end on Qwen3-0.6B (ONNX, CPU) and Llama 3.1 8B (Ollama). -
POST /v1/embeddings(float and base64), served by Ollama; ONNX models answer400 embeddings_not_supported.
Fixed¶
- ONNX Runtime GenAI decodes special tokens to empty text, which removed the
<tool_call>,<|tool_call|>,[TOOL_CALLS]and<|python_tag|>markers from the output. The engine now restores those (and only those) for models whose tokenizer drops them. finish_reasonisstop, notlength, when the model ends with an end-of-sequence token exactly atmax_tokens(the EOS ids come fromgenai_config.json).- The model scan no longer walks into the subfolders of a model folder.
prism mcp, with no server running, loads the model once and keeps it for later tool calls (released after 2 idle minutes) instead of reloading it on every call.
Changed¶
- The chat template is taken from the model's own
chat_template(chat_template.jinja,chat_template.jsonortokenizer_config.json) when it has one Prism knows, and only otherwise guessed from the name. Checked against the templates of Phi-3.5, Phi-4, Phi-4-mini, Qwen2.5-Coder and Qwen3: Prism's prompt is now identical to the rendered Jinja for all five. - Phi-4 and Phi-4-mini get their own formats (
<|im_start|>…<|im_sep|>, and<|role|>…<|end|>without newlines). Both were sent the Phi-3 format, and Phi-4 (14B) is not ChatML-compatible, so its prompts were malformed. - Message
contentgiven as a list of parts ([{"type": "text", ...}]) is flattened to its text; it used to appear in the prompt as a Python repr, and Ollama received the raw list. - GPU telemetry (NVML) is reused for 2 seconds, so
/healthand/v1/modelsno longer initialise NVML on every call;prism benchmarkstill reads it fresh. GET /v1/modelsandprism listshow the device a model will run on (CPUorCUDA (GPU), following--deviceand the hardware) instead of what its files were exported for; the latter isexported_forin the API. Under--device autoageneric-cpumodel runs on CUDA when a GPU is present.
0.1.0 - 2026-09-19¶
First public version.
Added¶
prismCLI:status,doctor,list,pull,run,chat,serve,benchmark,mcp,connect.- Engines: ONNX Runtime GenAI (CUDA or CPU) and Ollama (GGUF), behind one OpenAI-compatible server.
--device auto|cuda|cpu/$PRISM_DEVICE; the device actually used is reported bybenchmark,/healthand responses.prism doctorchecks (withldd, neverdlopen, which can crash) that ONNX Runtime's CUDA provider can find all its libraries and names any that are missing.- Per-model chat templates (Phi, ChatML/Qwen, Llama 3, DeepSeek).
- Curated aliases (
phi-4-mini, ...) resolve to the installed variant that suits the machine (CUDA if a GPU is present), so they are not ambiguous when several variants exist. - Server: API key (
--api-key), CORS allowlist (--cors-origin), Host-header check, 10 MB body cap, OpenAI-shaped JSON errors, streaming with role delta andfinish_reason, tokenizer-basedusage, client-disconnect cancellation. - Cursor, Cline and MCP connectors.
pyproject.tomlwith aprismconsole script andcuda,pull,dev,docsextras. Thecudaextra installs a matched stack (onnxruntime-genai-cudaandonnxruntime-gpu[cuda,cudnn], CUDA 13; Python 3.11+).- Model search paths via
$PRISM_MODEL_DIRS(default~/.prism/models). - Documentation site (MkDocs Material) and CI (Python 3.10-3.12, wheel smoke test).
Changed¶
- The server binds to
127.0.0.1by default (was0.0.0.0) and sends no CORS headers by default. - Inference is serialized behind a lock; concurrent requests queue instead of racing the engine.
prism pullfails when the download lacksgenai_config.jsonor weights, and prunes empty nested folders.- Models are no longer discovered in
./modelsor a sibling checkout;bin/prismno longer looks for a sibling virtualenv. UsePRISM_MODEL_DIRSandPRISM_PYTHON. fngandfoundry-nglaunchers are deprecated aliases ofprism.
Fixed¶
- The ONNX engine never selected an execution provider, so models whose
genai_config.jsonhas an empty provider list ran on the CPU while being labelled GPU. Provider selection is now explicit, with a warned fallback inautomode. - Wrong chat template for every non-Phi model.
- Unsynchronised engine access and unhandled errors that dropped connections.
usage.prompt_tokenswas a whitespace word count.- CUDA library discovery relied on hard-coded paths and on
LD_LIBRARY_PATHedits that cannot affect the running process.