micro-cc 0.2.0__tar.gz → 0.2.2__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.
- {micro_cc-0.2.0 → micro_cc-0.2.2}/PKG-INFO +1 -1
- {micro_cc-0.2.0 → micro_cc-0.2.2}/pyproject.toml +1 -1
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/claude_loop_.py +20 -3
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/models/anthropic.py +13 -2
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/models/litellm.py +11 -1
- micro_cc-0.2.2/src/micro_cc/models/ollama.py +372 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/screens/microcc-styles.tcss +5 -1
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/screens/window_overlay_.py +43 -10
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/start_live_.py +374 -24
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/tools/browser_tool_.py +2 -2
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/tools/computer_tool_.py +2 -2
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/utils/helpers.py +3 -1
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/utils/msg_store_.py +47 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/utils/tokenization_simple.py +4 -4
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc.egg-info/PKG-INFO +1 -1
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc.egg-info/SOURCES.txt +1 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/README.md +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/setup.cfg +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/__init__.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/browser/__init__.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/browser/_cookies.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/browser/_md_convert.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/browser/browser_manager.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/browser/simpletextbrowser.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/cache/__init__.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/cache/redis_cache.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/execute_tool.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/models/__init__.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/models/schema.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/screens/__init__.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/screens/md_render_.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/SKILL.md +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/DMMono-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Gloock-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Italiana-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Jura-Light.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Jura-Medium.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Jura-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Outfit-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Tektur-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/github/SKILL.md +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/SKILL.md +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/editing.md +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/pptxgenjs.md +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/__init__.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/add_slide.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/clean.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/office/helpers/__init__.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/office/helpers/merge_runs.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/office/helpers/simplify_redlines.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/office/pack.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/office/soffice.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/office/unpack.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/office/validate.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/office/validators/__init__.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/office/validators/base.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/office/validators/docx.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/office/validators/pptx.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/office/validators/redlining.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/pptx/scripts/thumbnail.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/skill_loader.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/skills/xlsx/SKILL.md +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/tests/__init__.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/tests/test_messages.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/tools/__init__.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/tools/bash_tool.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/tools/file_tools_.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/tools/mcp_client_.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/tools/plan_tools_.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/tools/search_tool_.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/tools/skill_tools_.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/tools/vision_tools_.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/tools/web_tools_.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/utils/__init__.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/utils/annotate_.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/utils/claude_md_loader.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/utils/file_watcher_.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/utils/process_tracker.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/utils/tokenization.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/utils/utils.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc/web_.py +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc.egg-info/dependency_links.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc.egg-info/entry_points.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc.egg-info/requires.txt +0 -0
- {micro_cc-0.2.0 → micro_cc-0.2.2}/src/micro_cc.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "micro-cc"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.02"
|
|
8
8
|
description = "Harness that gives frontier models full system access — shell, filesystem, browser, MCP — running directly on the metal."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -11,6 +11,7 @@ from micro_cc.execute_tool import execute_tool_call
|
|
|
11
11
|
from micro_cc.models.schema import function_to_schema
|
|
12
12
|
from micro_cc.models.anthropic import a_model_call
|
|
13
13
|
from micro_cc.models.litellm import l_model_call
|
|
14
|
+
from micro_cc.models.ollama import o_model_call
|
|
14
15
|
from micro_cc.skills.skill_loader import get_skill_summary
|
|
15
16
|
from micro_cc.tools.bash_tool import bash_
|
|
16
17
|
from micro_cc.tools.file_tools_ import read_, write_, edit_, glob_, grep_
|
|
@@ -28,7 +29,7 @@ from micro_cc.tools.search_tool_ import (
|
|
|
28
29
|
from micro_cc.tools.vision_tools_ import vision
|
|
29
30
|
from micro_cc.tools.mcp_client_ import resolve_mcp_for_litellm, call_mcp_tool
|
|
30
31
|
from micro_cc.utils.msg_store_ import load_summary
|
|
31
|
-
from micro_cc.utils.tokenization_simple import token_cutter
|
|
32
|
+
from micro_cc.utils.tokenization_simple import token_cutter, token_stats
|
|
32
33
|
from micro_cc.utils.helpers import tokenizer
|
|
33
34
|
from micro_cc.utils.claude_md_loader import load_claude_md_file
|
|
34
35
|
from micro_cc.utils.helpers import get_endpoint
|
|
@@ -47,10 +48,10 @@ async def claude_loop(
|
|
|
47
48
|
project_dir,
|
|
48
49
|
watcher=None,
|
|
49
50
|
model="sonnet-4.6",
|
|
51
|
+
max_tokens=80000,
|
|
50
52
|
):
|
|
51
53
|
"""micro cc"""
|
|
52
54
|
|
|
53
|
-
max_tokens = 80000
|
|
54
55
|
end_resp = get_endpoint()
|
|
55
56
|
redis_state = RedisStateManager()
|
|
56
57
|
|
|
@@ -191,7 +192,16 @@ make_plan, update_step, show_full_plan, add_step
|
|
|
191
192
|
trimmed_loop_msgs = system_context + plan_msg + trimmed_loop_msgs
|
|
192
193
|
|
|
193
194
|
try:
|
|
194
|
-
if end_resp == "
|
|
195
|
+
if end_resp == "Ollama":
|
|
196
|
+
stream = await o_model_call(
|
|
197
|
+
input=trimmed_loop_msgs,
|
|
198
|
+
model=model,
|
|
199
|
+
tools=tool_schemas,
|
|
200
|
+
mcp_openai_tools=mcp_openai_tools or None,
|
|
201
|
+
thinking=True,
|
|
202
|
+
stream=True,
|
|
203
|
+
)
|
|
204
|
+
elif end_resp == "LiteLLM":
|
|
195
205
|
stream = await l_model_call(
|
|
196
206
|
input=trimmed_loop_msgs,
|
|
197
207
|
model=model,
|
|
@@ -215,6 +225,7 @@ make_plan, update_step, show_full_plan, add_step
|
|
|
215
225
|
break
|
|
216
226
|
|
|
217
227
|
response = None
|
|
228
|
+
api_usage = {}
|
|
218
229
|
async for event in stream:
|
|
219
230
|
if event["type"] == "text_delta":
|
|
220
231
|
yield {"type": "text_delta", "content": event["text"]}
|
|
@@ -222,6 +233,12 @@ make_plan, update_step, show_full_plan, add_step
|
|
|
222
233
|
yield {"type": "thinking_delta", "content": event["thinking"]}
|
|
223
234
|
elif event["type"] == "response":
|
|
224
235
|
response = event["response"]
|
|
236
|
+
api_usage = event.get("usage", {})
|
|
237
|
+
|
|
238
|
+
if api_usage.get("input"):
|
|
239
|
+
token_stats["input"] = api_usage["input"]
|
|
240
|
+
if api_usage.get("output"):
|
|
241
|
+
token_stats["output"] += api_usage["output"]
|
|
225
242
|
|
|
226
243
|
if response is None:
|
|
227
244
|
yield {"type": "error", "message": "model call failed after retries"}
|
|
@@ -34,9 +34,15 @@ async def _wrap_stream(raw_stream):
|
|
|
34
34
|
thinking_signature = ""
|
|
35
35
|
tool_blocks = []
|
|
36
36
|
current_tool = None
|
|
37
|
+
usage = {}
|
|
37
38
|
|
|
38
39
|
async for event in raw_stream:
|
|
39
|
-
if event.type == "
|
|
40
|
+
if event.type == "message_start":
|
|
41
|
+
u = getattr(event.message, "usage", None)
|
|
42
|
+
if u:
|
|
43
|
+
usage["input"] = getattr(u, "input_tokens", 0)
|
|
44
|
+
|
|
45
|
+
elif event.type == "content_block_start":
|
|
40
46
|
if event.content_block.type == "tool_use":
|
|
41
47
|
current_tool = {"id": event.content_block.id, "name": event.content_block.name, "input_json": ""}
|
|
42
48
|
|
|
@@ -59,6 +65,11 @@ async def _wrap_stream(raw_stream):
|
|
|
59
65
|
tool_blocks.append(current_tool | {"input": args})
|
|
60
66
|
current_tool = None
|
|
61
67
|
|
|
68
|
+
elif event.type == "message_delta":
|
|
69
|
+
u = getattr(event, "usage", None)
|
|
70
|
+
if u:
|
|
71
|
+
usage["output"] = getattr(u, "output_tokens", 0)
|
|
72
|
+
|
|
62
73
|
blocks = []
|
|
63
74
|
if thinking:
|
|
64
75
|
blocks.append(ContentBlock(type="thinking", thinking=thinking, signature=thinking_signature))
|
|
@@ -67,7 +78,7 @@ async def _wrap_stream(raw_stream):
|
|
|
67
78
|
for tb in tool_blocks:
|
|
68
79
|
blocks.append(ContentBlock(type="tool_use", name=tb["name"], input=tb["input"], id=tb["id"]))
|
|
69
80
|
|
|
70
|
-
yield {"type": "response", "response": Response(content=blocks)}
|
|
81
|
+
yield {"type": "response", "response": Response(content=blocks), "usage": usage}
|
|
71
82
|
|
|
72
83
|
async def a_model_call(
|
|
73
84
|
input: Union[List[Dict[str, Any]], str],
|
|
@@ -218,6 +218,7 @@ async def _wrap_stream(openai_stream):
|
|
|
218
218
|
thinking = ""
|
|
219
219
|
thinking_signature = ""
|
|
220
220
|
tool_calls = {} # index -> {id, name, arguments_str}
|
|
221
|
+
usage = {}
|
|
221
222
|
|
|
222
223
|
async for chunk in openai_stream:
|
|
223
224
|
delta = chunk.choices[0].delta
|
|
@@ -246,6 +247,12 @@ async def _wrap_stream(openai_stream):
|
|
|
246
247
|
if tc.function and tc.function.arguments:
|
|
247
248
|
tool_calls[idx]["args"] += tc.function.arguments
|
|
248
249
|
|
|
250
|
+
# Usage — LiteLLM sends it on the final chunk
|
|
251
|
+
u = getattr(chunk, "usage", None)
|
|
252
|
+
if u:
|
|
253
|
+
usage["input"] = getattr(u, "prompt_tokens", 0)
|
|
254
|
+
usage["output"] = getattr(u, "completion_tokens", 0)
|
|
255
|
+
|
|
249
256
|
# Stream done — build final Response for claude_loop_
|
|
250
257
|
blocks = []
|
|
251
258
|
if thinking:
|
|
@@ -262,7 +269,7 @@ async def _wrap_stream(openai_stream):
|
|
|
262
269
|
ContentBlock(type="tool_use", name=tc["name"], input=args, id=tc["id"])
|
|
263
270
|
)
|
|
264
271
|
|
|
265
|
-
yield {"type": "response", "response": Response(content=blocks)}
|
|
272
|
+
yield {"type": "response", "response": Response(content=blocks), "usage": usage}
|
|
266
273
|
|
|
267
274
|
|
|
268
275
|
# ---------------------------------------------------------------------------
|
|
@@ -332,6 +339,9 @@ async def l_model_call(
|
|
|
332
339
|
"messages": messages,
|
|
333
340
|
}
|
|
334
341
|
|
|
342
|
+
if stream:
|
|
343
|
+
api_params["stream_options"] = {"include_usage": True}
|
|
344
|
+
|
|
335
345
|
if thinking:
|
|
336
346
|
api_params["reasoning_effort"] = "high"
|
|
337
347
|
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Ollama model caller — fully standalone.
|
|
2
|
+
|
|
3
|
+
Talks to a locally running Ollama daemon via its OpenAI-compatible
|
|
4
|
+
endpoint at http://localhost:11434/v1.
|
|
5
|
+
|
|
6
|
+
Handled internally — no dependency on other model callers:
|
|
7
|
+
- Anthropic message format → OpenAI chat format conversion
|
|
8
|
+
- Tool schema conversion (Anthropic shape → OpenAI function shape)
|
|
9
|
+
- Streaming wrapper that yields Anthropic-shaped events
|
|
10
|
+
- Ollama-specific quirks:
|
|
11
|
+
* thinking field is `reasoning` (not `reasoning_content` like LiteLLM/DeepSeek)
|
|
12
|
+
* final stream chunk with include_usage has empty `choices` list
|
|
13
|
+
* `think: True` passed via extra_body toggles reasoning mode
|
|
14
|
+
* `num_ctx` passed via extra_body.options overrides the 4K default
|
|
15
|
+
|
|
16
|
+
Env vars:
|
|
17
|
+
OLLAMA_BASE_URL default http://localhost:11434/v1
|
|
18
|
+
OLLAMA_MODEL user picks any tag pulled via `ollama pull <tag>`
|
|
19
|
+
OLLAMA_NUM_CTX default 32768 — fits Qwen3-14B KV cache in ~5GB
|
|
20
|
+
OLLAMA_MAX_OUTPUT default 4096 — generation budget per response
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from typing import List, Dict, Any, Optional, Union
|
|
24
|
+
from openai import AsyncOpenAI
|
|
25
|
+
from dotenv import load_dotenv
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
import asyncio
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
|
|
31
|
+
load_dotenv(os.path.expanduser("~/.micro-cc/.env"))
|
|
32
|
+
load_dotenv()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Response shape — same as Anthropic SDK so claude_loop_.py works unchanged
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ContentBlock:
|
|
41
|
+
type: str
|
|
42
|
+
text: str = ""
|
|
43
|
+
thinking: str = ""
|
|
44
|
+
signature: str = ""
|
|
45
|
+
name: str = ""
|
|
46
|
+
input: dict = field(default_factory=dict)
|
|
47
|
+
id: str = ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Response:
|
|
52
|
+
content: list = field(default_factory=list)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Converters — Anthropic format → OpenAI chat format
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def _tools_to_openai(anthropic_tools, mcp_openai_tools=None):
|
|
60
|
+
"""Anthropic tool schemas + MCP tools → OpenAI function-calling format."""
|
|
61
|
+
out = []
|
|
62
|
+
for t in anthropic_tools:
|
|
63
|
+
out.append({
|
|
64
|
+
"type": "function",
|
|
65
|
+
"function": {
|
|
66
|
+
"name": t["name"],
|
|
67
|
+
"description": t.get("description", ""),
|
|
68
|
+
"parameters": t.get("input_schema", {}),
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
if mcp_openai_tools:
|
|
72
|
+
out.extend(mcp_openai_tools)
|
|
73
|
+
return out or None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _msgs_to_openai(anthropic_msgs):
|
|
77
|
+
"""Convert Anthropic-format message list to OpenAI chat format.
|
|
78
|
+
|
|
79
|
+
Handles: system, user (string/multimodal/tool_result),
|
|
80
|
+
assistant (string/tool_use blocks). Thinking blocks are dropped —
|
|
81
|
+
local models can't replay signed thinking, and Ollama's OpenAI-compat
|
|
82
|
+
endpoint doesn't accept them on the resend path.
|
|
83
|
+
"""
|
|
84
|
+
out = []
|
|
85
|
+
for msg in anthropic_msgs:
|
|
86
|
+
role = msg.get("role")
|
|
87
|
+
content = msg.get("content", "")
|
|
88
|
+
|
|
89
|
+
if role == "system":
|
|
90
|
+
out.append({"role": "system", "content": content})
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
# --- Assistant with content blocks ---
|
|
94
|
+
if role == "assistant" and isinstance(content, list):
|
|
95
|
+
text_parts = []
|
|
96
|
+
tool_calls = []
|
|
97
|
+
for block in content:
|
|
98
|
+
if not isinstance(block, dict):
|
|
99
|
+
continue
|
|
100
|
+
btype = block.get("type")
|
|
101
|
+
if btype == "text":
|
|
102
|
+
text_parts.append(block["text"])
|
|
103
|
+
elif btype == "tool_use":
|
|
104
|
+
tool_calls.append({
|
|
105
|
+
"id": block["id"],
|
|
106
|
+
"type": "function",
|
|
107
|
+
"function": {
|
|
108
|
+
"name": block["name"],
|
|
109
|
+
"arguments": json.dumps(block["input"]),
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
# thinking blocks: intentionally skipped for Ollama
|
|
113
|
+
|
|
114
|
+
m = {
|
|
115
|
+
"role": "assistant",
|
|
116
|
+
"content": "\n".join(text_parts) if text_parts else None,
|
|
117
|
+
}
|
|
118
|
+
if tool_calls:
|
|
119
|
+
m["tool_calls"] = tool_calls
|
|
120
|
+
out.append(m)
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
# --- User with content blocks (tool_result, text, image, document) ---
|
|
124
|
+
if role == "user" and isinstance(content, list):
|
|
125
|
+
tool_results = []
|
|
126
|
+
other_parts = []
|
|
127
|
+
|
|
128
|
+
for block in content:
|
|
129
|
+
if not isinstance(block, dict):
|
|
130
|
+
continue
|
|
131
|
+
btype = block.get("type")
|
|
132
|
+
if btype == "tool_result":
|
|
133
|
+
tool_results.append({
|
|
134
|
+
"role": "tool",
|
|
135
|
+
"tool_call_id": block["tool_use_id"],
|
|
136
|
+
"content": block.get("content", ""),
|
|
137
|
+
})
|
|
138
|
+
elif btype == "text":
|
|
139
|
+
other_parts.append({"type": "text", "text": block["text"]})
|
|
140
|
+
elif btype == "image":
|
|
141
|
+
source = block.get("source", {})
|
|
142
|
+
media = source.get("media_type", "image/jpeg")
|
|
143
|
+
other_parts.append({
|
|
144
|
+
"type": "image_url",
|
|
145
|
+
"image_url": {
|
|
146
|
+
"url": f"data:{media};base64,{source.get('data', '')}",
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
elif btype == "document":
|
|
150
|
+
other_parts.append({
|
|
151
|
+
"type": "text",
|
|
152
|
+
"text": "[PDF document attached]",
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
out.extend(tool_results)
|
|
156
|
+
|
|
157
|
+
if other_parts:
|
|
158
|
+
if len(other_parts) == 1 and other_parts[0].get("type") == "text":
|
|
159
|
+
out.append({"role": "user", "content": other_parts[0]["text"]})
|
|
160
|
+
else:
|
|
161
|
+
out.append({"role": "user", "content": other_parts})
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
# --- Simple string content ---
|
|
165
|
+
out.append({"role": role, "content": content})
|
|
166
|
+
|
|
167
|
+
return out
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# Response wrappers — OpenAI shape → Anthropic-like Response
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def _extract_reasoning(obj) -> str:
|
|
175
|
+
"""Ollama surfaces thinking on `reasoning` via model_dump; OpenAI client
|
|
176
|
+
doesn't type that attribute, so fall back to model_dump().get('reasoning')."""
|
|
177
|
+
rc = getattr(obj, "reasoning", None)
|
|
178
|
+
if rc:
|
|
179
|
+
return rc
|
|
180
|
+
try:
|
|
181
|
+
dumped = obj.model_dump()
|
|
182
|
+
return dumped.get("reasoning") or ""
|
|
183
|
+
except Exception:
|
|
184
|
+
return ""
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _wrap_response(openai_resp):
|
|
188
|
+
"""Non-streaming OpenAI ChatCompletion → Anthropic-like Response."""
|
|
189
|
+
choice = openai_resp.choices[0]
|
|
190
|
+
msg = choice.message
|
|
191
|
+
blocks = []
|
|
192
|
+
|
|
193
|
+
reasoning = _extract_reasoning(msg)
|
|
194
|
+
if reasoning:
|
|
195
|
+
blocks.append(ContentBlock(type="thinking", thinking=reasoning, signature=""))
|
|
196
|
+
|
|
197
|
+
if msg.content:
|
|
198
|
+
blocks.append(ContentBlock(type="text", text=msg.content))
|
|
199
|
+
|
|
200
|
+
if msg.tool_calls:
|
|
201
|
+
for tc in msg.tool_calls:
|
|
202
|
+
try:
|
|
203
|
+
args = json.loads(tc.function.arguments)
|
|
204
|
+
except (json.JSONDecodeError, TypeError):
|
|
205
|
+
args = {}
|
|
206
|
+
blocks.append(ContentBlock(
|
|
207
|
+
type="tool_use",
|
|
208
|
+
name=tc.function.name,
|
|
209
|
+
input=args,
|
|
210
|
+
id=tc.id,
|
|
211
|
+
))
|
|
212
|
+
|
|
213
|
+
return Response(content=blocks)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async def _wrap_stream(openai_stream):
|
|
217
|
+
"""Accumulate OpenAI chunks, yield Anthropic-shaped events,
|
|
218
|
+
finish with a final Response containing all content blocks."""
|
|
219
|
+
text = ""
|
|
220
|
+
thinking = ""
|
|
221
|
+
tool_calls = {} # index → {id, name, args}
|
|
222
|
+
usage = {}
|
|
223
|
+
|
|
224
|
+
async for chunk in openai_stream:
|
|
225
|
+
# Final usage-only chunks have empty choices (Ollama sends this
|
|
226
|
+
# when stream_options.include_usage is on).
|
|
227
|
+
if not chunk.choices:
|
|
228
|
+
u = getattr(chunk, "usage", None)
|
|
229
|
+
if u:
|
|
230
|
+
usage["input"] = getattr(u, "prompt_tokens", 0)
|
|
231
|
+
usage["output"] = getattr(u, "completion_tokens", 0)
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
delta = chunk.choices[0].delta
|
|
235
|
+
|
|
236
|
+
# Visible text
|
|
237
|
+
if delta.content:
|
|
238
|
+
text += delta.content
|
|
239
|
+
yield {"type": "text_delta", "text": delta.content}
|
|
240
|
+
|
|
241
|
+
# Thinking — Ollama uses `reasoning`, not `reasoning_content`
|
|
242
|
+
rc = _extract_reasoning(delta)
|
|
243
|
+
if rc:
|
|
244
|
+
thinking += rc
|
|
245
|
+
yield {"type": "thinking_delta", "thinking": rc}
|
|
246
|
+
|
|
247
|
+
# Tool calls — must buffer arguments across chunks
|
|
248
|
+
if delta.tool_calls:
|
|
249
|
+
for tc in delta.tool_calls:
|
|
250
|
+
idx = tc.index
|
|
251
|
+
if idx not in tool_calls:
|
|
252
|
+
tool_calls[idx] = {
|
|
253
|
+
"id": tc.id or "",
|
|
254
|
+
"name": tc.function.name if tc.function else "",
|
|
255
|
+
"args": "",
|
|
256
|
+
}
|
|
257
|
+
if tc.function and tc.function.arguments:
|
|
258
|
+
tool_calls[idx]["args"] += tc.function.arguments
|
|
259
|
+
|
|
260
|
+
# Some Ollama builds attach usage on the final non-empty chunk
|
|
261
|
+
u = getattr(chunk, "usage", None)
|
|
262
|
+
if u:
|
|
263
|
+
usage["input"] = getattr(u, "prompt_tokens", 0)
|
|
264
|
+
usage["output"] = getattr(u, "completion_tokens", 0)
|
|
265
|
+
|
|
266
|
+
blocks = []
|
|
267
|
+
if thinking:
|
|
268
|
+
blocks.append(ContentBlock(type="thinking", thinking=thinking, signature=""))
|
|
269
|
+
if text:
|
|
270
|
+
blocks.append(ContentBlock(type="text", text=text))
|
|
271
|
+
for tc in tool_calls.values():
|
|
272
|
+
args = json.loads(tc["args"]) if tc["args"] else {}
|
|
273
|
+
blocks.append(ContentBlock(type="tool_use", name=tc["name"], input=args, id=tc["id"]))
|
|
274
|
+
|
|
275
|
+
yield {"type": "response", "response": Response(content=blocks), "usage": usage}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
# Main entry point — same signature pattern as models/anthropic.py
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
async def o_model_call(
|
|
283
|
+
input: Union[List[Dict[str, Any]], str],
|
|
284
|
+
model: str = None,
|
|
285
|
+
encoded_image: Optional[Union[str, List[str]]] = None,
|
|
286
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
287
|
+
mcp_openai_tools: list = None,
|
|
288
|
+
stream: bool = False,
|
|
289
|
+
thinking: bool = False,
|
|
290
|
+
max_tokens: int = None,
|
|
291
|
+
client_timeout: int = 480,
|
|
292
|
+
pdf: str = None,
|
|
293
|
+
retries: int = 3,
|
|
294
|
+
):
|
|
295
|
+
base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1")
|
|
296
|
+
model = model or os.getenv("OLLAMA_MODEL")
|
|
297
|
+
num_ctx = int(os.getenv("OLLAMA_NUM_CTX", "32768"))
|
|
298
|
+
if max_tokens is None:
|
|
299
|
+
max_tokens = int(os.getenv("OLLAMA_MAX_OUTPUT", "4096"))
|
|
300
|
+
|
|
301
|
+
client = AsyncOpenAI(
|
|
302
|
+
base_url=base_url,
|
|
303
|
+
api_key="ollama", # dummy — Ollama ignores the key
|
|
304
|
+
timeout=client_timeout,
|
|
305
|
+
)
|
|
306
|
+
base_sleep = 2
|
|
307
|
+
|
|
308
|
+
# ---- Build messages ----
|
|
309
|
+
messages = []
|
|
310
|
+
if pdf:
|
|
311
|
+
messages = [{"role": "user", "content": f"[PDF document attached]\n\n{input}"}]
|
|
312
|
+
elif encoded_image:
|
|
313
|
+
content = []
|
|
314
|
+
imgs = [encoded_image] if isinstance(encoded_image, str) else encoded_image
|
|
315
|
+
for img in imgs:
|
|
316
|
+
content.append({
|
|
317
|
+
"type": "image_url",
|
|
318
|
+
"image_url": {"url": f"data:image/jpeg;base64,{img}"},
|
|
319
|
+
})
|
|
320
|
+
content.append({"type": "text", "text": input})
|
|
321
|
+
messages = [{"role": "user", "content": content}]
|
|
322
|
+
elif isinstance(input, str):
|
|
323
|
+
messages = [{"role": "user", "content": input}]
|
|
324
|
+
else:
|
|
325
|
+
messages = _msgs_to_openai(input)
|
|
326
|
+
|
|
327
|
+
# ---- API parameters ----
|
|
328
|
+
# Ollama-specific: num_ctx in extra_body.options, think toggle at root
|
|
329
|
+
extra_body = {"options": {"num_ctx": num_ctx}}
|
|
330
|
+
if thinking:
|
|
331
|
+
extra_body["think"] = True
|
|
332
|
+
|
|
333
|
+
api_params = {
|
|
334
|
+
"model": model,
|
|
335
|
+
"max_tokens": max_tokens,
|
|
336
|
+
"stream": stream,
|
|
337
|
+
"messages": messages,
|
|
338
|
+
"extra_body": extra_body,
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if stream:
|
|
342
|
+
api_params["stream_options"] = {"include_usage": True}
|
|
343
|
+
|
|
344
|
+
if tools:
|
|
345
|
+
openai_tools = _tools_to_openai(tools, mcp_openai_tools=mcp_openai_tools)
|
|
346
|
+
if openai_tools:
|
|
347
|
+
api_params["tools"] = openai_tools
|
|
348
|
+
api_params["tool_choice"] = "auto"
|
|
349
|
+
|
|
350
|
+
# ---- Call with retries ----
|
|
351
|
+
for attempt in range(retries):
|
|
352
|
+
try:
|
|
353
|
+
response = await client.chat.completions.create(**api_params)
|
|
354
|
+
if stream:
|
|
355
|
+
return _wrap_stream(response)
|
|
356
|
+
else:
|
|
357
|
+
return _wrap_response(response)
|
|
358
|
+
|
|
359
|
+
except Exception as e:
|
|
360
|
+
import traceback
|
|
361
|
+
print(f"\n[ollama model_call]: {e}", flush=True)
|
|
362
|
+
traceback.print_exc()
|
|
363
|
+
if attempt < retries - 1:
|
|
364
|
+
sleep_time = base_sleep * (2 ** attempt)
|
|
365
|
+
print(
|
|
366
|
+
f"\n[ollama model_call]: Retrying in {sleep_time}s (attempt {attempt + 1}/{retries})..."
|
|
367
|
+
)
|
|
368
|
+
await asyncio.sleep(sleep_time)
|
|
369
|
+
else:
|
|
370
|
+
print(f"\n[ollama model_call]: Failed after {retries} attempts")
|
|
371
|
+
|
|
372
|
+
return None
|
|
@@ -24,12 +24,16 @@ PromptInput {
|
|
|
24
24
|
border-left: none;
|
|
25
25
|
border-right: none;
|
|
26
26
|
}
|
|
27
|
+
PromptInput > .text-area--cursor {
|
|
28
|
+
color: $background;
|
|
29
|
+
background: white;
|
|
30
|
+
}
|
|
27
31
|
#loader {
|
|
28
32
|
height: 1;
|
|
29
33
|
display: none;
|
|
30
34
|
color: orange;
|
|
31
35
|
}
|
|
32
|
-
#model-picker {
|
|
36
|
+
#model-picker, #rewind-picker, #slash-picker {
|
|
33
37
|
display: none;
|
|
34
38
|
height: auto;
|
|
35
39
|
max-height: 6;
|
|
@@ -14,7 +14,7 @@ BANNER = """\
|
|
|
14
14
|
[dim]───────[/] [bold]on the metal[/] [dim]─────────────────────[/]
|
|
15
15
|
[dim italic]Knowledge work is by extension a coding problem.[/]
|
|
16
16
|
|
|
17
|
-
[white]enter[/] [dim]submit[/] [dim]·[/] [white]esc[/] [dim]interrupt[/] [dim]·[/] [white]/model[/] [dim]switch[/] [dim]·[/] [white]/copy[/] [dim]clipboard[/] [dim]·[/] [white]/clear[/] [dim]reset[/] [dim]·[/] [white]/exit[/] [dim]quit[/]
|
|
17
|
+
[white]enter[/] [dim]submit[/] [dim]·[/] [white]esc[/] [dim]interrupt[/] [dim]·[/] [white]/model[/] [dim]switch[/] [dim]·[/] [white]/rewind[/] [dim]undo[/] [dim]·[/] [white]/copy[/] [dim]clipboard[/] [dim]·[/] [white]/clear[/] [dim]reset[/] [dim]·[/] [white]/exit[/] [dim]quit[/]
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
|
|
@@ -37,6 +37,8 @@ class MessageRow(Static):
|
|
|
37
37
|
t = msg["type"]
|
|
38
38
|
if t == "user":
|
|
39
39
|
return Text(f"› {msg['content']}", style="bold")
|
|
40
|
+
elif t == "user_queued":
|
|
41
|
+
return Text(f"⋯queued {msg['content']}", style="dim italic #ffA500")
|
|
40
42
|
elif t == "text":
|
|
41
43
|
return render_md(msg["content"])
|
|
42
44
|
elif t == "thinking":
|
|
@@ -45,19 +47,50 @@ class MessageRow(Static):
|
|
|
45
47
|
render_md(msg["content"], dim=True),
|
|
46
48
|
)
|
|
47
49
|
elif t == "tool_call":
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
Text(f" {msg.get('result', '')}", style="dim"),
|
|
54
|
-
)
|
|
50
|
+
name = msg['name']
|
|
51
|
+
result = msg.get('result')
|
|
52
|
+
if result is None:
|
|
53
|
+
return Text(f"⟐ {name} ⋯", style="yellow")
|
|
54
|
+
return Text(f"⟐ {name} → {str(result)[:40]}…", style="dim")
|
|
55
55
|
elif t == "error":
|
|
56
56
|
return Text(f"△ {msg['content']}", style="red")
|
|
57
57
|
elif t == "approval":
|
|
58
|
+
inp = msg.get('input', {})
|
|
59
|
+
name = msg['name']
|
|
60
|
+
_sep = Text("·" * 40, style="dim")
|
|
61
|
+
if name == "bash_":
|
|
62
|
+
code = inp.get('command', str(inp))
|
|
63
|
+
return Group(
|
|
64
|
+
Text(f"◇ {name} [y/N]", style="bold yellow"),
|
|
65
|
+
render_md(f"```bash\n{code}\n```"),
|
|
66
|
+
_sep,
|
|
67
|
+
)
|
|
68
|
+
elif name == "edit_":
|
|
69
|
+
parts = [Text(f"◇ {name} [y/N]", style="bold yellow")]
|
|
70
|
+
fp = inp.get('file_path', '')
|
|
71
|
+
if fp:
|
|
72
|
+
parts.append(Text(f" {fp}", style="dim"))
|
|
73
|
+
old = inp.get('old_string', '')
|
|
74
|
+
new = inp.get('new_string', '')
|
|
75
|
+
if old or new:
|
|
76
|
+
diff = f"```diff\n- {old}\n+ {new}\n```" if old else f"```\n{new}\n```"
|
|
77
|
+
parts.append(render_md(diff))
|
|
78
|
+
parts.append(_sep)
|
|
79
|
+
return Group(*parts)
|
|
80
|
+
elif name == "write_":
|
|
81
|
+
parts = [Text(f"◇ {name} [y/N]", style="bold yellow")]
|
|
82
|
+
fp = inp.get('file_path', '')
|
|
83
|
+
if fp:
|
|
84
|
+
parts.append(Text(f" {fp}", style="dim"))
|
|
85
|
+
content = inp.get('content', '')
|
|
86
|
+
if content:
|
|
87
|
+
preview = content[:500] + ("…" if len(content) > 500 else "")
|
|
88
|
+
parts.append(render_md(f"```\n{preview}\n```"))
|
|
89
|
+
parts.append(_sep)
|
|
90
|
+
return Group(*parts)
|
|
58
91
|
return Group(
|
|
59
|
-
Text(f"◇ {
|
|
60
|
-
Text(f" {
|
|
92
|
+
Text(f"◇ {name} [y/N]", style="bold yellow"),
|
|
93
|
+
Text(f" {str(inp)[:80]}…", style="dim"),
|
|
61
94
|
)
|
|
62
95
|
return Text("")
|
|
63
96
|
|