Skip to content

URSA-OSCAR 1.1.4 — Local-model malformed-tool-call diagnostic

Choose a tag to compare

@burrellka burrellka released this 24 May 17:43
· 23 commits to main since this release

URSA-OSCAR 1.1.4

Two production-debugged data fixes, one architectural cleanup, and a UX polish for local-model users. The full 1.1.2 → 1.1.4 changeset (a separate 1.1.3 Release was skipped; Docker Hub :1.1.3 images still work for anyone who pulled them; this Release supersedes them).

Fixed in 1.1.4

  • Malformed-tool-call diagnostic for under-capable local models (backend/src/ursa_oscar/api/ai.py). When a local model isn't capable enough to format an OpenAI-style tool call against URSA's full 18-tool surface (Qwen3-4b on CPU is the canonical example), it typically emits {" as text content trying to write a JSON tool-call, then finishes with stop_reason="stop". The chat panel previously rendered the partial JSON literally, leaving the user staring at a confusing single {. The chat handler now detects this shape (text content ≤10 chars starting with {, stop_reason=stop, no tool_calls) and surfaces a friendly diagnostic error with concrete next steps: switch to Claude API, use a larger local model (Qwen3-30b-instruct, Llama-3.3-70b-instruct), or run on GPU instead of CPU. Two regression tests cover the detection firing for the failure shape and not misfiring on legitimate short responses like Yes..

Fixed in 1.1.3

  • Session boundary detection no longer splits on clock-minute boundaries (backend/src/ursa_oscar/analytics/edf_parser.py). The EDF importer grouped files into sessions by hour-and-minute filename prefix. When a session's events file landed at 01:04:53 and the waveforms file landed at 01:05:00 (7 seconds apart but crossing the minute mark), they were treated as two separate sessions: one events-only, one waveforms-only. Mask-on duration inflated by the length of the affected session. Replaced with sliding-window temporal clustering at 30-second tolerance. Operators with affected nights must force re-import to pick up the corrected session boundaries via URSA_OSCAR_WATCH_FORCE_REIMPORT=true and a watcher recreate. Three regression tests cover the minute-boundary case, the 50-second-real-restart case, and the multi-minute-separation case.

  • Thinking-mode model support in the OpenAI-compat adapter (backend/src/ursa_oscar/ai_proxy/providers/openai_compat.py, frontend/src/components/AiChatPanel.tsx). Qwen3 (via LocalAI / Ollama) emits chain-of-thought in delta.reasoning; DeepSeek-R1 uses delta.reasoning_content. Both naming conventions are now read and surfaced as a new reasoning event type. The chat panel renders these as a collapsible "Reasoning" trail above the assistant's content, open by default while in-flight, collapsed once the answer arrives. Reasoning content stays out of the assistant message's content field so follow-up turns don't carry the chain-of-thought back to the LLM. Stream timeout bumped from 120s to 300s on the read path.

  • Settings page image-version chips are now self-introspecting, eliminating the dual-update problem (backend/src/ursa_oscar/api/system.py, mcp-server/src/ursa_oscar_mcp/__main__.py, watcher/src/ursa_oscar_watcher/__main__.py, frontend/vite.config.ts). Previously the chips read from URSA_OSCAR_*_IMAGE_VERSION env vars that the operator had to keep in sync with the image: lines in their compose file. Now each service is the source of truth for its own version: API reads importlib.metadata.version('ursa-oscar-backend'); MCP exposes an unauthenticated /version endpoint the API queries; the watcher writes /data/versions/watcher.txt at startup; the web bundle bakes __URSA_WEB_VERSION__ via Vite at build time. Env-var overrides are retained for testing scenarios but the common case requires no operator coordination. 11 regression tests cover the introspection helpers.

Fixed in 1.1.2

  • Test connection button reflects the saved config, not in-flight edits (frontend/src/pages/SettingsAi.tsx). The button used to enable the moment the operator typed an API key, but the /ai/test endpoint probes the persisted config. Five disabled states now, each with a tooltip naming the missing piece. Local LLM presets skip the key requirement.

  • Gemini provider preset refreshed (backend/src/ursa_oscar/ai_proxy/providers/presets.py). The default model list shipped with gemini-2.0-flash-exp and gemini-1.5-pro (both deprecated by Google as of May 2026) and gemini-1.5-flash (on the deprecation track). Replaced with gemini-3.5-flash. Also rolled OpenRouter's stale google/gemini-2.0-flash to google/gemini-3.5-flash.

README addition

The README now includes a section on the AI value proposition: the MCP-exposed-data-plus-specialized-AI-conversation pattern that motivated URSA in the first place, and the in-app AI assistant as the fallback path for deployments that can't expose a public MCP endpoint. The reusable architectural pattern is linked to the new template repo at https://github.com/burrellka/mcp-server-template.

Re-import note for operators

If your URSA installation has nights where mask-on duration looks inflated (typically 10+ hours when the real value should be 6-7 hours), at least one of those nights hit the minute-boundary bug. The fix corrects future imports automatically; existing nights need a force re-import to rewrite session boundaries.

Easiest path:

  1. In your compose env, set URSA_OSCAR_WATCH_FORCE_REIMPORT=true
  2. docker compose up -d --force-recreate ursa-oscar-watcher
  3. Wait one watcher poll cycle (default 30 seconds). The watcher re-imports every night present on the SD card source.
  4. Set URSA_OSCAR_WATCH_FORCE_REIMPORT=false again (or remove the override) and recreate the watcher.

The re-import is idempotent for nights not affected by the bug; nights with the corrupted session pair get rewritten with the corrected single-session shape.

No data migrations

No schema changes. No env-var changes. Same /data volume layout. Upgrade is docker compose pull && docker compose up -d --force-recreate.

Architecturally locked in 1.0 (carried into 1.1.x)

  • Single-tenant. One operator, one instance.
  • Self-hosted. No cloud sync, no SaaS option.
  • Password authentication only. No SSO / OAuth providers for operator login.
  • No email password recovery. Recovery is filesystem-level (delete auth.json, re-bootstrap).
  • Statistical method declaration in every analytical response.
  • Sample-size discipline as refusals, not warnings (INSUFFICIENT_DATA returns when below the floor).

Docker images

All four images are available at brain40/ursa-oscar-*:1.1.4 + :latest:

  • brain40/ursa-oscar-api:1.1.4
  • brain40/ursa-oscar-mcp:1.1.4
  • brain40/ursa-oscar-web:1.1.4
  • brain40/ursa-oscar-watcher:1.1.4

Upgrade

Operators on 1.0.x, 1.1.0, 1.1.1, 1.1.2, or 1.1.3: bump the four image tags to 1.1.4 and docker compose up -d --force-recreate. No data migrations. After upgrade, follow the Re-import note above if you have affected nights from the session-boundary bug.

Test coverage

  • Backend: 380+ tests pass (including 53/53 AI proxy, 9/9 EDF parser with new regression coverage, 11/11 version-introspection)
  • MCP: 40+ tests pass
  • Watcher: 25+ tests pass
  • Frontend: tsc -b && vite build clean

License

GNU GPL-3.0-or-later, preserving OSCAR's license terms.

Attribution

Built in the spirit and license terms of the OSCAR project. URSA-OSCAR is independent of OSCAR (different codebase, different deployment model) but downstream of OSCAR's file-format work.