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
vtx/llm/provider.yaml ADDED
@@ -0,0 +1,280 @@
1
+ # LLM Provider Catalog
2
+ # Add new providers here. Each entry defines how to connect to a provider.
3
+ #
4
+ # Fields:
5
+ # slug: Unique identifier
6
+ # display_name: Human-readable name
7
+ # description: One-line description
8
+ # family: API family - "openai_compat" or "anthropic"
9
+ # base_url: API endpoint URL
10
+ # api_key_env: Environment variable for API key (null for keyless)
11
+ # known_models: Fallback list (used when fetch fails or fetch_models=false)
12
+ # supports_tools: Tool calling support (default: true)
13
+ # supports_vision: Image input support (default: false)
14
+ # api_key_optional: Works without API key (default: false)
15
+ # is_local: Local provider (default: false)
16
+ # max_tokens: Default max output tokens (default: 8192)
17
+ # supports_thinking: Reasoning/thinking support (default: false)
18
+ # headers: Extra request headers, e.g. {"X-Foo": "bar"} (default: {})
19
+ # openmodelendpoint: Provider's /models catalog is public (no key needed for
20
+ # model discovery). Inference still requires a real key
21
+ # unless api_key_optional is also set. (default: false)
22
+ #
23
+ # Auto-fetch fields:
24
+ # fetch_models: Fetch model list from /models endpoint (default: false)
25
+ # models_endpoint: Path to append to base_url (default: "/models")
26
+ # model_parser:
27
+ # array_path: JSON key containing model array (default: "data")
28
+ # id_field: Field for model ID (default: "id")
29
+ # name_field: Field for model name (default: "name")
30
+ # context_field: Field for context length (default: "context_length")
31
+ # output_field: Field for max output tokens (default: "max_completion_tokens")
32
+ # cooldown_minutes: Cache TTL in minutes (default: 60)
33
+
34
+ providers:
35
+ - slug: openai
36
+ display_name: OpenAI
37
+ description: "Official OpenAI API (gpt-4o, gpt-4o-mini, ...)."
38
+ family: openai_compat
39
+ base_url: https://api.openai.com/v1
40
+ api_key_env: OPENAI_API_KEY
41
+ known_models:
42
+ - gpt-4o
43
+ - gpt-4o-mini
44
+ - gpt-4-turbo
45
+ supports_vision: true
46
+ max_tokens: 16384
47
+ fetch_models: true
48
+ models_endpoint: /models
49
+ model_parser:
50
+ cooldown_minutes: 60
51
+
52
+ - slug: anthropic
53
+ display_name: Anthropic
54
+ description: Direct Anthropic API.
55
+ family: anthropic
56
+ base_url: https://api.anthropic.com
57
+ api_key_env: ANTHROPIC_API_KEY
58
+ known_models:
59
+ - claude-sonnet-4-20250514
60
+ - claude-3-5-sonnet-20241022
61
+ - claude-3-5-haiku-20241022
62
+ supports_vision: true
63
+ supports_thinking: true
64
+ max_tokens: 8192
65
+
66
+ - slug: deepseek
67
+ display_name: DeepSeek
68
+ description: DeepSeek Chat + Reasoner.
69
+ family: openai_compat
70
+ base_url: https://api.deepseek.com/v1
71
+ api_key_env: DEEPSEEK_API_KEY
72
+ known_models:
73
+ - deepseek-chat
74
+ - deepseek-reasoner
75
+ supports_thinking: true
76
+ fetch_models: true
77
+ models_endpoint: /models
78
+ model_parser:
79
+ cooldown_minutes: 60
80
+
81
+ - slug: zhipu
82
+ display_name: ZhiPu
83
+ description: ZhiPu GLM models.
84
+ family: openai_compat
85
+ base_url: https://api.z.ai/api/coding/paas/v4
86
+ api_key_env: ZAI_API_KEY
87
+ known_models:
88
+ - glm-5.1
89
+ - glm-5.2
90
+ supports_thinking: true
91
+ fetch_models: true
92
+ models_endpoint: /models
93
+ model_parser:
94
+ cooldown_minutes: 60
95
+
96
+ - slug: openrouter
97
+ display_name: OpenRouter
98
+ description: "200+ models from every provider, one key."
99
+ family: openai_compat
100
+ base_url: https://openrouter.ai/api/v1
101
+ api_key_env: OPENROUTER_API_KEY
102
+ known_models:
103
+ - anthropic/claude-3.5-sonnet
104
+ - openai/gpt-4o
105
+ - google/gemini-2.0-flash-exp:free
106
+ supports_vision: true
107
+ fetch_models: true
108
+ models_endpoint: /models
109
+ model_parser:
110
+ cooldown_minutes: 60
111
+
112
+ - slug: groq
113
+ display_name: Groq
114
+ description: Ultra-fast LPU inference.
115
+ family: openai_compat
116
+ base_url: https://api.groq.com/openai/v1
117
+ api_key_env: GROQ_API_KEY
118
+ known_models:
119
+ - llama-3.3-70b-versatile
120
+ - mixtral-8x7b-32768
121
+ fetch_models: true
122
+ models_endpoint: /models
123
+ model_parser:
124
+ cooldown_minutes: 60
125
+
126
+ - slug: together
127
+ display_name: Together AI
128
+ description: Open-source models hosted by Together.
129
+ family: openai_compat
130
+ base_url: https://api.together.xyz/v1
131
+ api_key_env: TOGETHER_API_KEY
132
+ known_models:
133
+ - meta-llama/Llama-3.3-70B-Instruct-Turbo
134
+ fetch_models: true
135
+ models_endpoint: /models
136
+ model_parser:
137
+ cooldown_minutes: 60
138
+
139
+ - slug: fireworks
140
+ display_name: Fireworks AI
141
+ description: Fast open-source inference.
142
+ family: openai_compat
143
+ base_url: https://api.fireworks.ai/inference/v1
144
+ api_key_env: FIREWORKS_API_KEY
145
+ known_models: []
146
+ fetch_models: true
147
+ models_endpoint: /models
148
+ model_parser:
149
+ cooldown_minutes: 60
150
+
151
+ - slug: mistral
152
+ display_name: Mistral AI
153
+ description: Mistral's OpenAI-compatible endpoint.
154
+ family: openai_compat
155
+ base_url: https://api.mistral.ai/v1
156
+ api_key_env: MISTRAL_API_KEY
157
+ known_models: []
158
+ fetch_models: true
159
+ models_endpoint: /models
160
+ model_parser:
161
+ cooldown_minutes: 60
162
+
163
+ - slug: nvidia
164
+ display_name: NVIDIA NIM
165
+ description: NVIDIA inference endpoints.
166
+ family: openai_compat
167
+ base_url: https://integrate.api.nvidia.com/v1
168
+ api_key_env: NVIDIA_API_KEY
169
+ known_models: []
170
+ fetch_models: true
171
+ models_endpoint: /models
172
+ model_parser:
173
+ cooldown_minutes: 60
174
+
175
+ - slug: deepinfra
176
+ display_name: DeepInfra
177
+ description: Serverless GPU inference.
178
+ family: openai_compat
179
+ base_url: https://api.deepinfra.com/v1/openai
180
+ api_key_env: DEEPINFRA_API_KEY
181
+ known_models: []
182
+ fetch_models: true
183
+ models_endpoint: /models
184
+ model_parser:
185
+ cooldown_minutes: 60
186
+
187
+ - slug: huggingface
188
+ display_name: Hugging Face
189
+ description: Hugging Face inference API.
190
+ family: openai_compat
191
+ base_url: https://router.huggingface.co/v1
192
+ api_key_env: HF_TOKEN
193
+ known_models: []
194
+ fetch_models: true
195
+ models_endpoint: /models
196
+ model_parser:
197
+ cooldown_minutes: 60
198
+
199
+ - slug: ollama
200
+ display_name: "Ollama (local)"
201
+ description: Local models served by Ollama.
202
+ family: openai_compat
203
+ base_url: http://localhost:11434/v1
204
+ api_key_env: null
205
+ known_models: []
206
+ api_key_optional: true
207
+ is_local: true
208
+ openmodelendpoint: true
209
+ fetch_models: true
210
+ models_endpoint: /models
211
+ model_parser:
212
+ cooldown_minutes: 5
213
+
214
+ - slug: aerolink
215
+ display_name: Aerolink
216
+ description: Aerolink Anthropic-compatible gateway with auto-fetched model catalog.
217
+ family: anthropic
218
+ base_url: https://capi.aerolink.lat
219
+ api_key_env: ANTHROPIC_API_KEY
220
+ known_models: []
221
+ supports_vision: true
222
+ supports_thinking: true
223
+ max_tokens: 8192
224
+ fetch_models: true
225
+ models_endpoint: /v1/models
226
+ model_parser:
227
+ cooldown_minutes: 60
228
+
229
+ - slug: airouter
230
+ display_name: Airouter
231
+ description: Airouter gateway (free tier + paid models).
232
+ family: openai_compat
233
+ base_url: https://api.airouter.in/v1
234
+ api_key_env: AIROUTER_API_KEY
235
+ known_models: []
236
+ fetch_models: true
237
+ models_endpoint: /models
238
+ model_parser:
239
+ cooldown_minutes: 60
240
+
241
+ - slug: opencode
242
+ display_name: OpenCode Zen
243
+ description: Curated models via OpenCode Zen.
244
+ family: openai_compat
245
+ base_url: https://opencode.ai/zen/v1
246
+ api_key_env: OPENCODE_API_KEY
247
+ known_models: []
248
+ openmodelendpoint: true
249
+ fetch_models: true
250
+ models_endpoint: /models
251
+ model_parser:
252
+ cooldown_minutes: 60
253
+
254
+ - slug: kilo
255
+ display_name: Kilo Gateway
256
+ description: Kilo AI gateway (free tier + paid models).
257
+ family: openai_compat
258
+ base_url: https://api.kilo.ai/api/gateway
259
+ api_key_env: KILO_API_KEY
260
+ known_models: []
261
+ headers:
262
+ X-KILOCODE-EDITORNAME: vtx
263
+ User-Agent: vtx
264
+ openmodelendpoint: true
265
+ fetch_models: true
266
+ models_endpoint: /models
267
+ model_parser:
268
+ cooldown_minutes: 60
269
+
270
+ - slug: tokenrouter
271
+ display_name: TokenRouter
272
+ description: TokenRouter gateway.
273
+ family: openai_compat
274
+ base_url: https://api.tokenrouter.com/v1
275
+ api_key_env: TOKENROUTER_API_KEY
276
+ known_models: []
277
+ fetch_models: true
278
+ models_endpoint: /models
279
+ model_parser:
280
+ cooldown_minutes: 60
@@ -0,0 +1,230 @@
1
+ """Load LLM provider catalog from provider.yaml."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+
9
+ import yaml
10
+
11
+ from .models import ApiType, Model
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ModelParserConfig:
16
+ array_path: str = "data"
17
+ id_field: str = "id"
18
+ name_field: str = "name"
19
+ context_field: str = "context_length"
20
+ output_field: str = "max_completion_tokens"
21
+ cooldown_minutes: int = 60
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class ProviderInfo:
26
+ slug: str
27
+ display_name: str
28
+ description: str
29
+ family: str
30
+ base_url: str | None = None
31
+ api_key_env: str | None = None
32
+ known_models: tuple[str, ...] = ()
33
+ supports_tools: bool = True
34
+ supports_vision: bool = False
35
+ api_key_optional: bool = False
36
+ is_local: bool = False
37
+ max_tokens: int = 8192
38
+ supports_thinking: bool = False
39
+ fetch_models: bool = False
40
+ models_endpoint: str = "/models"
41
+ headers: dict[str, str] = field(default_factory=dict)
42
+ openmodelendpoint: bool = False
43
+ model_parser: ModelParserConfig = field(default_factory=ModelParserConfig)
44
+
45
+
46
+ _YAML_PATH = Path(__file__).parent / "provider.yaml"
47
+ _cache: dict[str, ProviderInfo] | None = None
48
+ _order_cache: list[str] | None = None
49
+
50
+
51
+ def _load() -> dict[str, ProviderInfo]:
52
+ global _cache
53
+ if _cache is not None:
54
+ return _cache
55
+
56
+ with open(_YAML_PATH, encoding="utf-8") as f:
57
+ data = yaml.safe_load(f)
58
+
59
+ providers: dict[str, ProviderInfo] = {}
60
+ for entry in data.get("providers", []):
61
+ parser_data = entry.get("model_parser") or {}
62
+ parser = ModelParserConfig(
63
+ array_path=parser_data.get("array_path", "data"),
64
+ id_field=parser_data.get("id_field", "id"),
65
+ name_field=parser_data.get("name_field", "name"),
66
+ context_field=parser_data.get("context_field", "context_length"),
67
+ output_field=parser_data.get("output_field", "max_completion_tokens"),
68
+ cooldown_minutes=parser_data.get("cooldown_minutes", 60),
69
+ )
70
+ p = ProviderInfo(
71
+ slug=entry["slug"],
72
+ display_name=entry["display_name"],
73
+ description=entry["description"],
74
+ family=entry["family"],
75
+ base_url=entry.get("base_url"),
76
+ api_key_env=entry.get("api_key_env"),
77
+ known_models=tuple(entry.get("known_models", [])),
78
+ supports_tools=entry.get("supports_tools", True),
79
+ supports_vision=entry.get("supports_vision", False),
80
+ api_key_optional=entry.get("api_key_optional", False),
81
+ is_local=entry.get("is_local", False),
82
+ max_tokens=entry.get("max_tokens", 8192),
83
+ supports_thinking=entry.get("supports_thinking", False),
84
+ fetch_models=entry.get("fetch_models", False),
85
+ models_endpoint=entry.get("models_endpoint", "/models"),
86
+ headers=entry.get("headers") or {},
87
+ openmodelendpoint=entry.get("openmodelendpoint", False),
88
+ model_parser=parser,
89
+ )
90
+ providers[p.slug] = p
91
+
92
+ _cache = providers
93
+ return _cache
94
+
95
+
96
+ def _get_order() -> list[str]:
97
+ global _order_cache
98
+ if _order_cache is not None:
99
+ return _order_cache
100
+ with open(_YAML_PATH, encoding="utf-8") as f:
101
+ data = yaml.safe_load(f)
102
+ _order_cache = [e["slug"] for e in data.get("providers", [])]
103
+ return _order_cache
104
+
105
+
106
+ DEFAULT_PROVIDER_SLUG = "openai"
107
+
108
+
109
+ def get(slug: str) -> ProviderInfo | None:
110
+ return _load().get(slug)
111
+
112
+
113
+ def list_providers() -> list[ProviderInfo]:
114
+ order = _get_order()
115
+ providers = _load()
116
+ return [providers[s] for s in order if s in providers]
117
+
118
+
119
+ def detect_provider_from_env() -> ProviderInfo:
120
+ providers = _load()
121
+ order = _get_order()
122
+
123
+ explicit = os.getenv("VTX_PROVIDER", "").strip().lower()
124
+ if explicit and explicit in providers:
125
+ return providers[explicit]
126
+
127
+ for slug in order:
128
+ p = providers[slug]
129
+ if p.is_local:
130
+ continue
131
+ if p.api_key_env and os.getenv(p.api_key_env):
132
+ return p
133
+
134
+ for slug in order:
135
+ p = providers[slug]
136
+ if p.is_local and p.api_key_optional:
137
+ return p
138
+
139
+ return providers[DEFAULT_PROVIDER_SLUG]
140
+
141
+
142
+ def is_provider_configured(p: ProviderInfo) -> bool:
143
+ if p.api_key_optional:
144
+ return True
145
+ if p.api_key_env is None:
146
+ return True
147
+ return bool(os.getenv(p.api_key_env))
148
+
149
+
150
+ def _provider_info_to_model(p: ProviderInfo, model_id: str) -> Model:
151
+ family_to_api = {
152
+ "openai_compat": ApiType(ApiType.OPENAI_SDK),
153
+ "anthropic": ApiType(ApiType.ANTHROPIC),
154
+ }
155
+ from .context_length import context_length_manager
156
+
157
+ limits = context_length_manager.get_limits(model_id)
158
+ is_matched = model_id in context_length_manager._limits or any(
159
+ model_id.lower() in k.lower() or k.lower() in model_id.lower()
160
+ for k in context_length_manager._limits
161
+ )
162
+
163
+ if is_matched:
164
+ max_tokens = limits.output
165
+ supports_images = limits.supports_vision
166
+ supports_thinking = limits.supports_reasoning
167
+ context_window = limits.context
168
+ supports_tools = limits.supports_tools
169
+ supports_audio = limits.supports_audio
170
+ else:
171
+ max_tokens = p.max_tokens
172
+ supports_images = p.supports_vision
173
+ supports_thinking = p.supports_thinking
174
+ context_window = limits.context
175
+ supports_tools = p.supports_tools
176
+ supports_audio = False
177
+
178
+ return Model(
179
+ id=model_id,
180
+ provider=p.slug,
181
+ api=family_to_api[p.family],
182
+ base_url=p.base_url or "",
183
+ max_tokens=max_tokens,
184
+ supports_images=supports_images,
185
+ supports_thinking=supports_thinking,
186
+ context_window=context_window,
187
+ supports_tools=supports_tools,
188
+ supports_audio=supports_audio,
189
+ )
190
+
191
+
192
+ def get_all_catalog_models() -> list[Model]:
193
+ from .model_fetcher import get_fetched_models
194
+
195
+ models: list[Model] = []
196
+ for p in _load().values():
197
+ fetched = get_fetched_models(p)
198
+ if fetched:
199
+ models.extend(fetched)
200
+ else:
201
+ for model_id in p.known_models:
202
+ models.append(_provider_info_to_model(p, model_id))
203
+ return models
204
+
205
+
206
+ def find_model(model_id: str, provider: str | None = None) -> Model | None:
207
+ from .model_fetcher import get_fetched_models
208
+
209
+ providers = _load()
210
+ if provider:
211
+ p = providers.get(provider)
212
+ if p:
213
+ fetched = get_fetched_models(p)
214
+ if fetched:
215
+ for m in fetched:
216
+ if m.id == model_id:
217
+ return m
218
+ if model_id in p.known_models:
219
+ return _provider_info_to_model(p, model_id)
220
+
221
+ for p in providers.values():
222
+ fetched = get_fetched_models(p)
223
+ if fetched:
224
+ for m in fetched:
225
+ if m.id == model_id:
226
+ return m
227
+ if model_id in p.known_models:
228
+ return _provider_info_to_model(p, model_id)
229
+
230
+ return None
@@ -0,0 +1,45 @@
1
+ from ..base import BaseProvider
2
+ from ..models import ApiType
3
+
4
+ PROVIDER_API_BY_NAME: dict[str, ApiType] = {
5
+ "openai": ApiType(ApiType.OPENAI_SDK),
6
+ "anthropic": ApiType(ApiType.ANTHROPIC),
7
+ "zhipu": ApiType(ApiType.OPENAI_SDK),
8
+ "deepseek": ApiType(ApiType.OPENAI_SDK),
9
+ "airouter": ApiType(ApiType.OPENAI_SDK),
10
+ "opencode": ApiType(ApiType.OPENAI_SDK),
11
+ "kilo": ApiType(ApiType.OPENAI_SDK),
12
+ "tokenrouter": ApiType(ApiType.OPENAI_SDK),
13
+ "openrouter": ApiType(ApiType.OPENAI_SDK),
14
+ "ollama": ApiType(ApiType.OPENAI_SDK),
15
+ "aerolink": ApiType(ApiType.ANTHROPIC),
16
+ }
17
+
18
+
19
+ def resolve_provider_api_type(provider: str | None) -> ApiType:
20
+ if provider is None:
21
+ return ApiType(ApiType.OPENAI_SDK)
22
+ api_type = PROVIDER_API_BY_NAME.get(provider)
23
+ if api_type is None:
24
+ return ApiType(ApiType.OPENAI_SDK)
25
+ return api_type
26
+
27
+
28
+ def get_provider_class(api_type: ApiType) -> type[BaseProvider]:
29
+ match api_type.value:
30
+ case ApiType.OPENAI_SDK:
31
+ from .openai_sdk import OpenAISDKProvider
32
+
33
+ return OpenAISDKProvider
34
+ case ApiType.ANTHROPIC:
35
+ from .anthropic_sdk import AnthropicSDKProvider
36
+
37
+ return AnthropicSDKProvider
38
+ case ApiType.OPENAI_COMPLETIONS:
39
+ from .openai_sdk import OpenAISDKProvider
40
+
41
+ return OpenAISDKProvider
42
+ raise ValueError(f"Unsupported API type: {api_type.value}")
43
+
44
+
45
+ __all__ = ["PROVIDER_API_BY_NAME", "get_provider_class", "resolve_provider_api_type"]