vtx-coding-agent 0.1.1__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 (117) hide show
  1. vtx/__init__.py +63 -0
  2. vtx/async_utils.py +40 -0
  3. vtx/builtin_skills/github/SKILL.md +139 -0
  4. vtx/builtin_skills/init/SKILL.md +74 -0
  5. vtx/builtin_skills/review/SKILL.md +73 -0
  6. vtx/builtin_skills/skill-builder/SKILL.md +133 -0
  7. vtx/cli.py +90 -0
  8. vtx/config.py +741 -0
  9. vtx/context/__init__.py +15 -0
  10. vtx/context/_xml.py +8 -0
  11. vtx/context/agent_mds.py +128 -0
  12. vtx/context/git.py +64 -0
  13. vtx/context/loader.py +41 -0
  14. vtx/context/skills.py +423 -0
  15. vtx/core/__init__.py +47 -0
  16. vtx/core/compaction.py +89 -0
  17. vtx/core/errors.py +17 -0
  18. vtx/core/handoff.py +51 -0
  19. vtx/core/scratchpad.py +54 -0
  20. vtx/core/types.py +197 -0
  21. vtx/defaults/__init__.py +0 -0
  22. vtx/defaults/config.yml +53 -0
  23. vtx/diff_display.py +12 -0
  24. vtx/events.py +224 -0
  25. vtx/gh_cli.py +82 -0
  26. vtx/git_branch.py +90 -0
  27. vtx/headless.py +127 -0
  28. vtx/llm/__init__.py +93 -0
  29. vtx/llm/base.py +217 -0
  30. vtx/llm/context_length.py +150 -0
  31. vtx/llm/dynamic_models.py +735 -0
  32. vtx/llm/model_fetcher.py +279 -0
  33. vtx/llm/models.py +78 -0
  34. vtx/llm/oauth/__init__.py +59 -0
  35. vtx/llm/oauth/copilot.py +358 -0
  36. vtx/llm/oauth/dynamic.py +236 -0
  37. vtx/llm/oauth/openai.py +400 -0
  38. vtx/llm/phase_parser.py +270 -0
  39. vtx/llm/provider.yaml +280 -0
  40. vtx/llm/provider_catalog.py +230 -0
  41. vtx/llm/providers/__init__.py +45 -0
  42. vtx/llm/providers/anthropic_sdk.py +256 -0
  43. vtx/llm/providers/mock.py +249 -0
  44. vtx/llm/providers/openai_sdk.py +246 -0
  45. vtx/llm/providers/sanitize.py +14 -0
  46. vtx/llm/sdk/__init__.py +13 -0
  47. vtx/llm/sdk/anthropic.py +382 -0
  48. vtx/llm/sdk/base.py +82 -0
  49. vtx/llm/sdk/openai.py +344 -0
  50. vtx/llm/tool_parser.py +161 -0
  51. vtx/loop.py +272 -0
  52. vtx/notify.py +109 -0
  53. vtx/permissions.py +114 -0
  54. vtx/prompts/__init__.py +45 -0
  55. vtx/prompts/builder.py +86 -0
  56. vtx/prompts/env.py +58 -0
  57. vtx/prompts/identity.py +166 -0
  58. vtx/prompts/tooling.py +36 -0
  59. vtx/py.typed +0 -0
  60. vtx/runtime.py +580 -0
  61. vtx/session.py +868 -0
  62. vtx/sounds/completion.wav +0 -0
  63. vtx/sounds/error.wav +0 -0
  64. vtx/sounds/permission.wav +0 -0
  65. vtx/themes.py +1104 -0
  66. vtx/tools/__init__.py +68 -0
  67. vtx/tools/_read_image.py +106 -0
  68. vtx/tools/_tool_utils.py +90 -0
  69. vtx/tools/base.py +36 -0
  70. vtx/tools/bash.py +371 -0
  71. vtx/tools/edit.py +261 -0
  72. vtx/tools/find.py +132 -0
  73. vtx/tools/read.py +238 -0
  74. vtx/tools/skill.py +278 -0
  75. vtx/tools/web.py +238 -0
  76. vtx/tools/write.py +88 -0
  77. vtx/tools_manager.py +216 -0
  78. vtx/turn.py +789 -0
  79. vtx/ui/__init__.py +0 -0
  80. vtx/ui/agent_runner.py +417 -0
  81. vtx/ui/app.py +665 -0
  82. vtx/ui/app_protocol.py +29 -0
  83. vtx/ui/autocomplete.py +440 -0
  84. vtx/ui/blocks.py +735 -0
  85. vtx/ui/chat.py +613 -0
  86. vtx/ui/clipboard.py +59 -0
  87. vtx/ui/commands/__init__.py +100 -0
  88. vtx/ui/commands/auth.py +306 -0
  89. vtx/ui/commands/base.py +122 -0
  90. vtx/ui/commands/models.py +144 -0
  91. vtx/ui/commands/sessions.py +388 -0
  92. vtx/ui/commands/settings.py +286 -0
  93. vtx/ui/completion_ui.py +313 -0
  94. vtx/ui/export.py +703 -0
  95. vtx/ui/floating_list.py +370 -0
  96. vtx/ui/formatting.py +287 -0
  97. vtx/ui/input.py +760 -0
  98. vtx/ui/latex.py +349 -0
  99. vtx/ui/launch.py +108 -0
  100. vtx/ui/path_complete.py +228 -0
  101. vtx/ui/prompt_history.py +102 -0
  102. vtx/ui/queue_ui.py +141 -0
  103. vtx/ui/selection_mode.py +18 -0
  104. vtx/ui/session_ui.py +235 -0
  105. vtx/ui/startup.py +124 -0
  106. vtx/ui/styles.py +327 -0
  107. vtx/ui/tool_output.py +34 -0
  108. vtx/ui/tree.py +437 -0
  109. vtx/ui/welcome.py +51 -0
  110. vtx/ui/widgets.py +558 -0
  111. vtx/update_check.py +49 -0
  112. vtx/version.py +22 -0
  113. vtx_coding_agent-0.1.1.dist-info/METADATA +259 -0
  114. vtx_coding_agent-0.1.1.dist-info/RECORD +117 -0
  115. vtx_coding_agent-0.1.1.dist-info/WHEEL +4 -0
  116. vtx_coding_agent-0.1.1.dist-info/entry_points.txt +2 -0
  117. vtx_coding_agent-0.1.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,279 @@
1
+ """Auto-fetch model lists from provider /models endpoints.
2
+
3
+ When a provider has ``fetch_models: true`` in provider.yaml, this module
4
+ can fetch ``<base_url>/models``, parse the response, and cache the result
5
+ to ``~/.vtx/models/<provider>.json`` with a configurable TTL.
6
+
7
+ Fetching is opt-in: call ``refresh_provider_models(slug)`` or
8
+ ``refresh_all_provider_models()`` to trigger a fetch. The catalog
9
+ only reads from cache, never blocks on network I/O.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import logging
16
+ import os
17
+ import time
18
+ from contextlib import suppress
19
+ from dataclasses import asdict, dataclass
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ import httpx
24
+
25
+ from .models import ApiType, Model
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ CACHE_DIR = "models"
30
+ DEFAULT_COOLDOWN = 60 # minutes
31
+
32
+
33
+ @dataclass
34
+ class FetchedModel:
35
+ id: str
36
+ name: str
37
+ context_length: int = 0
38
+ max_output_tokens: int = 0
39
+ supports_images: bool = False
40
+
41
+
42
+ def _get_cache_dir() -> Path:
43
+ from ..config import get_config_dir
44
+
45
+ env = os.environ.get("VTX_MODELS_CACHE_DIR")
46
+ return Path(env) if env else get_config_dir() / CACHE_DIR
47
+
48
+
49
+ def _cache_path(provider_slug: str) -> Path:
50
+ return _get_cache_dir() / f"{provider_slug}.json"
51
+
52
+
53
+ def _read_cache(provider_slug: str) -> list[FetchedModel] | None:
54
+ path = _cache_path(provider_slug)
55
+ try:
56
+ data = json.loads(path.read_text(encoding="utf-8"))
57
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
58
+ return None
59
+
60
+ fetched_at = data.get("fetched_at", 0)
61
+ cooldown = data.get("cooldown_minutes", DEFAULT_COOLDOWN) * 60
62
+ if time.time() - fetched_at > cooldown:
63
+ return None
64
+
65
+ models = []
66
+ for entry in data.get("models", []):
67
+ models.append(
68
+ FetchedModel(
69
+ id=entry["id"],
70
+ name=entry.get("name", entry["id"]),
71
+ context_length=entry.get("context_length", 0),
72
+ max_output_tokens=entry.get("max_output_tokens", 0),
73
+ supports_images=entry.get("supports_images", False),
74
+ )
75
+ )
76
+ return models
77
+
78
+
79
+ def _write_cache(provider_slug: str, models: list[FetchedModel], cooldown_minutes: int) -> None:
80
+ path = _cache_path(provider_slug)
81
+ try:
82
+ path.parent.mkdir(parents=True, exist_ok=True)
83
+ data = {
84
+ "fetched_at": time.time(),
85
+ "cooldown_minutes": cooldown_minutes,
86
+ "models": [asdict(m) for m in models],
87
+ }
88
+ tmp = path.with_suffix(".json.tmp")
89
+ tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
90
+ tmp.replace(path)
91
+ except OSError as exc:
92
+ logger.warning("Failed to write model cache for %s: %s", provider_slug, exc)
93
+
94
+
95
+ def _resolve_api_key(provider) -> str | None:
96
+ if provider.api_key_env:
97
+ key = os.environ.get(provider.api_key_env)
98
+ if key:
99
+ return key
100
+ if provider.api_key_optional:
101
+ return "vtx-local"
102
+ return None
103
+
104
+
105
+ def _parse_models(raw_models: list[dict[str, Any]], parser_config) -> list[FetchedModel]:
106
+ models: list[FetchedModel] = []
107
+ for raw in raw_models:
108
+ model_id = raw.get(parser_config.id_field, "")
109
+ if not model_id:
110
+ continue
111
+
112
+ name = raw.get(parser_config.name_field, model_id)
113
+ if isinstance(name, str) and ":" in name:
114
+ candidate = name.split(":", 1)[1].strip()
115
+ if candidate:
116
+ name = candidate
117
+
118
+ context_length = 0
119
+ ctx_val = raw.get(parser_config.context_field)
120
+ if ctx_val is not None:
121
+ with suppress(TypeError, ValueError):
122
+ context_length = int(ctx_val)
123
+
124
+ max_output = 0
125
+ out_val = raw.get(parser_config.output_field)
126
+ if out_val is not None:
127
+ with suppress(TypeError, ValueError):
128
+ max_output = int(out_val)
129
+
130
+ supports_images = False
131
+ arch = raw.get("architecture")
132
+ if isinstance(arch, dict):
133
+ modalities = arch.get("input_modalities", [])
134
+ supports_images = "image" in modalities if isinstance(modalities, list) else False
135
+ if not supports_images:
136
+ modalities = raw.get("input_modalities")
137
+ supports_images = isinstance(modalities, list) and "image" in modalities
138
+
139
+ models.append(
140
+ FetchedModel(
141
+ id=model_id,
142
+ name=name,
143
+ context_length=context_length,
144
+ max_output_tokens=max_output,
145
+ supports_images=supports_images,
146
+ )
147
+ )
148
+
149
+ return models
150
+
151
+
152
+ def _raw_model_list(payload: Any) -> list[dict[str, Any]]:
153
+ if isinstance(payload, list):
154
+ return [m for m in payload if isinstance(m, dict)]
155
+ if isinstance(payload, dict):
156
+ for key in ("data", "models", "results"):
157
+ val = payload.get(key)
158
+ if isinstance(val, list):
159
+ return [m for m in val if isinstance(m, dict)]
160
+ return []
161
+
162
+
163
+ def _fetch_models_sync(provider) -> list[FetchedModel]:
164
+ """Fetch models from a provider's /models endpoint (sync, with network)."""
165
+ if not provider.fetch_models or not provider.base_url:
166
+ return []
167
+
168
+ api_key = _resolve_api_key(provider)
169
+ base = provider.base_url.rstrip("/")
170
+ url = f"{base}{provider.models_endpoint}"
171
+
172
+ headers = {"Accept": "application/json"}
173
+ if api_key and api_key != "vtx-local":
174
+ headers["Authorization"] = f"Bearer {api_key}"
175
+
176
+ try:
177
+ with httpx.Client(timeout=10.0) as client:
178
+ resp = client.get(url, headers=headers)
179
+ if resp.status_code >= 400:
180
+ logger.debug("Failed to fetch models for %s: %s", provider.slug, resp.status_code)
181
+ return []
182
+ payload = resp.json()
183
+ except Exception as exc:
184
+ logger.debug("Error fetching models for %s: %s", provider.slug, exc)
185
+ return []
186
+
187
+ raw_models = _raw_model_list(payload)
188
+ if not raw_models:
189
+ return []
190
+
191
+ models = _parse_models(raw_models, provider.model_parser)
192
+ cooldown = provider.model_parser.cooldown_minutes
193
+ _write_cache(provider.slug, models, cooldown)
194
+ return models
195
+
196
+
197
+ def refresh_provider_models(slug: str) -> int:
198
+ """Force-refresh a single provider's model cache. Returns model count."""
199
+ from .provider_catalog import get
200
+
201
+ provider = get(slug)
202
+ if provider is None or not provider.fetch_models:
203
+ return 0
204
+ models = _fetch_models_sync(provider)
205
+ return len(models)
206
+
207
+
208
+ def refresh_all_provider_models() -> dict[str, int]:
209
+ """Force-refresh all providers. Returns {slug: model_count}."""
210
+ from .provider_catalog import list_providers
211
+
212
+ results: dict[str, int] = {}
213
+ for p in list_providers():
214
+ if not p.fetch_models:
215
+ continue
216
+ models = _fetch_models_sync(p)
217
+ results[p.slug] = len(models)
218
+ return results
219
+
220
+
221
+ def get_fetched_models(provider) -> list[Model]:
222
+ """Get fetched models from cache only (no network). Returns empty if not cached."""
223
+ if not provider.fetch_models:
224
+ return []
225
+
226
+ cached = _read_cache(provider.slug)
227
+ if cached is None:
228
+ return []
229
+
230
+ family_to_api = {
231
+ "openai_compat": ApiType(ApiType.OPENAI_SDK),
232
+ "anthropic": ApiType(ApiType.ANTHROPIC),
233
+ }
234
+
235
+ from .context_length import context_length_manager
236
+
237
+ models: list[Model] = []
238
+ for entry in cached:
239
+ limits = context_length_manager.get_limits(entry.id)
240
+ is_matched = entry.id in context_length_manager._limits or any(
241
+ entry.id.lower() in k.lower() or k.lower() in entry.id.lower()
242
+ for k in context_length_manager._limits
243
+ )
244
+
245
+ max_tokens = entry.max_output_tokens or provider.max_tokens
246
+ supports_images = entry.supports_images or provider.supports_vision
247
+ supports_thinking = provider.supports_thinking
248
+ context_window = entry.context_length or None
249
+
250
+ if is_matched:
251
+ if context_window is None or context_window == 0:
252
+ context_window = limits.context
253
+ if max_tokens == 0 or max_tokens == provider.max_tokens:
254
+ max_tokens = limits.output
255
+ if not supports_thinking:
256
+ supports_thinking = limits.supports_reasoning
257
+ if not supports_images:
258
+ supports_images = limits.supports_vision
259
+ supports_tools = limits.supports_tools
260
+ supports_audio = limits.supports_audio
261
+ else:
262
+ supports_tools = provider.supports_tools
263
+ supports_audio = False
264
+
265
+ models.append(
266
+ Model(
267
+ id=entry.id,
268
+ provider=provider.slug,
269
+ api=family_to_api[provider.family],
270
+ base_url=provider.base_url or "",
271
+ max_tokens=max_tokens,
272
+ supports_images=supports_images,
273
+ supports_thinking=supports_thinking,
274
+ context_window=context_window,
275
+ supports_tools=supports_tools,
276
+ supports_audio=supports_audio,
277
+ )
278
+ )
279
+ return models
vtx/llm/models.py ADDED
@@ -0,0 +1,78 @@
1
+ """
2
+ Model types and catalog.
3
+
4
+ Model metadata is fetched dynamically via the provider catalog
5
+ and models.dev API. Only the type definitions live here.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+
10
+ DEFAULT_MAX_TOKENS = 16384
11
+
12
+
13
+ class ApiType:
14
+ OPENAI_COMPLETIONS = "openai-completions"
15
+ OPENAI_SDK = "openai-sdk"
16
+ ANTHROPIC = "anthropic"
17
+
18
+ _VALUES: frozenset[str] = frozenset({OPENAI_COMPLETIONS, OPENAI_SDK, ANTHROPIC})
19
+
20
+ def __init__(self, value: str):
21
+ if value not in self._VALUES:
22
+ raise ValueError(f"Invalid ApiType: {value}")
23
+ self.value = value
24
+
25
+ def __eq__(self, other):
26
+ if isinstance(other, ApiType):
27
+ return self.value == other.value
28
+ if isinstance(other, str):
29
+ return self.value == other
30
+ return NotImplemented
31
+
32
+ def __hash__(self):
33
+ return hash(self.value)
34
+
35
+ def __repr__(self):
36
+ return f"ApiType({self.value!r})"
37
+
38
+
39
+ @dataclass
40
+ class Model:
41
+ id: str
42
+ provider: str
43
+ api: ApiType
44
+ base_url: str
45
+ max_tokens: int
46
+ supports_images: bool
47
+ supports_thinking: bool
48
+ context_window: int | None = None
49
+ supports_tools: bool = True
50
+ supports_audio: bool = False
51
+
52
+
53
+ def get_model(model_id: str, provider: str | None = None) -> Model | None:
54
+ from .dynamic_models import find_dynamic_model
55
+ from .provider_catalog import find_model
56
+
57
+ model = find_model(model_id, provider)
58
+ if model is None:
59
+ model = find_dynamic_model(model_id, provider)
60
+ return model
61
+
62
+
63
+ def get_all_models() -> list[Model]:
64
+ from .dynamic_models import get_dynamic_models
65
+ from .provider_catalog import get_all_catalog_models
66
+
67
+ merged: list[Model] = get_all_catalog_models()
68
+ merged.extend(get_dynamic_models())
69
+ return merged
70
+
71
+
72
+ def get_models_by_provider(provider: str) -> list[Model]:
73
+ return [m for m in get_all_models() if m.provider == provider]
74
+
75
+
76
+ def get_max_tokens(model_id: str) -> int:
77
+ model = get_model(model_id)
78
+ return model.max_tokens if model else DEFAULT_MAX_TOKENS
@@ -0,0 +1,59 @@
1
+ from .copilot import (
2
+ COPILOT_HEADERS,
3
+ CopilotCredentials,
4
+ clear_credentials,
5
+ get_base_url_from_token,
6
+ get_copilot_auth_path,
7
+ get_valid_token,
8
+ is_copilot_logged_in,
9
+ load_credentials,
10
+ login,
11
+ )
12
+ from .dynamic import (
13
+ DynamicProviderStatus,
14
+ clear_api_key,
15
+ get_dynamic_api_key,
16
+ get_dynamic_auth_path,
17
+ get_provider_status,
18
+ has_api_key,
19
+ load_api_key,
20
+ save_api_key,
21
+ )
22
+ from .openai import (
23
+ OpenAICredentials,
24
+ clear_openai_credentials,
25
+ get_openai_auth_path,
26
+ get_valid_openai_credentials,
27
+ get_valid_openai_token,
28
+ is_openai_logged_in,
29
+ load_openai_credentials,
30
+ )
31
+ from .openai import login as openai_login
32
+
33
+ __all__ = [
34
+ "COPILOT_HEADERS",
35
+ "CopilotCredentials",
36
+ "DynamicProviderStatus",
37
+ "OpenAICredentials",
38
+ "clear_api_key",
39
+ "clear_credentials",
40
+ "clear_openai_credentials",
41
+ "get_base_url_from_token",
42
+ "get_copilot_auth_path",
43
+ "get_dynamic_api_key",
44
+ "get_dynamic_auth_path",
45
+ "get_openai_auth_path",
46
+ "get_provider_status",
47
+ "get_valid_openai_credentials",
48
+ "get_valid_openai_token",
49
+ "get_valid_token",
50
+ "has_api_key",
51
+ "is_copilot_logged_in",
52
+ "is_openai_logged_in",
53
+ "load_api_key",
54
+ "load_credentials",
55
+ "load_openai_credentials",
56
+ "login",
57
+ "openai_login",
58
+ "save_api_key",
59
+ ]