dulus 0.2.12__tar.gz → 0.2.13__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. {dulus-0.2.12/dulus.egg-info → dulus-0.2.13}/PKG-INFO +2 -2
  2. {dulus-0.2.12 → dulus-0.2.13}/README.md +1 -1
  3. {dulus-0.2.12 → dulus-0.2.13/dulus.egg-info}/PKG-INFO +2 -2
  4. {dulus-0.2.12 → dulus-0.2.13}/providers.py +141 -4
  5. {dulus-0.2.12 → dulus-0.2.13}/pyproject.toml +1 -1
  6. {dulus-0.2.12 → dulus-0.2.13}/LICENSE +0 -0
  7. {dulus-0.2.12 → dulus-0.2.13}/MANIFEST.in +0 -0
  8. {dulus-0.2.12 → dulus-0.2.13}/agent.py +0 -0
  9. {dulus-0.2.12 → dulus-0.2.13}/backend/__init__.py +0 -0
  10. {dulus-0.2.12 → dulus-0.2.13}/backend/compressor.py +0 -0
  11. {dulus-0.2.12 → dulus-0.2.13}/backend/context.py +0 -0
  12. {dulus-0.2.12 → dulus-0.2.13}/backend/githook.py +0 -0
  13. {dulus-0.2.12 → dulus-0.2.13}/backend/marketplace.py +0 -0
  14. {dulus-0.2.12 → dulus-0.2.13}/backend/mempalace_bridge.py +0 -0
  15. {dulus-0.2.12 → dulus-0.2.13}/backend/personas.py +0 -0
  16. {dulus-0.2.12 → dulus-0.2.13}/backend/plugins.py +0 -0
  17. {dulus-0.2.12 → dulus-0.2.13}/backend/server.py +0 -0
  18. {dulus-0.2.12 → dulus-0.2.13}/backend/tasks.py +0 -0
  19. {dulus-0.2.12 → dulus-0.2.13}/batch_api.py +0 -0
  20. {dulus-0.2.12 → dulus-0.2.13}/checkpoint/__init__.py +0 -0
  21. {dulus-0.2.12 → dulus-0.2.13}/checkpoint/hooks.py +0 -0
  22. {dulus-0.2.12 → dulus-0.2.13}/checkpoint/store.py +0 -0
  23. {dulus-0.2.12 → dulus-0.2.13}/checkpoint/types.py +0 -0
  24. {dulus-0.2.12 → dulus-0.2.13}/claude_code_watcher.py +0 -0
  25. {dulus-0.2.12 → dulus-0.2.13}/clipboard_utils.py +0 -0
  26. {dulus-0.2.12 → dulus-0.2.13}/cloudsave.py +0 -0
  27. {dulus-0.2.12 → dulus-0.2.13}/common.py +0 -0
  28. {dulus-0.2.12 → dulus-0.2.13}/compaction.py +0 -0
  29. {dulus-0.2.12 → dulus-0.2.13}/config.py +0 -0
  30. {dulus-0.2.12 → dulus-0.2.13}/context.py +0 -0
  31. {dulus-0.2.12 → dulus-0.2.13}/data/__init__.py +0 -0
  32. {dulus-0.2.12 → dulus-0.2.13}/data/active_persona.json +0 -0
  33. {dulus-0.2.12 → dulus-0.2.13}/data/context.json +0 -0
  34. {dulus-0.2.12 → dulus-0.2.13}/data/marketplace.json +0 -0
  35. {dulus-0.2.12 → dulus-0.2.13}/data/personas.json +0 -0
  36. {dulus-0.2.12 → dulus-0.2.13}/data/tasks.json +0 -0
  37. {dulus-0.2.12 → dulus-0.2.13}/docs/README.md +0 -0
  38. {dulus-0.2.12 → dulus-0.2.13}/docs/__init__.py +0 -0
  39. {dulus-0.2.12 → dulus-0.2.13}/docs/api.html +0 -0
  40. {dulus-0.2.12 → dulus-0.2.13}/docs/architecture.md +0 -0
  41. {dulus-0.2.12 → dulus-0.2.13}/docs/azure-speech-template.json +0 -0
  42. {dulus-0.2.12 → dulus-0.2.13}/docs/dashboard/index.html +0 -0
  43. {dulus-0.2.12 → dulus-0.2.13}/docs/divider.svg +0 -0
  44. {dulus-0.2.12 → dulus-0.2.13}/docs/generate.py +0 -0
  45. {dulus-0.2.12 → dulus-0.2.13}/docs/hero.svg +0 -0
  46. {dulus-0.2.12 → dulus-0.2.13}/docs/index.html +0 -0
  47. {dulus-0.2.12 → dulus-0.2.13}/docs/news.md +0 -0
  48. {dulus-0.2.12 → dulus-0.2.13}/docs/nvidia-models.svg +0 -0
  49. {dulus-0.2.12 → dulus-0.2.13}/docs/particle-playground.html +0 -0
  50. {dulus-0.2.12 → dulus-0.2.13}/docs/personas/index.html +0 -0
  51. {dulus-0.2.12 → dulus-0.2.13}/docs/preview.html +0 -0
  52. {dulus-0.2.12 → dulus-0.2.13}/docs/sec-agents.svg +0 -0
  53. {dulus-0.2.12 → dulus-0.2.13}/docs/sec-brainstorm.svg +0 -0
  54. {dulus-0.2.12 → dulus-0.2.13}/docs/sec-bridges.svg +0 -0
  55. {dulus-0.2.12 → dulus-0.2.13}/docs/sec-features.svg +0 -0
  56. {dulus-0.2.12 → dulus-0.2.13}/docs/sec-freetier.svg +0 -0
  57. {dulus-0.2.12 → dulus-0.2.13}/docs/sec-memory.svg +0 -0
  58. {dulus-0.2.12 → dulus-0.2.13}/docs/sec-models.svg +0 -0
  59. {dulus-0.2.12 → dulus-0.2.13}/docs/sec-perms.svg +0 -0
  60. {dulus-0.2.12 → dulus-0.2.13}/docs/sec-plugins.svg +0 -0
  61. {dulus-0.2.12 → dulus-0.2.13}/docs/sec-quickstart.svg +0 -0
  62. {dulus-0.2.12 → dulus-0.2.13}/docs/sec-ssj.svg +0 -0
  63. {dulus-0.2.12 → dulus-0.2.13}/docs/spinners.svg +0 -0
  64. {dulus-0.2.12 → dulus-0.2.13}/docs/split-pane.svg +0 -0
  65. {dulus-0.2.12 → dulus-0.2.13}/docs/terminal-boot.svg +0 -0
  66. {dulus-0.2.12 → dulus-0.2.13}/docs/uploads/particle-playground.html +0 -0
  67. {dulus-0.2.12 → dulus-0.2.13}/dulus.egg-info/SOURCES.txt +0 -0
  68. {dulus-0.2.12 → dulus-0.2.13}/dulus.egg-info/dependency_links.txt +0 -0
  69. {dulus-0.2.12 → dulus-0.2.13}/dulus.egg-info/entry_points.txt +0 -0
  70. {dulus-0.2.12 → dulus-0.2.13}/dulus.egg-info/requires.txt +0 -0
  71. {dulus-0.2.12 → dulus-0.2.13}/dulus.egg-info/top_level.txt +0 -0
  72. {dulus-0.2.12 → dulus-0.2.13}/dulus.py +0 -0
  73. {dulus-0.2.12 → dulus-0.2.13}/dulus_gui.py +0 -0
  74. {dulus-0.2.12 → dulus-0.2.13}/dulus_mcp/__init__.py +0 -0
  75. {dulus-0.2.12 → dulus-0.2.13}/dulus_mcp/client.py +0 -0
  76. {dulus-0.2.12 → dulus-0.2.13}/dulus_mcp/config.py +0 -0
  77. {dulus-0.2.12 → dulus-0.2.13}/dulus_mcp/tools.py +0 -0
  78. {dulus-0.2.12 → dulus-0.2.13}/dulus_mcp/types.py +0 -0
  79. {dulus-0.2.12 → dulus-0.2.13}/gui/__init__.py +0 -0
  80. {dulus-0.2.12 → dulus-0.2.13}/gui/agent_bridge.py +0 -0
  81. {dulus-0.2.12 → dulus-0.2.13}/gui/chat_widget.py +0 -0
  82. {dulus-0.2.12 → dulus-0.2.13}/gui/main_window.py +0 -0
  83. {dulus-0.2.12 → dulus-0.2.13}/gui/personas.py +0 -0
  84. {dulus-0.2.12 → dulus-0.2.13}/gui/session_utils.py +0 -0
  85. {dulus-0.2.12 → dulus-0.2.13}/gui/settings_dialog.py +0 -0
  86. {dulus-0.2.12 → dulus-0.2.13}/gui/sidebar.py +0 -0
  87. {dulus-0.2.12 → dulus-0.2.13}/gui/tasks_view.py +0 -0
  88. {dulus-0.2.12 → dulus-0.2.13}/gui/themes.py +0 -0
  89. {dulus-0.2.12 → dulus-0.2.13}/gui/tool_panel.py +0 -0
  90. {dulus-0.2.12 → dulus-0.2.13}/input.py +0 -0
  91. {dulus-0.2.12 → dulus-0.2.13}/license_manager.py +0 -0
  92. {dulus-0.2.12 → dulus-0.2.13}/memory/__init__.py +0 -0
  93. {dulus-0.2.12 → dulus-0.2.13}/memory/audit.py +0 -0
  94. {dulus-0.2.12 → dulus-0.2.13}/memory/consolidator.py +0 -0
  95. {dulus-0.2.12 → dulus-0.2.13}/memory/context.py +0 -0
  96. {dulus-0.2.12 → dulus-0.2.13}/memory/offload.py +0 -0
  97. {dulus-0.2.12 → dulus-0.2.13}/memory/palace.py +0 -0
  98. {dulus-0.2.12 → dulus-0.2.13}/memory/scan.py +0 -0
  99. {dulus-0.2.12 → dulus-0.2.13}/memory/sessions.py +0 -0
  100. {dulus-0.2.12 → dulus-0.2.13}/memory/store.py +0 -0
  101. {dulus-0.2.12 → dulus-0.2.13}/memory/tools.py +0 -0
  102. {dulus-0.2.12 → dulus-0.2.13}/memory/types.py +0 -0
  103. {dulus-0.2.12 → dulus-0.2.13}/memory/vector_search.py +0 -0
  104. {dulus-0.2.12 → dulus-0.2.13}/multi_agent/__init__.py +0 -0
  105. {dulus-0.2.12 → dulus-0.2.13}/multi_agent/subagent.py +0 -0
  106. {dulus-0.2.12 → dulus-0.2.13}/multi_agent/tools.py +0 -0
  107. {dulus-0.2.12 → dulus-0.2.13}/offload_helper.py +0 -0
  108. {dulus-0.2.12 → dulus-0.2.13}/plugin/__init__.py +0 -0
  109. {dulus-0.2.12 → dulus-0.2.13}/plugin/autoadapter.py +0 -0
  110. {dulus-0.2.12 → dulus-0.2.13}/plugin/loader.py +0 -0
  111. {dulus-0.2.12 → dulus-0.2.13}/plugin/recommend.py +0 -0
  112. {dulus-0.2.12 → dulus-0.2.13}/plugin/store.py +0 -0
  113. {dulus-0.2.12 → dulus-0.2.13}/plugin/types.py +0 -0
  114. {dulus-0.2.12 → dulus-0.2.13}/setup.cfg +0 -0
  115. {dulus-0.2.12 → dulus-0.2.13}/skill/__init__.py +0 -0
  116. {dulus-0.2.12 → dulus-0.2.13}/skill/builtin.py +0 -0
  117. {dulus-0.2.12 → dulus-0.2.13}/skill/clawhub.py +0 -0
  118. {dulus-0.2.12 → dulus-0.2.13}/skill/executor.py +0 -0
  119. {dulus-0.2.12 → dulus-0.2.13}/skill/loader.py +0 -0
  120. {dulus-0.2.12 → dulus-0.2.13}/skill/tools.py +0 -0
  121. {dulus-0.2.12 → dulus-0.2.13}/skills.py +0 -0
  122. {dulus-0.2.12 → dulus-0.2.13}/spinner.py +0 -0
  123. {dulus-0.2.12 → dulus-0.2.13}/string_utils.py +0 -0
  124. {dulus-0.2.12 → dulus-0.2.13}/subagent.py +0 -0
  125. {dulus-0.2.12 → dulus-0.2.13}/task/__init__.py +0 -0
  126. {dulus-0.2.12 → dulus-0.2.13}/task/store.py +0 -0
  127. {dulus-0.2.12 → dulus-0.2.13}/task/tools.py +0 -0
  128. {dulus-0.2.12 → dulus-0.2.13}/task/types.py +0 -0
  129. {dulus-0.2.12 → dulus-0.2.13}/tests/test_checkpoint.py +0 -0
  130. {dulus-0.2.12 → dulus-0.2.13}/tests/test_compaction.py +0 -0
  131. {dulus-0.2.12 → dulus-0.2.13}/tests/test_diff_view.py +0 -0
  132. {dulus-0.2.12 → dulus-0.2.13}/tests/test_injection_fix.py +0 -0
  133. {dulus-0.2.12 → dulus-0.2.13}/tests/test_license.py +0 -0
  134. {dulus-0.2.12 → dulus-0.2.13}/tests/test_mcp.py +0 -0
  135. {dulus-0.2.12 → dulus-0.2.13}/tests/test_memory.py +0 -0
  136. {dulus-0.2.12 → dulus-0.2.13}/tests/test_plugin.py +0 -0
  137. {dulus-0.2.12 → dulus-0.2.13}/tests/test_skills.py +0 -0
  138. {dulus-0.2.12 → dulus-0.2.13}/tests/test_subagent.py +0 -0
  139. {dulus-0.2.12 → dulus-0.2.13}/tests/test_task.py +0 -0
  140. {dulus-0.2.12 → dulus-0.2.13}/tests/test_telegram_buffer.py +0 -0
  141. {dulus-0.2.12 → dulus-0.2.13}/tests/test_tool_registry.py +0 -0
  142. {dulus-0.2.12 → dulus-0.2.13}/tests/test_voice.py +0 -0
  143. {dulus-0.2.12 → dulus-0.2.13}/tmux_offloader.py +0 -0
  144. {dulus-0.2.12 → dulus-0.2.13}/tmux_tools.py +0 -0
  145. {dulus-0.2.12 → dulus-0.2.13}/tool_registry.py +0 -0
  146. {dulus-0.2.12 → dulus-0.2.13}/tools.py +0 -0
  147. {dulus-0.2.12 → dulus-0.2.13}/ui/__init__.py +0 -0
  148. {dulus-0.2.12 → dulus-0.2.13}/ui/input.py +0 -0
  149. {dulus-0.2.12 → dulus-0.2.13}/ui/render.py +0 -0
  150. {dulus-0.2.12 → dulus-0.2.13}/voice/__init__.py +0 -0
  151. {dulus-0.2.12 → dulus-0.2.13}/voice/keyterms.py +0 -0
  152. {dulus-0.2.12 → dulus-0.2.13}/voice/recorder.py +0 -0
  153. {dulus-0.2.12 → dulus-0.2.13}/voice/stt.py +0 -0
  154. {dulus-0.2.12 → dulus-0.2.13}/voice/tts.py +0 -0
  155. {dulus-0.2.12 → dulus-0.2.13}/webchat.py +0 -0
  156. {dulus-0.2.12 → dulus-0.2.13}/webchat_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.12
3
+ Version: 0.2.13
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -90,7 +90,7 @@ SET /sticky_input ON since the first run for the best experience!
90
90
 
91
91
  Dulus is a **lightweight Python reimplementation of Claude Code** that isn't locked to Claude. It ships the whole loop — REPL, tool dispatch, streaming, context compaction, checkpoints, sub-agents, voice, Telegram bridge, MCP, plugins — in roughly **12K lines you can actually read**. Fork it. Bend it. Run it offline against Qwen on your M2.
92
92
 
93
- > **v0.2.12 — May 8, 2026** — `sounddevice` also moved to `[voice]` extra (it pulls `cffi`, which needs `libffi-dev` to compile and breaks termux installs). Core `pip install dulus` is now zero-compile. Opt in to extras: `[memory]` for MemPalace, `[voice]` for mic input, `[full]` for both.
93
+ > **v0.2.13 — May 8, 2026** — Internal robustness fixes for Ollama streaming. No user-facing API changes.
94
94
  > Type `/news` to see what changed.
95
95
 
96
96
  ---
@@ -45,7 +45,7 @@ SET /sticky_input ON since the first run for the best experience!
45
45
 
46
46
  Dulus is a **lightweight Python reimplementation of Claude Code** that isn't locked to Claude. It ships the whole loop — REPL, tool dispatch, streaming, context compaction, checkpoints, sub-agents, voice, Telegram bridge, MCP, plugins — in roughly **12K lines you can actually read**. Fork it. Bend it. Run it offline against Qwen on your M2.
47
47
 
48
- > **v0.2.12 — May 8, 2026** — `sounddevice` also moved to `[voice]` extra (it pulls `cffi`, which needs `libffi-dev` to compile and breaks termux installs). Core `pip install dulus` is now zero-compile. Opt in to extras: `[memory]` for MemPalace, `[voice]` for mic input, `[full]` for both.
48
+ > **v0.2.13 — May 8, 2026** — Internal robustness fixes for Ollama streaming. No user-facing API changes.
49
49
  > Type `/news` to see what changed.
50
50
 
51
51
  ---
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.12
3
+ Version: 0.2.13
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -90,7 +90,7 @@ SET /sticky_input ON since the first run for the best experience!
90
90
 
91
91
  Dulus is a **lightweight Python reimplementation of Claude Code** that isn't locked to Claude. It ships the whole loop — REPL, tool dispatch, streaming, context compaction, checkpoints, sub-agents, voice, Telegram bridge, MCP, plugins — in roughly **12K lines you can actually read**. Fork it. Bend it. Run it offline against Qwen on your M2.
92
92
 
93
- > **v0.2.12 — May 8, 2026** — `sounddevice` also moved to `[voice]` extra (it pulls `cffi`, which needs `libffi-dev` to compile and breaks termux installs). Core `pip install dulus` is now zero-compile. Opt in to extras: `[memory]` for MemPalace, `[voice]` for mic input, `[full]` for both.
93
+ > **v0.2.13 — May 8, 2026** — Internal robustness fixes for Ollama streaming. No user-facing API changes.
94
94
  > Type `/news` to see what changed.
95
95
 
96
96
  ---
@@ -2347,6 +2347,109 @@ def calc_cost(model: str, in_tok: int, out_tok: int) -> float:
2347
2347
  return (in_tok * ic + out_tok * oc) / 1_000_000
2348
2348
 
2349
2349
 
2350
+ # ── Native tool-call format interceptors ──────────────────────────────────
2351
+ # Some models (Gemma 3/4, Mistral, ...) emit their NATIVE tool-call format
2352
+ # inside `delta.content` even when the API has been told to use OpenAI-style
2353
+ # tool schemas. Without interception the user sees raw markers like
2354
+ # `<|tool_call>call:Foo{"x":1}<tool_call|>` streamed as text, and the
2355
+ # intended tool call never fires — and on Ollama Cloud / vLLM the broken
2356
+ # format can also trip a 502 from the upstream proxy. The helpers below let
2357
+ # stream_ollama / stream_openai_compat detect these markers, switch into
2358
+ # buffer mode, and parse the buffered tail into proper tool_calls.
2359
+ _NATIVE_TOOL_OPENERS = (
2360
+ "<|tool_call|>", # Gemma official
2361
+ "<|tool_call>", # Gemma 4 asymmetric variant seen in the wild
2362
+ "<tool_call>", # Hermes / Qwen
2363
+ "[TOOL_CALLS]", # Mistral
2364
+ )
2365
+
2366
+ _GEMMA_QUOTE_TOKEN_FIXES = (
2367
+ ("<|\"|>", '"'),
2368
+ ("<|'|>", "'"),
2369
+ )
2370
+
2371
+ _NATIVE_FMT_V2 = re.compile(
2372
+ r"<\|?tool_call\|?>\s*call:\s*(\w+)\s*(\{.*?\})\s*<\|?(?:end_)?(?:/)?tool_call\|?>",
2373
+ re.DOTALL,
2374
+ )
2375
+ _NATIVE_FMT_V1 = re.compile(
2376
+ r"<\|?tool_call\|?>\s*(\{.*?\})\s*<\|?(?:end_)?(?:/)?tool_call\|?>",
2377
+ re.DOTALL,
2378
+ )
2379
+ _NATIVE_FMT_MISTRAL = re.compile(r"\[TOOL_CALLS\]\s*(\[.*?\])", re.DOTALL)
2380
+
2381
+
2382
+ def _find_native_tool_marker(text: str) -> "int | None":
2383
+ earliest = None
2384
+ for opener in _NATIVE_TOOL_OPENERS:
2385
+ idx = text.find(opener)
2386
+ if idx != -1 and (earliest is None or idx < earliest):
2387
+ earliest = idx
2388
+ return earliest
2389
+
2390
+
2391
+ def _extract_native_tool_calls(buf: str) -> list:
2392
+ """Parse buffered native-format tool calls. Returns [] on any failure."""
2393
+ if not buf:
2394
+ return []
2395
+ for tok, repl in _GEMMA_QUOTE_TOKEN_FIXES:
2396
+ buf = buf.replace(tok, repl)
2397
+
2398
+ out: list = []
2399
+
2400
+ # Format 2 first (more specific — explicit `call:NAME` outside the JSON)
2401
+ for m in _NATIVE_FMT_V2.finditer(buf):
2402
+ name, body = m.group(1), m.group(2)
2403
+ try:
2404
+ args = json.loads(body)
2405
+ if not isinstance(args, dict):
2406
+ args = {"_raw": body}
2407
+ except json.JSONDecodeError:
2408
+ args = {"_raw": body}
2409
+ out.append({"id": f"native_call_{len(out)}", "name": name, "input": args})
2410
+
2411
+ # Format 1: JSON envelope with `name` + `arguments`
2412
+ if not out:
2413
+ for m in _NATIVE_FMT_V1.finditer(buf):
2414
+ try:
2415
+ parsed = json.loads(m.group(1))
2416
+ if isinstance(parsed, dict):
2417
+ name = parsed.get("name") or parsed.get("function") or ""
2418
+ args = parsed.get("arguments") or parsed.get("args") or {}
2419
+ if name:
2420
+ if not isinstance(args, dict):
2421
+ args = {"_raw": str(args)}
2422
+ out.append({
2423
+ "id": f"native_call_{len(out)}",
2424
+ "name": name, "input": args,
2425
+ })
2426
+ except json.JSONDecodeError:
2427
+ continue
2428
+
2429
+ # Mistral [TOOL_CALLS] [{...}, {...}]
2430
+ if not out:
2431
+ for m in _NATIVE_FMT_MISTRAL.finditer(buf):
2432
+ try:
2433
+ arr = json.loads(m.group(1))
2434
+ if isinstance(arr, list):
2435
+ for item in arr:
2436
+ if not isinstance(item, dict):
2437
+ continue
2438
+ name = item.get("name") or (item.get("function") or {}).get("name") or ""
2439
+ args = item.get("arguments") or (item.get("function") or {}).get("arguments") or {}
2440
+ if name:
2441
+ if not isinstance(args, dict):
2442
+ args = {"_raw": str(args)}
2443
+ out.append({
2444
+ "id": f"native_call_{len(out)}",
2445
+ "name": name, "input": args,
2446
+ })
2447
+ except json.JSONDecodeError:
2448
+ continue
2449
+
2450
+ return out
2451
+
2452
+
2350
2453
  def estimate_tokens_kimi(api_key: str, model: str, messages: list) -> int | None:
2351
2454
  """Estimate token count using Kimi's native API endpoint.
2352
2455
 
@@ -3548,7 +3651,17 @@ def stream_ollama(
3548
3651
  text = ""
3549
3652
  thinking = ""
3550
3653
  tool_buf: dict = {}
3551
-
3654
+
3655
+ # Native tool-call interceptor state. When the model emits its native
3656
+ # `<|tool_call|>...` envelope inside `content` (Gemma 3/4 in particular
3657
+ # do this even when given OpenAI-style tool schemas), we stop yielding
3658
+ # text and accumulate everything from the marker onward. At end-of-stream
3659
+ # we parse the buffer into proper tool_calls. Without this the user sees
3660
+ # `<|tool_call>call:Foo{...}<tool_call|>` as raw text, the tool never
3661
+ # fires, and on Ollama Cloud the malformed exchange can trip a 502.
3662
+ _native_buf = "" # text accumulated after a native marker
3663
+ _native_intercept = False # True once we've seen any native marker
3664
+
3552
3665
  # State for prompt-based tool call parsing across streamed chunks
3553
3666
  use_deep_tools = config.get("deep_tools", False) if config else False
3554
3667
  _auto_wrap_json = is_deepseek_r1 and use_deep_tools
@@ -3603,9 +3716,22 @@ def stream_ollama(
3603
3716
  if display:
3604
3717
  text += display
3605
3718
  yield TextChunk(display)
3719
+ elif _native_intercept:
3720
+ # Already inside a native tool-call envelope — buffer silently.
3721
+ _native_buf += content
3606
3722
  else:
3607
- text += content
3608
- yield TextChunk(content)
3723
+ marker = _find_native_tool_marker(content)
3724
+ if marker is not None:
3725
+ # Yield clean prefix, then start buffering from the marker.
3726
+ prefix = content[:marker]
3727
+ if prefix:
3728
+ text += prefix
3729
+ yield TextChunk(prefix)
3730
+ _native_buf += content[marker:]
3731
+ _native_intercept = True
3732
+ else:
3733
+ text += content
3734
+ yield TextChunk(content)
3609
3735
 
3610
3736
  # Handle native ollama tools format
3611
3737
  for tc in msg.get("tool_calls", []):
@@ -3632,7 +3758,18 @@ def stream_ollama(
3632
3758
  for idx in sorted(tool_buf):
3633
3759
  v = tool_buf[idx]
3634
3760
  tool_calls.append({"id": v["id"], "name": v["name"], "input": v["input"]})
3635
-
3761
+
3762
+ # Merge native-format tool calls intercepted from `content` (Gemma 3/4 etc.)
3763
+ if _native_intercept:
3764
+ intercepted = _extract_native_tool_calls(_native_buf)
3765
+ if intercepted:
3766
+ tool_calls.extend(intercepted)
3767
+ else:
3768
+ # Parser couldn't make sense of it — surface the raw buffer so the
3769
+ # user sees something instead of a silent stall.
3770
+ text += _native_buf
3771
+ yield TextChunk(_native_buf)
3772
+
3636
3773
  # Merge prompt-based tools from parser
3637
3774
  if _prompt_tool_mode:
3638
3775
  for tc in parser.tool_calls:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dulus"
7
- version = "0.2.12"
7
+ version = "0.2.13"
8
8
  description = "Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes