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.
- {dulus-0.2.12/dulus.egg-info → dulus-0.2.13}/PKG-INFO +2 -2
- {dulus-0.2.12 → dulus-0.2.13}/README.md +1 -1
- {dulus-0.2.12 → dulus-0.2.13/dulus.egg-info}/PKG-INFO +2 -2
- {dulus-0.2.12 → dulus-0.2.13}/providers.py +141 -4
- {dulus-0.2.12 → dulus-0.2.13}/pyproject.toml +1 -1
- {dulus-0.2.12 → dulus-0.2.13}/LICENSE +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/MANIFEST.in +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/agent.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/backend/__init__.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/backend/compressor.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/backend/context.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/backend/githook.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/backend/marketplace.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/backend/mempalace_bridge.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/backend/personas.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/backend/plugins.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/backend/server.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/backend/tasks.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/batch_api.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/checkpoint/__init__.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/checkpoint/hooks.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/checkpoint/store.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/checkpoint/types.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/claude_code_watcher.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/clipboard_utils.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/cloudsave.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/common.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/compaction.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/config.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/context.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/data/__init__.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/data/active_persona.json +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/data/context.json +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/data/marketplace.json +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/data/personas.json +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/data/tasks.json +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/README.md +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/__init__.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/api.html +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/architecture.md +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/azure-speech-template.json +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/dashboard/index.html +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/divider.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/generate.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/hero.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/index.html +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/news.md +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/nvidia-models.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/particle-playground.html +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/personas/index.html +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/preview.html +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/sec-agents.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/sec-brainstorm.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/sec-bridges.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/sec-features.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/sec-freetier.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/sec-memory.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/sec-models.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/sec-perms.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/sec-plugins.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/sec-quickstart.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/sec-ssj.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/spinners.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/split-pane.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/terminal-boot.svg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/docs/uploads/particle-playground.html +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/dulus.egg-info/SOURCES.txt +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/dulus.egg-info/dependency_links.txt +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/dulus.egg-info/entry_points.txt +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/dulus.egg-info/requires.txt +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/dulus.egg-info/top_level.txt +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/dulus.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/dulus_gui.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/dulus_mcp/__init__.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/dulus_mcp/client.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/dulus_mcp/config.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/dulus_mcp/tools.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/dulus_mcp/types.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/gui/__init__.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/gui/agent_bridge.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/gui/chat_widget.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/gui/main_window.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/gui/personas.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/gui/session_utils.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/gui/settings_dialog.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/gui/sidebar.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/gui/tasks_view.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/gui/themes.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/gui/tool_panel.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/input.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/license_manager.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/memory/__init__.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/memory/audit.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/memory/consolidator.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/memory/context.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/memory/offload.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/memory/palace.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/memory/scan.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/memory/sessions.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/memory/store.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/memory/tools.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/memory/types.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/memory/vector_search.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/multi_agent/__init__.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/multi_agent/subagent.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/multi_agent/tools.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/offload_helper.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/plugin/__init__.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/plugin/autoadapter.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/plugin/loader.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/plugin/recommend.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/plugin/store.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/plugin/types.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/setup.cfg +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/skill/__init__.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/skill/builtin.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/skill/clawhub.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/skill/executor.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/skill/loader.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/skill/tools.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/skills.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/spinner.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/string_utils.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/subagent.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/task/__init__.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/task/store.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/task/tools.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/task/types.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tests/test_checkpoint.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tests/test_compaction.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tests/test_diff_view.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tests/test_injection_fix.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tests/test_license.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tests/test_mcp.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tests/test_memory.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tests/test_plugin.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tests/test_skills.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tests/test_subagent.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tests/test_task.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tests/test_telegram_buffer.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tests/test_tool_registry.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tests/test_voice.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tmux_offloader.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tmux_tools.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tool_registry.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/tools.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/ui/__init__.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/ui/input.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/ui/render.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/voice/__init__.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/voice/keyterms.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/voice/recorder.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/voice/stt.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/voice/tts.py +0 -0
- {dulus-0.2.12 → dulus-0.2.13}/webchat.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
3608
|
-
|
|
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.
|
|
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
|
|
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
|