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.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
}
|