differix 0.1.0a1__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.
- agents/editor.py +455 -0
- agents/researcher.py +405 -0
- agents/reviewer.py +374 -0
- cli/model_cli.py +430 -0
- config.py +557 -0
- differix-0.1.0a1.dist-info/METADATA +123 -0
- differix-0.1.0a1.dist-info/RECORD +62 -0
- differix-0.1.0a1.dist-info/WHEEL +5 -0
- differix-0.1.0a1.dist-info/entry_points.txt +2 -0
- differix-0.1.0a1.dist-info/top_level.txt +6 -0
- main.py +2796 -0
- memory/__init__.py +59 -0
- memory/embeddings/__init__.py +29 -0
- memory/embeddings/factory.py +178 -0
- memory/embeddings/providers.py +259 -0
- memory/manager.py +186 -0
- memory/migrator.py +253 -0
- memory/ranker.py +203 -0
- memory/schemas.py +127 -0
- memory/storage.py +552 -0
- tools/agents_chat.py +152 -0
- tools/asset_extractor.py +545 -0
- tools/asset_manager.py +115 -0
- tools/atomic_changeset.py +264 -0
- tools/backup_manager.py +122 -0
- tools/changelog_engine.py +261 -0
- tools/coherence_checker.py +771 -0
- tools/compiler.py +531 -0
- tools/component_normalizer.py +130 -0
- tools/context_assembler.py +631 -0
- tools/contract_validator.py +478 -0
- tools/cost_predictor.py +141 -0
- tools/drift_guard.py +196 -0
- tools/file_manager.py +119 -0
- tools/handshake.py +351 -0
- tools/lesson_manager.py +570 -0
- tools/lesson_memory_bridge.py +398 -0
- tools/llm_client.py +103 -0
- tools/llm_selector.py +362 -0
- tools/model_ranker.py +535 -0
- tools/palette_extractor.py +1043 -0
- tools/plugin_manager.py +204 -0
- tools/project_context_gatherer.py +404 -0
- tools/project_scanner.py +41 -0
- tools/project_structure.py +244 -0
- tools/reasoning_logger.py +78 -0
- tools/reflection.py +452 -0
- tools/sandbox_utils.py +56 -0
- tools/security/__init__.py +0 -0
- tools/security/behavior_guard.py +83 -0
- tools/security/verification_firewall.py +113 -0
- tools/security_filter.py +213 -0
- tools/session_adr.py +311 -0
- tools/skill_manager.py +129 -0
- tools/state_manager.py +383 -0
- tools/structure_index.py +570 -0
- tools/test_schema_consistency.py +19 -0
- tools/verification_firewall.py +113 -0
- tools/visual_capture.py +65 -0
- tools/visual_diff.py +220 -0
- tools/visual_reviewer.py +79 -0
- tools/web_search.py +371 -0
cli/model_cli.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import shlex
|
|
5
|
+
from typing import Dict, Any, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
# NOTE: the actual data/caching layer lives in llm_selector.py (fetches and
|
|
8
|
+
# caches the OpenRouter catalog, and now also owns agent<->model bindings).
|
|
9
|
+
# This used to point at a "core.model_fetcher" / "model_fetcher" module that
|
|
10
|
+
# never existed in this project, which meant every import silently failed
|
|
11
|
+
# and fell through to a hardcoded stub -- complete with a fake cache path
|
|
12
|
+
# llm_selector.py never wrote to, and a fake agent roster ("director",
|
|
13
|
+
# "scriptwriter", "critic", "visual_designer", "voiceover") that has nothing
|
|
14
|
+
# to do with the real pipeline. That's why `llm` showed no context/pricing:
|
|
15
|
+
# it was reading an empty stub, not your real catalog.
|
|
16
|
+
try:
|
|
17
|
+
from core.llm_selector import (
|
|
18
|
+
fetch_openrouter_models,
|
|
19
|
+
get_cached_models,
|
|
20
|
+
get_model_details,
|
|
21
|
+
sync_model_catalog,
|
|
22
|
+
get_agent_bindings,
|
|
23
|
+
save_agent_bindings,
|
|
24
|
+
DEFAULT_AGENTS,
|
|
25
|
+
)
|
|
26
|
+
except ImportError:
|
|
27
|
+
from tools.llm_selector import (
|
|
28
|
+
fetch_openrouter_models,
|
|
29
|
+
get_cached_models,
|
|
30
|
+
get_model_details,
|
|
31
|
+
sync_model_catalog,
|
|
32
|
+
get_agent_bindings,
|
|
33
|
+
save_agent_bindings,
|
|
34
|
+
DEFAULT_AGENTS,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Terminal ANSI Color codes
|
|
39
|
+
ANSI_RESET = "\033[0m"
|
|
40
|
+
ANSI_BOLD = "\033[1m"
|
|
41
|
+
ANSI_CYAN = "\033[36m"
|
|
42
|
+
ANSI_GREEN = "\033[32m"
|
|
43
|
+
ANSI_YELLOW = "\033[33m"
|
|
44
|
+
ANSI_RED = "\033[31m"
|
|
45
|
+
ANSI_MAGENTA = "\033[35m"
|
|
46
|
+
ANSI_GRAY = "\033[90m"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def format_price(val: Any) -> str:
|
|
50
|
+
"""Format an already-per-1M-token price (as llm_selector's catalog stores it)."""
|
|
51
|
+
try:
|
|
52
|
+
per_m = float(val)
|
|
53
|
+
if per_m == 0:
|
|
54
|
+
return "$0.00"
|
|
55
|
+
if per_m < 0.01:
|
|
56
|
+
return f"${per_m:.4f}"
|
|
57
|
+
return f"${per_m:.2f}"
|
|
58
|
+
except (ValueError, TypeError):
|
|
59
|
+
return "N/A"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def format_context(ctx: Any) -> str:
|
|
63
|
+
"""Format context length into human-readable representation."""
|
|
64
|
+
try:
|
|
65
|
+
c = int(ctx)
|
|
66
|
+
if c >= 1_000_000:
|
|
67
|
+
return f"{c / 1_000_000:.1f}M"
|
|
68
|
+
if c >= 1000:
|
|
69
|
+
return f"{c // 1000}k"
|
|
70
|
+
return str(c)
|
|
71
|
+
except (ValueError, TypeError):
|
|
72
|
+
return "N/A"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def print_header(title: str) -> None:
|
|
76
|
+
print(f"\n{ANSI_BOLD}{ANSI_CYAN}=== {title} ==={ANSI_RESET}\n")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def display_llm_status() -> None:
|
|
80
|
+
"""Display current agent model bindings with context and pricing specifications."""
|
|
81
|
+
print_header("VegeX Multi-Agent LLM Model Bindings")
|
|
82
|
+
|
|
83
|
+
bindings = get_agent_bindings()
|
|
84
|
+
cached_models = get_cached_models() # dict: model_id -> spec
|
|
85
|
+
|
|
86
|
+
header_fmt = "{:<16} {:<38} {:<12} {:<12} {:<12}"
|
|
87
|
+
print(
|
|
88
|
+
ANSI_BOLD
|
|
89
|
+
+ header_fmt.format("AGENT", "MODEL ID", "CONTEXT", "INPUT/1M", "OUTPUT/1M")
|
|
90
|
+
+ ANSI_RESET
|
|
91
|
+
)
|
|
92
|
+
print("-" * 94)
|
|
93
|
+
|
|
94
|
+
if not cached_models:
|
|
95
|
+
print(f"{ANSI_YELLOW}[!] No local catalog cached yet. Run 'llm sync' to fetch context/pricing data.{ANSI_RESET}\n")
|
|
96
|
+
|
|
97
|
+
for agent, model_id in bindings.items():
|
|
98
|
+
model_info = cached_models.get(model_id)
|
|
99
|
+
if model_info:
|
|
100
|
+
context = format_context(model_info.get("context_length", "N/A"))
|
|
101
|
+
in_cost = format_price(model_info.get("price_input_1m", 0))
|
|
102
|
+
out_cost = format_price(model_info.get("price_output_1m", 0))
|
|
103
|
+
else:
|
|
104
|
+
context = "Unknown"
|
|
105
|
+
in_cost = "Unknown"
|
|
106
|
+
out_cost = "Unknown"
|
|
107
|
+
|
|
108
|
+
print(
|
|
109
|
+
f"{ANSI_GREEN}{agent:<16}{ANSI_RESET} "
|
|
110
|
+
f"{ANSI_BOLD}{model_id:<38}{ANSI_RESET} "
|
|
111
|
+
f"{context:<12} "
|
|
112
|
+
f"{in_cost:<12} "
|
|
113
|
+
f"{out_cost:<12}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
print("-" * 94)
|
|
117
|
+
print(f"{ANSI_GRAY}Run 'llm <agent> select' to change an agent's model or 'llm sync' to update catalog.{ANSI_RESET}\n")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def sync_catalog_command() -> None:
|
|
121
|
+
"""Force sync local catalog from OpenRouter API."""
|
|
122
|
+
print(f"{ANSI_CYAN}[*] Synchronizing OpenRouter catalog...{ANSI_RESET}")
|
|
123
|
+
models = sync_model_catalog()
|
|
124
|
+
print(f"{ANSI_GREEN}[+] Catalog updated successfully. Total models: {len(models)}{ANSI_RESET}\n")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def filter_models(
|
|
128
|
+
models: Dict[str, Dict[str, Any]],
|
|
129
|
+
query: Optional[str] = None,
|
|
130
|
+
provider: Optional[str] = None,
|
|
131
|
+
max_input_price: Optional[float] = None,
|
|
132
|
+
min_context: Optional[int] = None,
|
|
133
|
+
) -> List[Dict[str, Any]]:
|
|
134
|
+
"""Filter the cached catalog (model_id -> spec dict) by provider, query
|
|
135
|
+
term, price, and context window. Returns a list of specs with 'id'
|
|
136
|
+
folded in, sorted by model_id, for stable display ordering."""
|
|
137
|
+
filtered = []
|
|
138
|
+
for m_id, spec in models.items():
|
|
139
|
+
m_id_l = m_id.lower()
|
|
140
|
+
m_name = str(spec.get("name", "")).lower()
|
|
141
|
+
context_len = spec.get("context_length", 0) or 0
|
|
142
|
+
# Catalog already stores price per 1M tokens (see llm_selector.py) --
|
|
143
|
+
# no per-token conversion here, unlike the raw OpenRouter response.
|
|
144
|
+
prompt_cost = float(spec.get("price_input_1m", 0) or 0)
|
|
145
|
+
|
|
146
|
+
if query:
|
|
147
|
+
q = query.lower()
|
|
148
|
+
if q not in m_id_l and q not in m_name:
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
if provider:
|
|
152
|
+
p = provider.lower()
|
|
153
|
+
if not m_id_l.startswith(p + "/") and p not in m_id_l:
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
if max_input_price is not None and prompt_cost > max_input_price:
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
if min_context is not None and context_len < min_context:
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
entry = dict(spec)
|
|
163
|
+
entry["id"] = m_id
|
|
164
|
+
filtered.append(entry)
|
|
165
|
+
|
|
166
|
+
filtered.sort(key=lambda e: e["id"])
|
|
167
|
+
return filtered
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def interactive_select_model(agent_name: str) -> None:
|
|
171
|
+
"""Interactive filter and selection prompt for an agent."""
|
|
172
|
+
models = get_cached_models()
|
|
173
|
+
if not models:
|
|
174
|
+
print(f"{ANSI_YELLOW}[*] No local catalog found. Fetching from OpenRouter...{ANSI_RESET}")
|
|
175
|
+
models = sync_model_catalog()
|
|
176
|
+
|
|
177
|
+
if not models:
|
|
178
|
+
print(f"{ANSI_RED}[!] Error: Failed to retrieve models from catalog.{ANSI_RESET}")
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
bindings = get_agent_bindings()
|
|
182
|
+
current_model = bindings.get(agent_name, "None")
|
|
183
|
+
|
|
184
|
+
print_header(f"Model Selection for Agent: {agent_name.upper()}")
|
|
185
|
+
print(f"Current Binding: {ANSI_GREEN}{current_model}{ANSI_RESET}\n")
|
|
186
|
+
|
|
187
|
+
query = ""
|
|
188
|
+
provider = ""
|
|
189
|
+
max_price = None
|
|
190
|
+
min_context = None
|
|
191
|
+
|
|
192
|
+
while True:
|
|
193
|
+
results = filter_models(
|
|
194
|
+
models,
|
|
195
|
+
query=query if query else None,
|
|
196
|
+
provider=provider if provider else None,
|
|
197
|
+
max_input_price=max_price,
|
|
198
|
+
min_context=min_context,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
print(f"\n{ANSI_BOLD}Filtered Models ({len(results)} found):{ANSI_RESET}")
|
|
202
|
+
print(f"Filters -> Query: '{query or 'all'}', Provider: '{provider or 'all'}', Max $/1M: {max_price or 'any'}, Min Ctx: {min_context or 'any'}")
|
|
203
|
+
print("-" * 90)
|
|
204
|
+
print(
|
|
205
|
+
ANSI_BOLD
|
|
206
|
+
+ "{:<4} {:<42} {:<12} {:<12} {:<12}".format(
|
|
207
|
+
"#", "MODEL ID", "CONTEXT", "INPUT/1M", "OUTPUT/1M"
|
|
208
|
+
)
|
|
209
|
+
+ ANSI_RESET
|
|
210
|
+
)
|
|
211
|
+
print("-" * 90)
|
|
212
|
+
|
|
213
|
+
display_slice = results[:20]
|
|
214
|
+
for idx, m in enumerate(display_slice, 1):
|
|
215
|
+
m_id = m.get("id", "")
|
|
216
|
+
ctx = format_context(m.get("context_length", 0))
|
|
217
|
+
in_p = format_price(m.get("price_input_1m", 0))
|
|
218
|
+
out_p = format_price(m.get("price_output_1m", 0))
|
|
219
|
+
|
|
220
|
+
highlight = ANSI_GREEN if m_id == current_model else ""
|
|
221
|
+
reset = ANSI_RESET if m_id == current_model else ""
|
|
222
|
+
|
|
223
|
+
print(
|
|
224
|
+
f"{highlight}{idx:<4} {m_id:<42} {ctx:<12} {in_p:<12} {out_p:<12}{reset}"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if len(results) > 20:
|
|
228
|
+
print(f"{ANSI_GRAY}... and {len(results) - 20} more models. Refine search query.{ANSI_RESET}")
|
|
229
|
+
|
|
230
|
+
print("\nCommands / Options:")
|
|
231
|
+
print(" [1-20] : Select model by list number")
|
|
232
|
+
print(" /search <term> : Filter by model name or ID")
|
|
233
|
+
print(" /provider <name> : Filter by provider (e.g. anthropic, openai, meta-llama, mistralai)")
|
|
234
|
+
print(" /maxprice <num> : Max prompt price per 1M tokens (e.g. 5.0)")
|
|
235
|
+
print(" /minctx <num> : Minimum context window (e.g. 128000)")
|
|
236
|
+
print(" /reset : Clear all filters")
|
|
237
|
+
print(" /cancel : Exit without changing")
|
|
238
|
+
|
|
239
|
+
choice = input(f"\n{ANSI_CYAN}Select option or command > {ANSI_RESET}").strip()
|
|
240
|
+
|
|
241
|
+
if not choice or choice.lower() == "/cancel":
|
|
242
|
+
print(f"{ANSI_YELLOW}Selection canceled. Model unchanged.{ANSI_RESET}\n")
|
|
243
|
+
break
|
|
244
|
+
|
|
245
|
+
if choice.lower() == "/reset":
|
|
246
|
+
query = ""
|
|
247
|
+
provider = ""
|
|
248
|
+
max_price = None
|
|
249
|
+
min_context = None
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
if choice.startswith("/search"):
|
|
253
|
+
parts = choice.split(maxsplit=1)
|
|
254
|
+
query = parts[1].strip() if len(parts) > 1 else ""
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
if choice.startswith("/provider"):
|
|
258
|
+
parts = choice.split(maxsplit=1)
|
|
259
|
+
provider = parts[1].strip() if len(parts) > 1 else ""
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
if choice.startswith("/maxprice"):
|
|
263
|
+
parts = choice.split(maxsplit=1)
|
|
264
|
+
if len(parts) > 1:
|
|
265
|
+
try:
|
|
266
|
+
max_price = float(parts[1].strip())
|
|
267
|
+
except ValueError:
|
|
268
|
+
print(f"{ANSI_RED}Invalid price number.{ANSI_RESET}")
|
|
269
|
+
else:
|
|
270
|
+
max_price = None
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
if choice.startswith("/minctx"):
|
|
274
|
+
parts = choice.split(maxsplit=1)
|
|
275
|
+
if len(parts) > 1:
|
|
276
|
+
try:
|
|
277
|
+
min_context = int(parts[1].strip())
|
|
278
|
+
except ValueError:
|
|
279
|
+
print(f"{ANSI_RED}Invalid context length number.{ANSI_RESET}")
|
|
280
|
+
else:
|
|
281
|
+
min_context = None
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
if choice.isdigit():
|
|
285
|
+
idx = int(choice)
|
|
286
|
+
if 1 <= idx <= len(display_slice):
|
|
287
|
+
selected = display_slice[idx - 1]
|
|
288
|
+
selected_id = selected.get("id")
|
|
289
|
+
set_agent_model(agent_name, selected_id)
|
|
290
|
+
break
|
|
291
|
+
else:
|
|
292
|
+
print(f"{ANSI_RED}Index out of range. Enter a number between 1 and {len(display_slice)}.{ANSI_RESET}")
|
|
293
|
+
else:
|
|
294
|
+
# Check if user directly typed a model id verbatim
|
|
295
|
+
if choice in models:
|
|
296
|
+
set_agent_model(agent_name, choice)
|
|
297
|
+
break
|
|
298
|
+
else:
|
|
299
|
+
# Default to searching with input
|
|
300
|
+
query = choice
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def set_agent_model(agent_name: str, model_id: str) -> None:
|
|
304
|
+
"""Set model ID for a specific agent and persist, preserving other bindings."""
|
|
305
|
+
from tools.llm_selector import CONFIG_FILE
|
|
306
|
+
|
|
307
|
+
cfg = {}
|
|
308
|
+
if os.path.exists(CONFIG_FILE):
|
|
309
|
+
try:
|
|
310
|
+
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
311
|
+
cfg = json.load(f)
|
|
312
|
+
if not isinstance(cfg, dict):
|
|
313
|
+
cfg = {}
|
|
314
|
+
except Exception:
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
if "bindings" not in cfg:
|
|
318
|
+
cfg["bindings"] = {}
|
|
319
|
+
|
|
320
|
+
cfg["bindings"][agent_name] = model_id
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
324
|
+
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
|
325
|
+
except Exception as e:
|
|
326
|
+
print(f"{ANSI_RED}Error saving bindings: {e}{ANSI_RESET}")
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
print(f"\n{ANSI_GREEN}[+] Agent '{agent_name}' is now bound to '{model_id}'{ANSI_RESET}\n")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def sync_active_models_specs(agents: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
333
|
+
"""Refreshes local pricing/context specs for whichever models are
|
|
334
|
+
currently bound to the active agents, so 'llm status' and cost tracking
|
|
335
|
+
don't keep showing stale/'Unknown' numbers.
|
|
336
|
+
|
|
337
|
+
Only triggers a full catalog re-fetch when the local cache is empty or
|
|
338
|
+
is missing a spec for a currently-bound model - cheap to call on every
|
|
339
|
+
run_agent_cycle() start, which is what main.py does.
|
|
340
|
+
|
|
341
|
+
`agents` optionally restricts the check to a subset of bound agent keys;
|
|
342
|
+
omit it to check every currently bound agent.
|
|
343
|
+
"""
|
|
344
|
+
bindings = get_agent_bindings()
|
|
345
|
+
if agents:
|
|
346
|
+
bindings = {a: m for a, m in bindings.items() if a in agents}
|
|
347
|
+
|
|
348
|
+
cached_ids = set(get_cached_models().keys())
|
|
349
|
+
active_ids = {model_id for model_id in bindings.values() if model_id}
|
|
350
|
+
missing_ids = active_ids - cached_ids
|
|
351
|
+
|
|
352
|
+
synced = False
|
|
353
|
+
if not cached_ids or missing_ids:
|
|
354
|
+
sync_model_catalog()
|
|
355
|
+
synced = True
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
"agents_checked": list(bindings.keys()),
|
|
359
|
+
"models_checked": list(active_ids),
|
|
360
|
+
"synced": synced,
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def handle_llm_command(args: Union[str, List[str], None]) -> None:
|
|
365
|
+
"""Main CLI router for 'llm' commands.
|
|
366
|
+
|
|
367
|
+
Accepts either a pre-split list of args (["researcher", "select"]) or a
|
|
368
|
+
raw command-line string ("researcher select"), since main.py's REPL
|
|
369
|
+
passes the latter (cmd_line.split(maxsplit=1) leaves the tail as one
|
|
370
|
+
string, and the __main__ entrypoint passes " ".join(sys.argv[2:])).
|
|
371
|
+
Without this, args[0] on a raw string silently returns its first
|
|
372
|
+
character instead of its first word.
|
|
373
|
+
"""
|
|
374
|
+
if args is None:
|
|
375
|
+
args = []
|
|
376
|
+
elif isinstance(args, str):
|
|
377
|
+
args = shlex.split(args) if args.strip() else []
|
|
378
|
+
|
|
379
|
+
if not args:
|
|
380
|
+
display_llm_status()
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
subcmd = args[0].lower()
|
|
384
|
+
|
|
385
|
+
if subcmd in ["status", "list", "show"]:
|
|
386
|
+
display_llm_status()
|
|
387
|
+
elif subcmd in ["sync", "refresh", "update"]:
|
|
388
|
+
sync_catalog_command()
|
|
389
|
+
elif subcmd in ["select", "choose", "pick"]:
|
|
390
|
+
if len(args) > 1:
|
|
391
|
+
interactive_select_model(args[1])
|
|
392
|
+
else:
|
|
393
|
+
print("Select an agent:")
|
|
394
|
+
bindings = get_agent_bindings()
|
|
395
|
+
for idx, agent in enumerate(bindings.keys(), 1):
|
|
396
|
+
print(f" {idx}. {agent}")
|
|
397
|
+
choice = input("Enter agent name or number: ").strip()
|
|
398
|
+
agent_list = list(bindings.keys())
|
|
399
|
+
if choice.isdigit() and 1 <= int(choice) <= len(agent_list):
|
|
400
|
+
interactive_select_model(agent_list[int(choice) - 1])
|
|
401
|
+
elif choice in bindings:
|
|
402
|
+
interactive_select_model(choice)
|
|
403
|
+
else:
|
|
404
|
+
print(f"{ANSI_RED}Invalid agent choice.{ANSI_RESET}")
|
|
405
|
+
elif len(args) >= 2 and args[1].lower() in ["select", "pick", "choose"]:
|
|
406
|
+
agent = args[0]
|
|
407
|
+
interactive_select_model(agent)
|
|
408
|
+
elif len(args) >= 3 and args[1].lower() == "set":
|
|
409
|
+
agent = args[0]
|
|
410
|
+
model_id = args[2]
|
|
411
|
+
set_agent_model(agent, model_id)
|
|
412
|
+
elif len(args) == 2 and args[0] not in ["set", "select", "sync", "status"]:
|
|
413
|
+
set_agent_model(args[0], args[1])
|
|
414
|
+
else:
|
|
415
|
+
print(f"{ANSI_RED}Unknown command: {' '.join(args)}{ANSI_RESET}")
|
|
416
|
+
print("Usage:")
|
|
417
|
+
print(" llm Display current bindings")
|
|
418
|
+
print(" llm sync Refresh OpenRouter catalog")
|
|
419
|
+
print(" llm <agent> select Interactive model selector")
|
|
420
|
+
print(" llm <agent> set <model_id> Direct model assignment")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def main() -> None:
|
|
424
|
+
"""Standalone entry point for CLI."""
|
|
425
|
+
args = sys.argv[1:]
|
|
426
|
+
handle_llm_command(args)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
if __name__ == "__main__":
|
|
430
|
+
main()
|