mlx-optiq

Changelog

Every version of mlx-optiq published to PyPI, what shipped, and why. The source is the repo's CHANGELOG.md; this page is its public mirror.

v0.4.27

Fixed

  • DeepSeek-V4 tool calls were never executed. V4 ships no chat template, so OptiQ supplies one that teaches the model the DSML format (<|DSML|invoke name="...">), and nothing could parse it back. Handles multiple invokes per block, string="false" JSON values, and blocks truncated at the token cap.
  • DSML tool calls are parsed server-side too. mlx-lm picks a tool parser by pattern-matching the chat template and has no DeepSeek entry, so optiq serve returned the block as plain content: third-party clients saw prose and --task bfcl --served would have scored zero.
  • The Lab never compacted a long conversation. It grew until --max-context capped it, and that cap is a RotatingKVCache, which evicts from the front: system prompt first, mid-message, silently. The Lab now compacts at 80% of the server's window, archives what it drops, and emits a compacted event.

Added

  • A contract test round-trips every chat template OptiQ ships against the tool-call parser. Any format we teach a model must be one we can read back.

Changed

  • Compaction moved to one implementation shared by OptiQ Code and the Lab.

v0.4.26

Added

  • optiq eval --served URL scores a model through a running optiq serve instead of loading it into the eval process. Served BFCL reads the tool_calls the server returns rather than re-parsing text.

Fixed

  • Reasoning models scored 0 when served. mlx-lm splits output into message.reasoning and message.content and omits either when empty. Both are rejoined now; reasoning_content accepted as an alias.

Changed

  • All six benchmarks generate through one backend, so prompts, greedy decode and the per-arch repetition penalty are shared by both paths.
  • --served refuses --kv-bits, --kv-config and --stream-experts* (they patch this process, not the server), and --task kl, --task smoketest and MMLU without --reasoning (all need raw logits).

v0.4.25

Fixed

  • Gemma-4 with a per-layer kv_config no longer dies past ~10k tokens. Verified on gemma-4-26B-A4B-it-OptiQ-4bit to 14169 tokens and pinned by a regression test.
  • Only the first of several chained Devstral tool calls was executed. A turn asking to read two files ran one and dropped the other.
  • The auto context cap could size a KV window Metal refuses to allocate. It is now bounded by the device's recommended working set as well as free RAM (Devstral-24B on an M3 Max: 48128 to 33792 tokens).

v0.4.24

Fixed

  • Gemma-4 with quantized KV crashed once anything else had touched attention in the same process. The dispatch was installed on sys.modules, which is not always the dict the model's classes execute in. It now rebinds through the live model and re-asserts per request.
  • Flash-attention training reverted the KV dispatch on exit and the installer latched, so training then serving in one process left Gemma-4 quantized KV broken for the life of it.
  • LoRA training could write an all-NaN adapter, then offer it for mounting. A batch with no unmasked target tokens made the loss compute 0/0 and the global grad-norm clip spread it everywhere. The denominator is clamped and non-finite adapters are refused.
  • Devstral's tool calls were dropped when served. The Mistral parser required a [TOOL_CALLS] marker that mlx-lm's server had already consumed. It is optional now.
  • LoRA trained the base model. apply_sensitivity_aware_lora never froze it, so 133 non-quantized tensors on Qwen3.5-0.8B were updated at the LoRA learning rate and written into the adapter.

v0.4.23

Fixed

  • optiq serve --stream-experts aborted the process on the first requestThere is no Stream(gpu, N) in current thread. The model is pre-loaded on the main thread; nn.Module.parameters() skips keys starting with _, leaving rope._freqs on Gemma-4's global-attention layers unevaluated, and mlx-lm generates on another thread. The loader now evaluates model.state.
  • --stream-experts was a silent no-op on every Gemma-4 MoE. Their expert keys are …layers.N.experts.switch_glu.…, which no entry in _EXPERT_SEGMENTS matched.
  • A failed expert-streaming request fails the request instead of killing the server.

v0.4.22

Added

  • MCP servers in OptiQ Code and the Lab. stdio only. optiq code mcp import reads existing servers from Claude Code, Claude Desktop, Codex, opencode, OpenClaw, Hermes Agent, Mistral Vibe or Cursor; add / list / remove / test manage them. Config ~/.optiq/code/mcp.json or <repo>/.optiq/mcp.json, written 0600. /mcp lists what is connected. Shared with the Lab via ~/.optiq/mcp.json.
  • MCP servers no longer inherit the environment: PATH, HOME, locale and temp dir only. Capped at 64 tools per server and 100,000 characters per result; anything dropped is reported. Tools arrive as mcp__<server>__<tool> and need approval unless marked readOnlyHint. plan mounts read-only tools, minimal mounts none.
  • lab, live and slow tests run by default. There is no CI, so an excluded marker never ran at all. browser stays a separate invocation — pytest-playwright's sync API and pytest-asyncio cannot share a process.
  • Browser coverage for every Lab page, 44 tests to 114. Arena, Hub and Cluster had none.
  • The Lab states that its macOS sandbox blocks writes and network but not reads — tool code can read any file you can, including ~/.ssh. Narrowing reads was attempted and abandoned: dyld aborts before Python starts.
  • [project.urls] declares Homepage, Documentation and Changelog, and the package declares a contact email.

Fixed

  • Model output could run script in a Lab session. Chat and arena both rendered replies through marked.parse(), which does not sanitize. Lab endpoints authenticate with a same-origin cookie and two of them run Python and shell. Both sinks now strip executable elements, on* handlers and javascript:/vbscript:/data: URLs.
  • Structured output destroyed the ranking it constrained. Masking added -1e9 to every disallowed logit, making them all exactly -1e9 in float32. Masking is positional now, and a dead-end grammar stops instead of emitting noise.
  • Every SFT and DPO fine-tune from the Lab died before its first stepimage_size passed to a trainer config that does not declare it.
  • The Capability Score averaged over however many benchmarks ran. Five results divided by five, reported as the six-metric score; an all-failed run scored 0.0. Reports n_benchmarks and is_complete, returns None when nothing was measured.
  • run_tests reported passed=0 failed=0 for a command that never ran. Exit 127 read as "nothing failed". It now says nothing was measured.
  • The flash-attention patch never came off a module it imported itself, leaking the tiled backward into every later model in the process. Also crashed on the tuple form of keys/values.
  • Batching a shared-KV model with quantized KV died on the first prefill. Gemma-4 shares KV across layers; the sharing layer's tuples went down mlx-lm's unquantized branch. The sequential path had the fix, the batch path never installed it.
  • Batch + GQA + quantized KV was unusable with a real mask. Upstream leaves the mask 4-D after expanding K/V, so it cannot broadcast. Patched; a no-op once upstream fixes it.
  • DiffusionGemma's vision tower never loaded. Sidecar keys were vision_tower.…, the model nests at model.encoder.vision_tower.…, so all 31 tensors were skipped and the quant answered about images unseen. The loader raises now when a sidecar tensor has no home.
  • memory_limit_mb was a no-op on the Mac sandbox. setrlimit is ignored by macOS and the failure was swallowed; a 4 GB allocation ran under a 256 MB limit. A watchdog polls resident memory and kills the process group.
  • A cluster could shard a model into nothing. An all-zero weight vector — a failed RAM probe on every node — gave every rank zero layers. Falls back to an even split.
  • optiq cluster peers omitted the machine you are standing on.
  • optiq latency predicted throughput for models that do not exist. A typo'd path printed Size: 0.0 MB then 1015 tok/s, exit 0. Refuses when the path holds no weights.
  • The Lab could not run a tool for a two-channel model. Muse-Glimmer's ATEM markup was rendered as prose because the Lab's healing never consulted the shared matcher in optiq.toolcalls. Affected the built-in python and terminal tools identically.
  • Approval was asked before the preset was checked, so a tool about to be refused still raised a modal.
  • The Lab's model dropdown mislabelled three families and one sizedhara-250m listed as 8B, size read before the -OptiQ-Nbit suffix was stripped.
  • /api/status answered a different shape with no supervisor, and a missing supervisor returned 500. Both branches emit the same keys; the absent case is a 409.
  • A typo in the Lab's Quantize form returned a 500, and an empty bit selection silently became the 4/8 default.
  • Deep Research's JSON fallback substituted another key's list when the expected key was absent.
  • New chat kept the previous draft, images and documents.
  • The Responses stream translator emitted its terminal events twice.
  • The knapsack said nothing when the per-block floor overrode the BPW budget — a 4.0 target coming out at 5.34 looked like a miscalculation.
  • A source declaring an MTP head but carrying no mtp.* tensors was a silent no-op. How Qwen3.8-27B shipped without one: the upstream bf16 conversion had already dropped the head.
  • /arena scrolled sideways — 364px, from a <select> sizing to a full repo id.

Earlier releases

The 14 releases before v0.4.22. Click any version to expand it. Everything before v0.4.8 is in the archive.

v0.4.21

Added

  • Image input in OptiQ Code. Drag an image onto the prompt, or press Ctrl+V to attach one from the clipboard; --image PATH (repeatable) does the same for optiq code -p. The status bar shows what is attached and the transcript lists each file. A terminal cannot deliver binary on paste, so Cmd+V of a screenshot arrives as nothing and the clipboard has to be read directly, which is why it is a key rather than a paste handler. Only absolute paths auto-attach: a drop always produces one, while "fix the logo in assets/logo.png" is talking about a file, not attaching it. Images ride the opening turn only, since re-sending base64 every turn spends the context window the local model is already short of. The served model must support image input.
  • site/_build_llms_full.py: llms-full.txt is generated now, from the pages llms.txt already lists. It described itself as the complete docs corpus while sitting three releases stale (vision as Gemma-4 only, "Now supported on" notes the docs had dropped, no new model families) because nothing built it and nothing checked it. The page list is read out of llms.txt's Site map rather than copied, so the two cannot disagree, and --check fails the build when the dump is behind.
v0.4.20

Added

  • optiq code --preset plan: read-only investigation. Four tools (read_file, search, git, done) and a prompt that asks for a plan, not a change. Tab cycles plan and standard in the TUI; Shift-Tab already cycles approval. A preset rather than a flag, because the standard prompt says "if you can run it, run it", which is exactly wrong here. Enforced where the tool is dispatched: restricting the schema only asks the model not to write.
  • Image input on Muse-Glimmer. The vision tower is ported to MLX and matched to the transformers reference at 4e-07: variable-resolution ViT, window attention, 2D RoPE interleaved [w, h, w, h], bilinearly resampled position embeddings, and a 2x2 pixel shuffle. Kept at bf16 in the optiq/ sidecar like every other VLM family.
  • optiq code --preset minimal: the benchmarking composition. Two tools (bash + edit_file), a fixed one-line system prompt nothing may append to, no AGENTS.md, no project context, no compaction. OptiQ's product is a measurement, and the full agent is part of what a Capability Score measures — the same model scored 9/25 through one harness and 5/25 through another. --preset standard (default) is unchanged.
  • muse_glimmer (meta-models/Muse-Glimmer): new architecture, ported from the transformers reference and matched to 1.8e-06. Gated attention, NoPE on the full-attention layers and a sliding window on the rest, two RMSNorm conventions and two epsilons per block, RMS-normalized embedding, pre-scaled logit softcap.
  • optiq eval --stream-experts {auto,on,off} / --stream-experts-cache: score a quant that does not fit in RAM. serve and OptiqEngine already streamed MoE experts from SSD; eval did not, so the one command producing the published Capability Score was the only one that could not open the models streaming exists for. A 21 GB MoE on a 26 GB Mac loads at 4.2 GB resident.
  • reasoning_strength is now probed by the eval's concise-template path. Cuts Muse-Glimmer's GSM8K traces from 299 to 124 tokens at the same answer.

Fixed

  • A sweep could measure one layer of 417 and still write a quant. Under --reference uniform_4bit the bf16 index kept the checkpoint's model.language_model.* names while the loaded module is model.*, so every language layer of a nested-tower VLM was skipped. The result was a normal-sized artifact that loaded and generated fluent text with bit-widths that came from no measurement. Indexed the flattened alias; a sweep that measures under half its layers now raises instead of continuing.
  • A MoE could lose every expert and still pass the skip guard. Nemotron-H ships its 128 experts unfused under mixer as experts.{e}.{up,down}_proj while the running model has one fused mixer.switch_mlp.{fc1,fc2}, so all 46 expert tensors were skipped. That is 28% of layers — under the count rule — but the majority of a 30B model's weights, because a MoE's experts are a few fused tensors. The guard now fails on parameter mass as well as layer count.
  • OptiqEngine assumed every architecture nests its decoder under .model. Nemotron-H calls it backbone, and an MTP-carrying checkpoint arrives wrapped in _MTPLXTextModel exposing neither, so the engine raised AttributeError at construction — before a token was generated.
  • BFCL scored Muse-Glimmer 0/200 for a tool-call syntax nobody had taught the matcher — the fifth family to hit this after Devstral, Gemma-4, Laguna and LFM2.5. Its chat template defines an Onyx/ATEM shape (<atem:invoke name=...>) and its system prompt instructs the model to use it, so it emits that regardless of what the benchmark asks for. A parser gap reads as a model that cannot call tools at all.
  • Muse-Glimmer and LFM2.5-VL shipped with vision towers nothing could reach. Both were ported and matched to the reference (4e-07 and correlation 1.0) and neither could take an image: optiq/vlm/__init__.py never imported them, so get_frontend() returned None and every image request failed with "OptiQ has no vision front-end registered". They also implemented from_sidecar/image_features/merge rather than the from_pretrained/preprocess/merged_embeddings the engine calls, and from_sidecar had no callers anywhere. Both now implement the protocol and register on import optiq. The tower tests kept passing throughout because they construct the classes directly, so tests/test_vision_frontend_registry.py goes through the registry instead, and asserts a bare import optiq.vlm is enough.
  • A blocked tool call could burn an entire run. Telling a model "that tool is not available, do not call it again" is advice, not a bound: one refused call cost 40 turns and 150k tokens and produced nothing. Three refusals now stop the run with stop_reason=blocked_tool. The check also had to move ahead of the repeat-call dedup guard, which was swallowing the identical retries so the counter never advanced.
  • Eval extractors scored the reasoning trace on Onyx/ATEM channel models. They split on </think>, which those models never emit — the answer follows to=user<|message|> — so GSM8K, MMLU, IFEval and HumanEval parsed answers out of the chain of thought.
v0.4.19

Added

  • sandbox_python_image / sandbox_shell_image: the container images for the python and terminal tools were hardcoded. The container tier ships a bare python:3.11-slim with no numpy, pandas or matplotlib, while the macOS tiers run the user's own interpreter and inherit its packages, so the same tool has very different capability by platform and the docs promise inline matplotlib charts the container tier cannot produce. Point these at an image that carries what you need.
  • run_python(..., workdir=) and execute_tool(..., workdir=): a directory the python and terminal tools share and keep for a session.
  • OptiqEngine(path, stream_experts=..., cache_experts=N): SSD expert streaming from the library API, the same policy optiq serve applies.

Fixed

  • The sandbox forgot everything between calls. Each run_python call built a throwaway directory and deleted it on the way out, so a file written in one call was gone by the next. Invisible until an agent tries to build on prior work, then baffling: a real Lab agent loop wrote a JSON file, could not find it, wrote it again, and burned all eight turns doing that, with rc=0 reported every time. run_python(..., workdir=) and execute_tool(..., workdir=) now take a directory the python and terminal tools share and keep for the session. Default is unchanged and still throwaway.
  • run_terminal never resolved a caller-supplied cwd, so on macOS every write into it failed with "Operation not permitted": /var/folders is really /private/var/folders and the sandbox-exec profile compares the resolved path. run_python had always done this for its own temp dir. Nothing passed a cwd before, so nothing noticed.
  • OptiqEngine never streamed MoE experts. The wiring lived only in optiq serve, so the documented Python path for image input failed on exactly the models streaming exists for: a 27 GB 256-expert MoE loaded every expert resident, took a 36 GB Mac to 38 GB of swap and was killed, while the same artifact served fine under --stream-experts. should_stream() already returned True for it; nothing asked. OptiqEngine(path, stream_experts="auto"|"on"|"off", cache_experts=N) now applies the same policy serve does, default auto. Measured on Ornith-1.0-35B-OptiQ-6bit: 7.2 GB peak instead of 27 GB resident, same answer.
v0.4.18

Changed

  • Everything OptiQ-specific now lives in the quant's optiq/ subfolder. Sidecars moved there in 0.3.1 but optiq_metadata.json stayed at the root; it is optiq/metadata.json now, leaving the root for what the wider ecosystem reads (config, generation config, chat template, tokenizer, weights). Readers try the new path first and fall back to the old one, so the 20+ quants already published keep working and optiq lora train --rank-scaling by_bits still resolves them from the Hub.

Added

  • The sensitivity sweep ships with the quant, as optiq/sensitivity.json. Only the final bit map was published before, which reproduces that one quant and nothing else; re-aiming the same architecture at a different width needs the KL scores, and those were written to a scratch checkpoint and left behind. A 6-bit build of an already-measured 9B therefore cost a ten-hour re-sweep. The file is ~50 KB.
  • optiq/artifacts.py: one place that knows where OptiQ's files live, with the legacy fallback.
  • tests/test_artifact_layout.py: 19 tests over the layout, the fallback, and publish completeness (weights, metadata, and the sweep when one was run).

Fixed

  • A missing vision sidecar raised FileNotFoundError with only a path. Sidecars moved into optiq/ in 0.3.1, so a client older than that finds nothing at the root and the error explains none of it: a user publicly blamed mlx-optiq>=0.4.17 for a broken sidecar before finding they ran 0.2.18. The message now lists both locations, and says so plainly when the file is present under optiq/ but the running version cannot see it.
v0.4.17

Fixed

  • Lab sandbox gave no isolation on Linux and Windows. Tier detection looked only for Apple's container, so a box with Docker running executed model code as the real user. Docker and Podman are detected now. Verified on Linux: tier subprocess -> container, host home invisible, network denied.
  • A container CLI on PATH was assumed to be a running one. Probed once, cached.
  • Windows: /bin/sh and PATH=/usr/bin:/bin were hardcoded and import resource unguarded, so both tools failed outright. Shell and env are per-platform.
  • run_python and run_terminal had separate tier chains, so the reported tier could differ from the one that ran. Both dispatch on detect_sandbox_kind().
  • from optiq.sandbox import run_python raised ModuleNotFoundError: it re-exported a module that does not exist.
  • 27 settings were bare os.environ.get calls across 17 modules, some read at import time, so a config file could never win.
  • OPTIQ_HOME had five implementations that disagreed on expanduser(). OPTIQ_HOME=~/x could create a directory named ~.
  • ensure_server() ignored base_url and model from ~/.optiq/code/config.json.

Added

  • optiq config: every setting, its value, and which source won.
  • optiq/settings.py: one registry, flag > env > repo config > user config > default. Files are <repo>/.optiq/optiq.json and ~/.optiq/config.json; unknown keys warn.
  • OPTIQ_SANDBOX_CONTAINER: 0 disables the container tier, a name forces one.
  • The Lab warns when the active tier is subprocess.

Changed

  • macOS prefers sandbox-exec over Docker and Podman, which boot a VM per call. Apple container still ranks first.

Removed

  • Two Mage-VL torch oracles that shipped in every wheel importing [dev] deps, with a developer's paths baked in. Moved to tests/oracles/; retires OPTIQ_TORCH_PY.
v0.4.16

Changed

  • One install, one dev extra. pip install mlx-optiq ships every command: convert, serve, eval, lora, kv-cache, latency, lab, code, game. The lab, game, cli, convert, all, vlm and audio extras are gone, kept as empty aliases so old install lines still resolve. mlx-optiq[dev] carries the test suite, browser tests, benchmark datasets and the torch/mlx-vlm oracles.
  • Install footprint 985 MB -> 550 MB, shipping more by default than the old 591 MB base.
  • Linux and Windows work for the API-driven path. optiq code and optiq lab against an OpenAI-compatible base_url never import mlx.core; the OS classifiers say so now. Quantizing and local inference still need Apple Silicon.

Removed

  • scipy (98 MB) and data-designer (122 MB): declared core dependencies that nothing imported.
  • mlx-whisper: zero usages, audio pipelines are unsupported.
  • convert_vlm_to_mlx(): dead code, and the last runtime reference to mlx_vlm.

Fixed

  • datasets and langdetect moved to [dev], 217 MB reached only by optiq eval and HF calibration mixes. A missing one now names the extra instead of raising a bare ModuleNotFoundError.
  • Install hints in optiq lab / optiq game pointed at extras that are now empty aliases, so following them installed nothing.
  • The docs advertised mlx-optiq[serve] and mlx-optiq[eval], which have never existed.
  • pygame is core, so optiq game works from a plain install.
v0.4.15

Added

  • optiq game (hidden command): plays Neural Drive, a 131M diffusion world model that generates every frame of a racing game at 384x192, quantized to 86 MB with mixed 4/8-bit. Weights pull from mlx-community/neural-drive-optiq-mlx on first run; the window needs pip install 'mlx-optiq[game]'.
  • optiq.core.shardwise: requantize a checkpoint shard by shard without building the model. optiq convert instantiates a model to quantize it, which a 300B checkpoint cannot survive on consumer hardware; this holds one tensor at a time, so peak memory tracks the largest tensor rather than the model (~12 GB against a 167 GB source on DeepSeek-V4). Existed only as one-off scripts in scripts/, which do not ship. requantize_shardwise(src, out, policy=moe_bit_policy(...)).
  • moe_bit_policy(): the structural bit allocation for a large sparse MoE — routed experts lowest, attention and shared expert higher, router never quantized.
  • optiq game: hidden command. Drives Neural Drive, a 131M latent-diffusion world model quantized to 86 MB, which generates every frame from the last 7 plus your controls. Weights download from the Hub on first run.

Fixed

  • shardwise: a mixed-precision source is now read per layer. The config's global bits is only a default, and a mixed source carries per-layer overrides beside it (Hy3-Alis-MLX-Dynamic has 799). Reading only the global values fails on the first tensor that differs, which would have made every OptiQ quant unreadable by OptiQ's own requantizer.
v0.4.14

Added

  • Low-bit range-search encoder (optiq/core/lowbit.py), wired into optiq convert. Each group's quantization range is chosen by search instead of its raw min/max. Applies to affine layers at 2 and 3 bits by default; 4-bit and above are byte-identical to before. --low-bit-search/--no-low-bit-search, OPTIQ_LOWBIT_SEARCH, OPTIQ_LOWBIT_SEARCH_MAX_BITS.
  • hy_v3 (tencent/Hy3) architecture. Vendored from oMLX (Apache-2.0); stock mlx-lm ships hunyuan.py and hunyuan_v1_dense.py, neither of which is hy_v3.

Fixed

  • Sensitivity: MoE experts were silently excluded on the bf16-streaming path. A layer absent from the bf16 index is skipped, and LFM2.5-8B-A1B ships experts unfused as feed_forward.experts.{e}.{w1,w2,w3} against one fused switch_mlp. All 66 fused projections were skipped, never reached the allocator, and kept the default 8-bit: 8.38 bpw from a 4.5 target, twice the size of its own uniform-4-bit baseline. Now 5.15 bpw with experts at mixed precision. Affects any MoE whose source stores experts per-expert under feed_forward.
  • Sensitivity: param_count measured the packed weight when the reference model is already quantized, understating quantized layers by 32/bits.
v0.4.13

Fixed

  • Sensitivity: param_count measured the packed weight when the reference model is already quantized, understating every quantized layer by 32/bits. The allocator then spent the budget on them. LFM2.5-8B-A1B came out at 8.38 bpw from a 4.5 target, twice the size of its uniform-4-bit baseline, while reporting 5.17. Affects --reference uniform_4bit on architectures that mix quantized and unquantized layers.
v0.4.12

Added

  • DeepSeek-V4 family (deepseek_v4): Flash/Pro architecture with per-layer Local / Compressed / SparseCompressed attention, hyper-connections and DeepSeekMoE. Vendored from mlx-lm PR #1192; not merged upstream.
  • LFM2.5 (Liquid AI) family quants: 230M, 350M, 1.2B-Instruct, 1.2B-Thinking (Capability Score 54.14), 1.2B-JP-202606, 2.6B (Capability Score 35.19).
  • deepseek_v4: chat template ported from the base repo's encoding_dsv4.py, written by the converter when the source ships none. Pinned against DeepSeek's own encoder.
  • Sensitivity: the uniform-4-bit reference can stream off SSD, so it need not fit in RAM. OPTIQ_STREAM_REFERENCE=1|0 forces either path.
  • IFEval reports instruction-level accuracy alongside prompt-level.
  • optiq code: token usage reported, including cache hits and reasoning tokens.
  • optiq convert: streaming convert for fp8/large MoE sources, allocation-aware and one block at a time.
  • optiq code: replace_lines tool, addressing an edit by the line numbers read_file prints so only changed lines are sent.
  • optiq code: read_file numbers its lines and its header carries a content tag ([name#a1b2: lines A-B of N]); pass the tag to replace_lines and a stale edit is rejected.
  • optiq code: edit_file recovers from an anchor differing only in trailing whitespace, indentation, quote style, or copied N: prefixes. Every rung still needs exactly one matching site.
  • optiq code: the stream is cut when a model starts writing its own tool result, and the fabricated text discarded.
  • optiq code: nine harness thresholds are now config fields. 21 settings to 29.
  • optiq.toolcalls: one tool-call parser shared by the BFCL eval and the agent loop, covering all eight family formats.

Fixed

  • IFEval's loose metric was a second strict score: it cleaned the response once and scored all-or-nothing. Now builds the published eight response variants and passes an instruction if any satisfies it. Capability Score is unaffected (it reads prompt-level strict).
  • Eval ran LFM2.5 without the repetition_penalty 1.1 its model card and generation_config.json both specify. Both lfm2 and lfm2_moe now carry it.
  • optiq convert: the source's generation_config.json keys are preserved for every model, not just three hardcoded families. LFM2.5's do_sample was being dropped.
  • optiq code: the agent loop recognised only fenced JSON tool calls, so the Pythonic, Gemma-4, Mistral and Laguna formats the eval already handled were invisible to it. It now recognises all of them, and more than one call per turn.
  • optiq code: compaction could grow the context, because a dropped tool result was replaced by a placeholder longer than many results. Added a size floor and shortened the placeholder.
  • optiq code: _solution_sig hashed *.py only, so every grounding mechanism silently switched off on a JS/Go/Rust repo.
  • optiq code: repeated edit_file failures nudged a full-file rewrite, which fed straight into output-token truncation. The nudge now points at replace_lines.
  • optiq code: empty tool results are compacted first and not protected by recency.
  • optiq code: search uses ripgrep when installed, as its comment had long claimed.
  • optiq code: context_window() no longer raises on a client built without a server.
  • optiq convert: a VLM whose weight prefix is missing from _MM_PREFIXES no longer ships as a silently text-only quant with its vision_config stripped. Weights that are neither a known tower nor a language weight now raise and name the prefix to add.
  • optiq convert: the IncompleteSnapshotError fallback added in 0.4.8 was unreachable; a local_files_only call bypassed the wrapper entirely.
  • tests/model_matrix.py: mistral, laguna, nanbeige and mage-vl had docs pages but were never added, so every eval sweep silently skipped them. Both Nemotron 3 base ids were missing their -BF16 suffix and did not resolve.
v0.4.11

Fixed

  • optiq code: auto_approve was a CLI-only parameter. -y reached the TUI directly and never went through load(), so it could not be set in config.json or the environment, while every other setting obeys flag > env > repo > user > default. Unattended drivers had no way to say "don't stop and ask", so the TUI halted before every write_file/edit_file/bash waiting for a keypress that never came: the model kept talking (four turns, 22.6k tokens in the case that found it) while not one file was written, which reads exactly like a hung generation. ApprovalPolicy.gated had the same shape and is now gated_tools, so the middle ground is reachable (set it to bash to let edits through but still ask before running commands). A bare invocation passes None rather than False, so the flag cannot stamp over a config that asked for auto-approval.
  • optiq code: guidance truncation was silent. fh.read(8000) cut AGENTS.md/CLAUDE.md mid-sentence and said nothing, and since guidance is prepended to every run, a long file lost its tail on every turn forever with no way for the author to notice. It now returns the overflow alongside the text and the loop logs it. 8000 chars stays the default: guidance costs ~2k tokens on every request, about 6% of a 32k local window, so the cap earns its place and the fix is telling you, not raising it blindly.
  • optiq code: four values behaved like settings but could not be set (server_ready_timeout, server_host, server_port, and the guidance cap). All four now go through the same config chain, taking the surface from 16 fields to 21. server_ready_timeout matters most: a large model loading cold can exceed 180s, and that failure reads as a broken install rather than a slow one.
v0.4.10

Fixed

  • Licences for vendored third-party code were missing from the wheel. The package ships a trimmed subset of mlx-vlm (27 files, MIT, (c) 2025 Prince Canuma) and MTPLX (112 files, (c) 2026 Youssof Altoukhi), but [tool.setuptools.package-data] listed only data extensions, so setuptools silently dropped vlm/_mlxvlm/LICENSE, runtime/mtp/LICENSE.MTPLX and runtime/mtp/NOTICE.MTPLX while happily including the code they cover. Both licences require the notice travel with the source. Present from 0.4.10; absent in every release up to 0.4.9.
  • OptiQ's own LICENSE file did not exist. pyproject.toml declared MIT and the classifier claimed MIT, but no licence text shipped anywhere, so the wheel asserted a licence it never included. Added at the repo root, wired in via license-files, and it now names the vendored third-party components.
v0.4.9

Added

  • OptiQ Lab chat: video attach. A clip is sampled into frames in the browser (<video> + canvas, no upload and no ffmpeg) and sent as image parts, which is how a VLM reads video. Defaults: 8 frames per clip, frames capped at 640 px on the long edge (~1.9k vision tokens for a 16:9 clip), stills downscaled to 1280 px, 16 images per turn, 512 MB per file. The composer shows the estimated vision-token cost before you send.

Fixed

  • Image and video through optiq serve crashed for Mage-VL (AttributeError: 'Model' object has no attribute 'language_model'), so the documented image/video path was broken for that family in both the CLI server and the Lab. OptiqEngine._init_from_runtime assumed every VLM nests its text tower under .language_model; Mage-VL's adapter flattens it (sanitize remaps model.language_model.* -> model.*), so the loaded model is the language model. Now falls back to the model itself. Regression test in tests/test_engine_flat_vlm.py.

Changed

  • optiq code: max_retries 6 -> 3 and request_timeout 900s -> 1200s. The two multiply: the SDK retries after the first attempt, so the old pair allowed 7 x 900s = 105 minutes of retrying, with nothing logged in between. A server whose generation thread has died (a Metal OOM leaves the process up and /v1/models still answering 200) was therefore indistinguishable from a hang -- observed as a 95-minute stall on a 24 GB M4. The new pair bounds the worst case at 80 min while raising the per-turn ceiling to 20 min, which a 26B on that machine needs: a single agent turn has taken ~840s.
v0.4.8

Added

  • Mage-VL VLM family (video + image): microsoft/Mage-VL, a Mage-ViT vision tower + Qwen3-4B LM. New optiq/vlm/mage_vl/ (MLX vision tower validated bit-exact vs torch, Qwen2VL-style preprocessing, frontend) and the mage_vl -> qwen3 LM adapter. First video model in the lineup; video is uniform sampled frames (the base repo's DCVC neural codec is optional and not required). Capability Score 70.27.

Fixed

  • optiq code: every entry point now defaults max_tokens to 16384. run_agent, run_headless, Engine.chat and the TUI each hardcoded 8192, so a direct call got half the configured budget; all now import DEFAULT_MAX_TOKENS from config.
  • optiq convert: a finished quant no longer aborts at the final save with IncompleteSnapshotError. The lean download skips non-weight files (.gitattributes, asset images, config backups) that mlx-lm's strict local_files_only re-resolve then rejected. The download wrapper completes the snapshot on demand, and convert now installs it. First hit on a Qwen3.5-VLM (Qwythos-27B).
  • BFCL eval: models whose chat template silently drops the tools= argument scored 0 (no_call, the model never saw the functions). The eval now detects the missing tools in the rendered prompt and injects them into the system message with the call format. Mage-VL went 0 -> 88.5%.