v0.3.0
First multi-backend release. Magi can now talk to any OpenAI-compatible Chat
Completions endpoint — local Ollama, OpenAI, Groq, OpenRouter — selected by
a new magi.toml, alongside the existing Anthropic Messages API path. No
regression: the Anthropic surface and all its tests are byte-equivalent.
Added
OpenAiCompatibleProvider(src/agent/provider.rs) — Chat Completions over{base_url}/chat/completionswithstream:true, astream::unfoldSSE state machine that finalizes onfinish_reason/[DONE]/ stream-end with an idempotentdoneguard (MessageDoneemitted exactly once),data:prefix tolerant of optional space, malformed lines swallowed, HTTP non-2xx surfaced asErr,MAX_SSE_BUFFER_BYTES(8 MiB) cap. Constructor takes a named-fieldOpenAiSettingsstruct (no positional same-type swap).magi.tomlconfiguration (src/config.rs) —MagiConfig/OpenAiConfig/AnthropicConfig,serde(deny_unknown_fields)so typos (andapi_key) fail at parse time, env > TOML > defaults precedence (MAGI_PROVIDER/OPENAI_BASE_URL/OPENAI_MODEL).MagiConfig::loaddistinguishesNotFound(silent default) from other I/O errors (surfaced as a TUI startup notice). Referencemagi.toml.exampleis committed; user-localmagi.tomlis gitignored.- Coalesced
map_messages/map_tools. Same-turn parallelContent::ToolUseblocks collapse into ONE assistant message with atool_callsarray; UserText/ToolResultare emitted in content order (each block as its own OpenAI message).Tool::input_schemaforwards astools:[{type:"function",function:{…}}]. - Bounded tool-call accumulator.
MAX_TOOL_CALL_SLOTS = 64caps streamedtool_calls[].index; over-cap warns (eprintln) instead of dropping silently (RF-8). Orphanargumentsfragments (slot has neither id nor name yet) are skipped with a warning to prevent mis-attribution. tests-121(was 95). 26 new tests — config parsing & precedence; coalesced message mapping; mixed-content User ordering; OpenAI text streaming (with [DONE], without [DONE]/finish_reason, malformed line, HTTP error, stream-end-only finalize); fragmentedtool_callsassembly; bounded index; post-stop TextDelta suppression; args-before-id skip;resolve_providerwiring.
Security
OPENAI_API_KEYfrom environment only. Never read frommagi.toml;deny_unknown_fieldsrejects anapi_keyfield at parse time. The dummy"ollama"fallback for local Ollama is documented inline; real backends fail loudly with 401 if the env var is unset.- Keyring separation unchanged. The Anthropic key (
magi-rs) and DB master key (magi-rs-internal) remain in separate keyring services;test_agent_history_resilience_to_key_rotationis untouched.
Documentation
- README "Configuration:
magi.toml" section — full precedence table for the four settings (MAGI_PROVIDER/OPENAI_BASE_URL/OPENAI_MODEL/ANTHROPIC_MODEL), keys-never-in-TOML invariant (and thedeny_unknown_fieldsparse-error consequence), Ollama quickstart. reqwest::Clientno-timeout rationale documented — local Ollama can spend tens of seconds on cold-load before the first SSE event; a total-request timeout would truncate healthy long streams. Stream-side termination is handled by the three finalize triggers +MAX_SSE_BUFFER_BYTES.
Full Changelog: v0.2.1...v0.3.0