algo-cli-runtime 0.14.0__py3-none-any.whl

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 (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
algo_cli/model_info.py ADDED
@@ -0,0 +1,548 @@
1
+ """Ollama model metadata cache.
2
+
3
+ Loads metadata via ``ollama show MODEL`` (preferred when the model is installed)
4
+ and/or ``client.show()``, persists to CONFIG_DIR/model_info/, and writes a
5
+ markdown harness record to CONFIG_DIR/models/ for harness RAG.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import re
11
+ import shutil
12
+ import subprocess
13
+ import time
14
+ from typing import Any
15
+
16
+ from .config import CONFIG_DIR, _atomic_write_text
17
+
18
+
19
+ MODEL_INFO_DIR = CONFIG_DIR / "model_info"
20
+ MODELS_DIR = CONFIG_DIR / "models"
21
+
22
+ _THINKING_FAMILIES: frozenset[str] = frozenset({"qwen3", "qwen3moe"})
23
+ _VISION_FAMILIES: frozenset[str] = frozenset({"clip", "llava", "minicpm-v"})
24
+ _SIZE_RE = re.compile(r"([\d.]+)\s*([BbMmTtKk])", re.ASCII)
25
+ # Gemini models routed via Ollama Cloud require thought_signature round-tripping
26
+ # which the current Ollama Python SDK does not expose. Match these by name so the
27
+ # agent loop can disable thinking before sending tool-call sequences.
28
+ _GEMINI_NAME_RE = re.compile(r"^gemini[-_]", re.IGNORECASE)
29
+ # Grok models routed through xAI (api.x.ai/v1) use OAuth auth and the
30
+ # OpenAI-compatible chat surface — see xai_client.py.
31
+ _GROK_NAME_RE = re.compile(r"^grok[-_]", re.IGNORECASE)
32
+ # ChatGPT/OpenAI models routed through ChatGPT OAuth. Do not catch gpt-oss,
33
+ # which is an Ollama/Ollama Cloud model family.
34
+ _CHATGPT_NAME_RE = re.compile(r"^(?:chatgpt[-_]|gpt-(?!oss)|o[134](?:[-_]|$))", re.IGNORECASE)
35
+
36
+ _CACHE: dict[str, dict[str, Any]] = {}
37
+
38
+
39
+ def _safe_name(model: str) -> str:
40
+ return re.sub(r"[^\w\-.]", "_", model)
41
+
42
+
43
+ def _get(obj: Any, key: str, default: Any = None) -> Any:
44
+ if obj is None:
45
+ return default
46
+ if hasattr(obj, key):
47
+ return getattr(obj, key, default)
48
+ if isinstance(obj, dict):
49
+ return obj.get(key, default)
50
+ return default
51
+
52
+
53
+ def _context_length(raw_info: dict) -> int | None:
54
+ for key, val in raw_info.items():
55
+ if "context_length" in str(key).lower() and isinstance(val, int) and val > 0:
56
+ return val
57
+ return None
58
+
59
+
60
+ def _bare_model_name(model: str) -> str:
61
+ return (model or "").split(":", 1)[0].strip().lower()
62
+
63
+
64
+ _SHOW_CTX_RE = re.compile(r"^\s*context\s+length\s+(\d+)\s*$", re.IGNORECASE | re.MULTILINE)
65
+ _SHOW_ARCH_RE = re.compile(r"^\s*architecture\s+(\S+)\s*$", re.IGNORECASE | re.MULTILINE)
66
+ _SHOW_PARAMS_RE = re.compile(r"^\s*parameters\s+(\S+)\s*$", re.IGNORECASE | re.MULTILINE)
67
+ _SHOW_QUANT_RE = re.compile(r"^\s*quantization\s+(\S*)\s*$", re.IGNORECASE | re.MULTILINE)
68
+ _VALID_QUANT_RE = re.compile(r"^(?=.*[A-Z])(?=.*\d)[A-Z0-9_]+$", re.ASCII)
69
+ _KNOWN_CAPABILITIES = {"completion", "tools", "tool", "thinking", "vision", "embedding", "insert"}
70
+
71
+ # Static fallbacks only when ``ollama show`` and client.show() both fail.
72
+ _CLOUD_MODEL_HINTS: dict[str, dict[str, Any]] = {
73
+ "minimax-m3": {"context_length": 524_288, "supports_thinking": True},
74
+ "glm-5.2": {"context_length": 1_000_000, "supports_thinking": True},
75
+ "glm-5.1": {"context_length": 202_752},
76
+ "glm-5": {"context_length": 202_752},
77
+ "qwen3-coder": {"context_length": 262_144},
78
+ "qwen3": {"context_length": 262_144},
79
+ "deepseek-v3.1": {"context_length": 131_072},
80
+ }
81
+
82
+
83
+ def _show_name_candidates(model: str, *, cloud: bool = False) -> list[str]:
84
+ """Names to try with ``ollama show`` (config may omit ``:cloud`` tag)."""
85
+ names: list[str] = []
86
+ seen: set[str] = set()
87
+
88
+ def add(name: str) -> None:
89
+ cleaned = name.strip()
90
+ if cleaned and cleaned not in seen:
91
+ seen.add(cleaned)
92
+ names.append(cleaned)
93
+
94
+ add(model)
95
+ if cloud and ":" not in model:
96
+ add(f"{model}:cloud")
97
+ return names
98
+
99
+
100
+ def parse_ollama_show_output(text: str, *, model: str = "") -> dict[str, Any]:
101
+ """Parse human-readable output from ``ollama show MODEL``."""
102
+ capabilities: set[str] = set()
103
+ in_capabilities = False
104
+ for line in text.splitlines():
105
+ stripped = line.strip()
106
+ if stripped.lower().startswith("capabilities"):
107
+ inline = [part.lower() for part in stripped.split()[1:]]
108
+ capabilities.update(part for part in inline if part in _KNOWN_CAPABILITIES)
109
+ in_capabilities = True
110
+ continue
111
+ if in_capabilities:
112
+ if not stripped or stripped.startswith("Metadata"):
113
+ in_capabilities = False
114
+ continue
115
+ token = stripped.split()[0].lower().rstrip(":")
116
+ if token in _KNOWN_CAPABILITIES:
117
+ capabilities.add(token)
118
+
119
+ ctx_match = _SHOW_CTX_RE.search(text)
120
+ arch_match = _SHOW_ARCH_RE.search(text)
121
+ params_match = _SHOW_PARAMS_RE.search(text)
122
+ quant_match = _SHOW_QUANT_RE.search(text)
123
+
124
+ family = (arch_match.group(1) if arch_match else "").lower()
125
+ families = [family] if family else []
126
+ parameter_size = params_match.group(1) if params_match else ""
127
+ if parameter_size.isdigit() and int(parameter_size) > 0:
128
+ parameter_size = f"{int(parameter_size):,}"
129
+ quantization = (quant_match.group(1) if quant_match else "").strip()
130
+ if quantization and not _VALID_QUANT_RE.match(quantization):
131
+ quantization = ""
132
+
133
+ return {
134
+ "name": model,
135
+ "family": family,
136
+ "families": families,
137
+ "parameter_size": parameter_size,
138
+ "quantization": quantization,
139
+ "context_length": int(ctx_match.group(1)) if ctx_match else None,
140
+ "supports_thinking": "thinking" in capabilities or family in _THINKING_FAMILIES,
141
+ "supports_vision": "vision" in capabilities or bool(_VISION_FAMILIES & set(families)),
142
+ "supports_tools": "tools" in capabilities or "tool" in capabilities,
143
+ "capabilities": sorted(capabilities),
144
+ }
145
+
146
+
147
+ def fetch_model_info_from_cli(model: str, *, cloud: bool = False) -> dict[str, Any] | None:
148
+ """Run ``ollama show`` and parse model metadata (works for local and :cloud tags)."""
149
+ ollama = shutil.which("ollama")
150
+ if not ollama:
151
+ return None
152
+ for candidate in _show_name_candidates(model, cloud=cloud):
153
+ try:
154
+ proc = subprocess.run(
155
+ [ollama, "show", candidate],
156
+ capture_output=True,
157
+ text=True,
158
+ encoding="utf-8",
159
+ errors="replace",
160
+ timeout=20,
161
+ check=False,
162
+ )
163
+ except (OSError, subprocess.TimeoutExpired):
164
+ continue
165
+ if proc.returncode != 0 or not (proc.stdout or "").strip():
166
+ continue
167
+ parsed = parse_ollama_show_output(proc.stdout, model=model)
168
+ if not parsed.get("context_length"):
169
+ continue
170
+ parsed["fetched_at"] = time.time()
171
+ parsed["source"] = "ollama-show"
172
+ parsed["show_name"] = candidate
173
+ return parsed
174
+ return None
175
+
176
+
177
+ def _normalize_sdk_show(response: Any, model: str) -> dict[str, Any]:
178
+ details = _get(response, "details")
179
+ raw_info = _get(response, "model_info") or {}
180
+ if not isinstance(raw_info, dict):
181
+ try:
182
+ raw_info = dict(raw_info)
183
+ except Exception:
184
+ raw_info = {}
185
+
186
+ family = str(_get(details, "family") or "").lower()
187
+ families_raw = _get(details, "families") or []
188
+ families = [str(f).lower() for f in families_raw]
189
+
190
+ return {
191
+ "name": model,
192
+ "family": family,
193
+ "families": families,
194
+ "parameter_size": str(_get(details, "parameter_size") or ""),
195
+ "quantization": str(_get(details, "quantization_level") or ""),
196
+ "context_length": _context_length(raw_info),
197
+ "supports_thinking": family in _THINKING_FAMILIES,
198
+ "supports_vision": bool(_VISION_FAMILIES & set(families)),
199
+ "supports_tools": True,
200
+ "fetched_at": time.time(),
201
+ "source": "ollama-sdk",
202
+ }
203
+
204
+
205
+ def _merge_info_fields(base: dict[str, Any], extra: dict[str, Any]) -> dict[str, Any]:
206
+ merged = dict(base)
207
+ for key, value in extra.items():
208
+ if key in {"fetched_at", "source", "show_name"}:
209
+ if key not in merged or merged.get(key) in (None, ""):
210
+ merged[key] = value
211
+ continue
212
+ if merged.get(key) in (None, "", 0) and value not in (None, "", 0):
213
+ merged[key] = value
214
+ return merged
215
+
216
+
217
+ def fetch_model_info(client: Any | None, model: str, *, cloud: bool = False) -> dict[str, Any]:
218
+ """Load model metadata from ``ollama show``, SDK show(), or static hints."""
219
+ info: dict[str, Any] | None = None
220
+ if client is not None:
221
+ try:
222
+ response = client.show(model)
223
+ info = _normalize_sdk_show(response, model)
224
+ except Exception as exc:
225
+ info = {"name": model, "error": str(exc), "fetched_at": time.time()}
226
+
227
+ cli_info = fetch_model_info_from_cli(model, cloud=cloud)
228
+ if cli_info:
229
+ if info is None or "error" in info:
230
+ info = cli_info
231
+ else:
232
+ info = _merge_info_fields(info, cli_info)
233
+
234
+ if info is None:
235
+ hints = cloud_model_hints(model)
236
+ if hints:
237
+ info = {"name": model, **hints, "provider": "ollama-cloud", "source": "hints"}
238
+ else:
239
+ info = {"name": model, "error": "model metadata unavailable", "fetched_at": time.time()}
240
+
241
+ return merge_model_hints(info, model)
242
+
243
+
244
+ def load_model_info(model: str) -> dict[str, Any] | None:
245
+ if model in _CACHE:
246
+ return _CACHE[model]
247
+ path = MODEL_INFO_DIR / f"{_safe_name(model)}.json"
248
+ if not path.exists():
249
+ return None
250
+ try:
251
+ data = json.loads(path.read_text(encoding="utf-8"))
252
+ _CACHE[model] = data
253
+ return data
254
+ except Exception:
255
+ return None
256
+
257
+
258
+ def save_model_info(model: str, info: dict[str, Any]) -> None:
259
+ MODEL_INFO_DIR.mkdir(parents=True, exist_ok=True)
260
+ path = MODEL_INFO_DIR / f"{_safe_name(model)}.json"
261
+ try:
262
+ _atomic_write_text(path, json.dumps(info, ensure_ascii=False, indent=2))
263
+ _CACHE[model] = info
264
+ except Exception:
265
+ pass
266
+
267
+
268
+ def ensure_model_info(client: Any, model: str, *, cloud: bool = False) -> dict[str, Any]:
269
+ """Return cached model info, fetching via ollama show / SDK if needed."""
270
+ info = load_model_info(model)
271
+ if info and "error" not in info and get_context_length(info):
272
+ return merge_model_hints(info, model)
273
+ info = fetch_model_info(client, model, cloud=cloud)
274
+ if "error" not in info and get_context_length(info):
275
+ save_model_info(model, info)
276
+ write_model_record(model, info)
277
+ return info
278
+
279
+
280
+ def write_model_record(model: str, info: dict[str, Any]) -> None:
281
+ """Write a markdown harness record for this model to CONFIG_DIR/models/."""
282
+ MODELS_DIR.mkdir(parents=True, exist_ok=True)
283
+ safe = _safe_name(model)
284
+ path = MODELS_DIR / f"{safe}.md"
285
+
286
+ family = info.get("family", "")
287
+ params = info.get("parameter_size", "")
288
+ quant = info.get("quantization", "")
289
+ ctx = info.get("context_length")
290
+ thinks = info.get("supports_thinking", False)
291
+ vision = info.get("supports_vision", False)
292
+
293
+ description_parts = []
294
+ if params:
295
+ description_parts.append(f"{params} parameters")
296
+ if quant:
297
+ description_parts.append(quant)
298
+ if ctx:
299
+ description_parts.append(f"{ctx:,} context")
300
+ if thinks:
301
+ description_parts.append("supports thinking")
302
+ if vision:
303
+ description_parts.append("supports vision")
304
+ description = ", ".join(description_parts) or "local Ollama model"
305
+
306
+ tags = ["model", "ollama"]
307
+ if family:
308
+ tags.append(family)
309
+ if thinks:
310
+ tags.append("thinking")
311
+ if vision:
312
+ tags.append("vision")
313
+
314
+ content = (
315
+ f"---\n"
316
+ f"id: ollama:model:{safe}\n"
317
+ f"harness: algo-cli\n"
318
+ f"kind: model\n"
319
+ f"title: {model}\n"
320
+ f"description: {description}\n"
321
+ f"tags: [{', '.join(tags)}]\n"
322
+ f"---\n"
323
+ f"# {model}\n\n"
324
+ f"**Family:** {family or '?'} \n"
325
+ f"**Parameters:** {params or '?'} \n"
326
+ f"**Quantization:** {quant or '?'} \n"
327
+ f"**Context Length:** {ctx or '?'} \n"
328
+ f"**Supports Thinking:** {'yes' if thinks else 'no'} \n"
329
+ f"**Supports Vision:** {'yes' if vision else 'no'} \n"
330
+ f"**Supports Tools:** yes \n"
331
+ )
332
+
333
+ try:
334
+ _atomic_write_text(path, content)
335
+ except Exception:
336
+ pass
337
+
338
+
339
+ def get_context_length(info: dict[str, Any]) -> int | None:
340
+ raw = info.get("context_length")
341
+ if isinstance(raw, int) and raw > 0:
342
+ return raw
343
+ return None
344
+
345
+
346
+ def cloud_model_hints(model: str) -> dict[str, Any]:
347
+ """Static metadata from Ollama library when show() returns incomplete fields."""
348
+ bare = _bare_model_name(model)
349
+ hinted = _CLOUD_MODEL_HINTS.get(bare)
350
+ return dict(hinted) if hinted else {}
351
+
352
+
353
+ def merge_model_hints(info: dict[str, Any], model: str) -> dict[str, Any]:
354
+ """Fill missing context/thinking fields from cloud hints without overwriting API data."""
355
+ merged = dict(info)
356
+ for key, value in cloud_model_hints(model).items():
357
+ if merged.get(key) in (None, "", 0):
358
+ merged[key] = value
359
+ return merged
360
+
361
+
362
+ def resolve_model_info(cfg: Any, client: Any | None) -> dict[str, Any]:
363
+ """Best-effort model metadata for footer/status (xAI synth, ollama show, SDK, cache)."""
364
+ model = str(getattr(cfg, "model", "") or "")
365
+ if not model:
366
+ return {}
367
+ if is_xai_model(model):
368
+ return synthesize_xai_info(model)
369
+ if is_chatgpt_model(model):
370
+ return synthesize_chatgpt_info(model)
371
+
372
+ cloud = bool(getattr(cfg, "cloud", False))
373
+ try:
374
+ info = ensure_model_info(client, model, cloud=cloud) if client is not None else None
375
+ if info and "error" not in info and get_context_length(info):
376
+ return info
377
+ except Exception:
378
+ pass
379
+
380
+ cli_info = fetch_model_info_from_cli(model, cloud=cloud)
381
+ if cli_info:
382
+ merged = merge_model_hints(cli_info, model)
383
+ if "error" not in merged:
384
+ save_model_info(model, merged)
385
+ return merged
386
+
387
+ cached = load_model_info(model)
388
+ if cached and "error" not in cached:
389
+ return merge_model_hints(cached, model)
390
+
391
+ fetched = fetch_model_info(client, model, cloud=cloud)
392
+ if fetched and "error" not in fetched:
393
+ if get_context_length(fetched):
394
+ save_model_info(model, fetched)
395
+ return fetched
396
+ return {"name": model}
397
+
398
+
399
+ def is_cloud_model_name(name: str) -> bool:
400
+ lowered = (name or "").lower()
401
+ return lowered.endswith(":cloud") or lowered.endswith("-cloud") or ":cloud-" in lowered
402
+
403
+
404
+ def effective_context_limits(
405
+ cfg: Any,
406
+ model_info: dict[str, Any] | None = None,
407
+ ) -> tuple[int, int | None]:
408
+ """Return (runtime_limit, model_native_limit).
409
+
410
+ runtime_limit matches the window agent_loop actually requests: the
411
+ model-adaptive profile window when model_adaptive is on (which already
412
+ honors explicit /ctx overrides), else cfg.num_ctx — capped at the model's
413
+ native context when known. Compaction and the footer must use this value,
414
+ not the native window, or long sessions silently truncate before
415
+ compaction ever fires.
416
+ """
417
+ cfg_limit = max(int(getattr(cfg, "num_ctx", 8192) or 8192), 1)
418
+ if getattr(cfg, "model_adaptive", False):
419
+ try:
420
+ from . import model_profile as _model_profile
421
+
422
+ cfg_limit = max(int(_model_profile.effective_params(cfg, model_info).num_ctx), 1)
423
+ except Exception:
424
+ pass
425
+ native = get_context_length(model_info or {})
426
+ if native is None:
427
+ return cfg_limit, None
428
+ return min(cfg_limit, native), native
429
+
430
+
431
+ def parameter_size_billions(info: dict[str, Any]) -> float | None:
432
+ """Parse parameter_size string like '8.2B', '70B', '235B', '500M', '1T' → billions.
433
+
434
+ Also handles bare raw counts (e.g. '756,162,687,872' from ``ollama show``)
435
+ by treating them as the exact parameter count and dividing by 1e9.
436
+
437
+ Returns None when the field is absent or unparseable.
438
+ """
439
+ raw = info.get("parameter_size", "")
440
+ if not raw or not isinstance(raw, str):
441
+ return None
442
+ cleaned = raw.strip().replace(",", "")
443
+ m = _SIZE_RE.search(cleaned)
444
+ if m:
445
+ try:
446
+ val = float(m.group(1))
447
+ except ValueError:
448
+ return None
449
+ unit = m.group(2).upper()
450
+ if unit == "T":
451
+ return val * 1000.0
452
+ if unit == "B":
453
+ return val
454
+ if unit == "M":
455
+ return val / 1000.0
456
+ if unit == "K":
457
+ return val / 1_000_000.0
458
+ return val
459
+ # No unit suffix — try parsing as a raw parameter count.
460
+ try:
461
+ return float(cleaned) / 1e9
462
+ except ValueError:
463
+ return None
464
+
465
+
466
+ def supports_thinking(info: dict[str, Any]) -> bool:
467
+ # If we have no model info (cloud mode, show() failed), don't suppress thinking —
468
+ # return True so cfg.show_thinking is respected as-is.
469
+ if "supports_thinking" not in info:
470
+ return True
471
+ return bool(info["supports_thinking"])
472
+
473
+
474
+ def is_gemini_model(model: str) -> bool:
475
+ """Detect Gemini-family models by name (e.g. 'gemini-3-flash-preview:cloud').
476
+
477
+ Gemini routed via Ollama Cloud requires preserving thought_signature on every
478
+ tool-call round-trip; the Ollama Python SDK does not currently surface that
479
+ field. Disable thinking for these models to avoid 400 errors on round 2.
480
+ """
481
+ if not model or not isinstance(model, str):
482
+ return False
483
+ bare = model.split(":", 1)[0]
484
+ return bool(_GEMINI_NAME_RE.match(bare))
485
+
486
+
487
+ def is_xai_model(model: str) -> bool:
488
+ """Detect Grok models routed through xAI (e.g. 'grok-4-latest', 'grok-3').
489
+
490
+ Grok models use OAuth auth (xai_auth) against api.x.ai/v1 instead of the
491
+ local Ollama daemon. The agent loop branches on this to swap clients.
492
+ """
493
+ if not model or not isinstance(model, str):
494
+ return False
495
+ bare = model.split(":", 1)[0]
496
+ return bool(_GROK_NAME_RE.match(bare))
497
+
498
+
499
+ def is_chatgpt_model(model: str) -> bool:
500
+ """Detect ChatGPT/OpenAI models routed through ChatGPT OAuth.
501
+
502
+ This intentionally excludes gpt-oss so Ollama Cloud/open-weight models are
503
+ not stolen by the OpenAI provider route.
504
+ """
505
+ if not model or not isinstance(model, str):
506
+ return False
507
+ bare = model.split(":", 1)[0]
508
+ return bool(_CHATGPT_NAME_RE.match(bare))
509
+
510
+
511
+ _CHATGPT_CONTEXT_LENGTHS: dict[str, int] = {
512
+ "gpt-5.5": 1_000_000,
513
+ "gpt-5.4": 1_000_000,
514
+ "gpt-5.4-mini": 400_000,
515
+ }
516
+
517
+
518
+ def synthesize_chatgpt_info(model: str) -> dict[str, Any]:
519
+ """Build a model_info dict for ChatGPT/OpenAI models (no client.show())."""
520
+ bare = _bare_model_name(model)
521
+ return {
522
+ "name": model,
523
+ "family": "chatgpt",
524
+ "families": ["chatgpt", "openai"],
525
+ "parameter_size": "",
526
+ "quantization": "",
527
+ "context_length": _CHATGPT_CONTEXT_LENGTHS.get(bare, 128_000),
528
+ "supports_thinking": True,
529
+ "supports_vision": False,
530
+ "supports_tools": True,
531
+ "provider": "chatgpt",
532
+ }
533
+
534
+
535
+ def synthesize_xai_info(model: str) -> dict[str, Any]:
536
+ """Build a model_info dict for xAI Grok models (no client.show() available)."""
537
+ return {
538
+ "name": model,
539
+ "family": "grok",
540
+ "families": ["grok"],
541
+ "parameter_size": "",
542
+ "quantization": "",
543
+ "context_length": 131072,
544
+ "supports_thinking": True,
545
+ "supports_vision": False,
546
+ "supports_tools": True,
547
+ "provider": "xai",
548
+ }