@agxnte/reidx 2.0.9 → 2.1.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agxnte/reidx",
3
- "version": "2.0.9",
3
+ "version": "2.1.0",
4
4
  "description": "Terminal-native personal intelligence and coding CLI (agent-first runtime)",
5
5
  "bin": {
6
6
  "reid": "bin/reidx.js"
package/pyproject.toml CHANGED
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "reidx"
3
- version = "2.0.9"
4
- description = "Terminal-native personal intelligence and coding CLI (agent-first runtime)"
3
+ version = "2.1.0"
4
+ description = "Terminal-native personal intelligence and coding CLI (agent-first runtime)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
7
7
  license = { text = "MIT" }
@@ -1,3 +1,3 @@
1
1
  """ReidX: terminal-native agent-first CLI runtime."""
2
2
 
3
- __version__ = "2.0.9"
3
+ __version__ = "2.1.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  Speaks the OpenAI Chat Completions API (POST /v1/chat/completions). The same
4
4
  wire format is spoken by llama.cpp's server (`/v1/chat/completions`) and by
5
- LM Studio's local server see `OpenAICompatibleProvider` below for the
5
+ LM Studio's local server - see `OpenAICompatibleProvider` below for the
6
6
  local-endpoint variant that skips the auth header when no key is set.
7
7
 
8
8
  Tool schemas ReidX produces (`ToolBase.schema()`) are already OpenAI-style,
@@ -24,6 +24,7 @@ class OpenAIProvider(BaseProvider):
24
24
  name = "openai"
25
25
  DEFAULT_BASE_URL = "https://api.openai.com"
26
26
  DEFAULT_MODEL = "gpt-4o-mini"
27
+ MODELS_PATH = "/v1/models"
27
28
 
28
29
  def __init__(
29
30
  self,
@@ -126,7 +127,7 @@ class OpenAIProvider(BaseProvider):
126
127
  return self._parse(body, model)
127
128
 
128
129
  def fetch_models(self) -> list[str]:
129
- url = f"{self.base_url}/v1/models"
130
+ url = f"{self.base_url}{self.MODELS_PATH}"
130
131
  body = get_json(url, self._headers())
131
132
  models: list[str] = []
132
133
  for item in body.get("data", []):
@@ -148,6 +149,7 @@ class OpenAICompatibleProvider(OpenAIProvider):
148
149
  name = "openai-compatible"
149
150
  DEFAULT_BASE_URL = "http://localhost:8080"
150
151
  DEFAULT_MODEL = "local"
152
+ MODELS_PATH = "/v1/models"
151
153
 
152
154
  def __init__(
153
155
  self,
@@ -174,4 +176,4 @@ class OpenAICompatibleProvider(OpenAIProvider):
174
176
 
175
177
  def _json_dump(obj) -> str: # type: ignore[no-untyped-def]
176
178
  import json
177
- return json.dumps(obj, ensure_ascii=False)
179
+ return json.dumps(obj, ensure_ascii=False)
@@ -153,6 +153,8 @@ def validate_provider(
153
153
  msg = str(exc)
154
154
  if msg.startswith("HTTP 401") or msg.startswith("HTTP 403"):
155
155
  return False, f"authentication failed ({msg})"
156
+ if msg.startswith("HTTP 404"):
157
+ return False, f"endpoint not found - check base URL ({msg})"
156
158
  if msg.startswith("HTTP "):
157
159
  return False, f"provider error: {msg}"
158
160
  return False, f"{msg} (use --skip-verify to save anyway)"
@@ -1,3 +1,10 @@
1
+ """Provider catalog: canonical metadata for known providers.
2
+
3
+ All OpenAI-compatible providers use `base_url` without a `/v1` suffix -
4
+ the client appends `/v1` when building request URLs. For Anthropic, the
5
+ client uses the base URL as-is with `/v1/messages` and `/v1/models`.
6
+ Ollama uses its native `/api` endpoints.
7
+ """
1
8
  from __future__ import annotations
2
9
 
3
10
  from dataclasses import dataclass, field
@@ -22,276 +29,277 @@ _BUILTIN: list[ProviderDefinition] = [
22
29
  ProviderDefinition(
23
30
  id="openai", name="OpenAI", description="GPT-4o, o-series, and DALL-E models",
24
31
  kind="openai", base_url="https://api.openai.com", default_model="gpt-4o-mini",
25
- aliases=["gpt", "chatgpt"], popular=True, icon="🟢",
32
+ aliases=["gpt", "chatgpt"], popular=True, icon="OAI",
26
33
  ),
27
34
  ProviderDefinition(
28
35
  id="anthropic", name="Anthropic", description="Claude Sonnet, Opus, and Haiku models",
29
36
  kind="anthropic", base_url="https://api.anthropic.com", default_model="claude-sonnet-4-20250514",
30
- aliases=["claude"], auth_method="x-api-key", popular=True, icon="🟠",
37
+ aliases=["claude"], auth_method="x-api-key", popular=True, icon="ANT",
31
38
  ),
32
39
  ProviderDefinition(
33
40
  id="google-ai", name="Google AI", description="Gemini Pro, Flash, and Flash-Lite models",
34
41
  kind="openai-compatible", base_url="https://generativelanguage.googleapis.com/v1beta/openai",
35
42
  default_model="gemini-2.0-flash", aliases=["gemini", "google", "google-gemini"],
36
- popular=True, icon="🔵",
43
+ popular=True, icon="GAI",
37
44
  ),
38
- ProviderDefinition(
45
+ ProviderDefinition(
39
46
  id="openrouter", name="OpenRouter", description="Unified API for 300+ models from all providers",
40
- kind="openai-compatible", base_url="https://openrouter.ai/api/v1",
41
- default_model="auto", aliases=["or"], popular=True, icon="🔀",
47
+ kind="openai-compatible", base_url="https://openrouter.ai/api",
48
+ default_model="auto", aliases=["or"], popular=True, icon="ORT",
42
49
  ),
43
50
  ProviderDefinition(
44
51
  id="zai", name="Z.ai", description="GLM-4 and GLM-4.5 series models",
45
52
  kind="openai-compatible", base_url="https://open.bigmodel.cn/api/paas/v4",
46
- default_model="glm-4", aliases=["z-ai", "zhipu", "glm", "bigmodel"], popular=True, icon="🟣",
53
+ default_model="glm-4", aliases=["z-ai", "zhipu", "glm", "bigmodel"], popular=True, icon="ZAI",
47
54
  ),
48
55
  ProviderDefinition(
49
56
  id="agentrouter", name="AgentRouter", description="Multi-model routing for AI agents",
50
57
  kind="openai-compatible", base_url="https://api.agentrouter.io/v1",
51
- default_model="auto", aliases=["agent-router"], icon="🤖",
58
+ default_model="auto", aliases=["agent-router"], icon="AGT",
52
59
  ),
53
60
  ProviderDefinition(
54
61
  id="together", name="Together AI", description="Open-source Llama, Mixtral, and Qwen models",
55
62
  kind="openai-compatible", base_url="https://api.together.xyz/v1",
56
63
  default_model="meta-llama/Llama-3.3-70B-Instruct-Turbo", aliases=["together-ai"],
57
- popular=True, icon="🤝",
64
+ popular=True, icon="TGT",
58
65
  ),
59
66
  ProviderDefinition(
60
67
  id="fireworks", name="Fireworks AI", description="Fast inference for open-source models",
61
68
  kind="openai-compatible", base_url="https://api.fireworks.ai/inference/v1",
62
69
  default_model="accounts/fireworks/models/llama-v3p1-70b-instruct",
63
- aliases=["fireworks-ai"], popular=True, icon="🎆",
70
+ aliases=["fireworks-ai"], popular=True, icon="FRW",
64
71
  ),
65
72
  ProviderDefinition(
66
73
  id="groq", name="Groq", description="Ultra-fast LPU inference for Llama and Mixtral",
67
74
  kind="openai-compatible", base_url="https://api.groq.com/openai/v1",
68
- default_model="llama-3.3-70b-versatile", aliases=[], popular=True, icon="",
75
+ default_model="llama-3.3-70b-versatile", aliases=[], popular=True, icon="GRQ",
69
76
  ),
70
77
  ProviderDefinition(
71
78
  id="deepinfra", name="DeepInfra", description="Cost-effective inference for open models",
72
79
  kind="openai-compatible", base_url="https://api.deepinfra.com/v1/openai",
73
- default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="🔧",
80
+ default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="DPI",
74
81
  ),
75
82
  ProviderDefinition(
76
83
  id="cerebras", name="Cerebras", description="Fastest AI inference with Wafer-Scale Engines",
77
84
  kind="openai-compatible", base_url="https://api.cerebras.ai/v1",
78
- default_model="llama3.1-70b", aliases=[], icon="🧠",
85
+ default_model="llama3.1-70b", aliases=[], icon="CER",
79
86
  ),
80
87
  ProviderDefinition(
81
88
  id="cohere", name="Cohere", description="Command R+ and Aya models for enterprise",
82
89
  kind="openai-compatible", base_url="https://api.cohere.ai/v1",
83
- default_model="command-r-plus-08-2024", aliases=[], icon="🔗",
90
+ default_model="command-r-plus-08-2024", aliases=[], icon="COH",
84
91
  ),
85
92
  ProviderDefinition(
86
93
  id="xai", name="xAI", description="Grok models from xAI",
87
94
  kind="openai-compatible", base_url="https://api.x.ai/v1",
88
- default_model="grok-2-latest", aliases=["grok", "x-ai"], popular=True, icon="✖️",
95
+ default_model="grok-2-latest", aliases=["grok", "x-ai"], popular=True, icon="XAI",
89
96
  ),
90
97
  ProviderDefinition(
91
98
  id="mistral", name="Mistral", description="Mistral Large, Codestral, and Pixtral models",
92
99
  kind="openai-compatible", base_url="https://api.mistral.ai/v1",
93
- default_model="mistral-large-latest", aliases=["mistral-ai"], popular=True, icon="🌬️",
100
+ default_model="mistral-large-latest", aliases=["mistral-ai"], popular=True, icon="MIS",
94
101
  ),
95
102
  ProviderDefinition(
96
103
  id="perplexity", name="Perplexity", description="Sonar models with built-in web search",
97
104
  kind="openai-compatible", base_url="https://api.perplexity.ai",
98
- default_model="llama-3.1-sonar-large-128k-online", aliases=[], icon="🔮",
105
+ default_model="llama-3.1-sonar-large-128k-online", aliases=[], icon="PPL",
99
106
  ),
100
107
  ProviderDefinition(
101
- id="ollama", name="Ollama", description="Run models locally Llama, Qwen, Phi, and more",
108
+ id="ollama", name="Ollama", description="Run models locally Llama, Qwen, Phi, and more",
102
109
  kind="ollama", base_url="http://localhost:11434",
103
- default_model="llama3.2", aliases=[], auth_method="none", popular=True, icon="🐪",
110
+ default_model="llama3.2", aliases=[], auth_method="none", popular=True, icon="OLL",
104
111
  ),
105
112
  ProviderDefinition(
106
113
  id="lm-studio", name="LM Studio", description="Local model server with OpenAI-compatible API",
107
114
  kind="openai-compatible", base_url="http://localhost:1234/v1",
108
- default_model="local", aliases=["lmstudio"], auth_method="none", icon="🏠",
115
+ default_model="local", aliases=["lmstudio"], auth_method="none", icon="LMS",
109
116
  ),
110
117
  ProviderDefinition(
111
118
  id="vllm", name="vLLM", description="High-throughput local inference server",
112
119
  kind="openai-compatible", base_url="http://localhost:8000/v1",
113
- default_model="auto", aliases=[], auth_method="none", icon="🚀",
120
+ default_model="auto", aliases=[], auth_method="none", icon="VLL",
114
121
  ),
115
122
  ProviderDefinition(
116
123
  id="azure-openai", name="Azure OpenAI", description="OpenAI models via Azure cloud (set base URL to your endpoint)",
117
124
  kind="openai", base_url="https://api.openai.com", default_model="gpt-4o-mini",
118
- aliases=["azure"], icon="☁️",
125
+ aliases=["azure"], icon="AZO",
119
126
  ),
120
127
  ProviderDefinition(
121
128
  id="huggingface", name="HuggingFace", description="Inference API for 100k+ models",
122
129
  kind="openai-compatible", base_url="https://api-inference.huggingface.co/v1",
123
- default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=["hf"], icon="🤗",
130
+ default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=["hf"], icon="HF",
124
131
  ),
125
132
  ProviderDefinition(
126
133
  id="sambanova", name="SambaNova", description="Fast RDU-based inference for open models",
127
134
  kind="openai-compatible", base_url="https://api.sambanova.ai/v1",
128
- default_model="Meta-Llama-3.1-70B-Instruct", aliases=[], icon="🔴",
135
+ default_model="Meta-Llama-3.1-70B-Instruct", aliases=[], icon="SMB",
129
136
  ),
130
137
  ProviderDefinition(
131
138
  id="novita", name="Novita", description="Affordable API for 100+ open models",
132
139
  kind="openai-compatible", base_url="https://api.novita.ai/v3/openai",
133
- default_model="meta-llama/llama-3.1-70b-instruct", aliases=[], icon="💎",
140
+ default_model="meta-llama/llama-3.1-70b-instruct", aliases=[], icon="NVT",
134
141
  ),
135
142
  ProviderDefinition(
136
143
  id="featherless", name="Featherless", description="Serverless inference for open models",
137
144
  kind="openai-compatible", base_url="https://api.featherless.ai/v1/openai",
138
- default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="🪶",
145
+ default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="FTL",
139
146
  ),
140
147
  ProviderDefinition(
141
148
  id="chutes", name="Chutes", description="GPU-accelerated inference for AI workloads",
142
149
  kind="openai-compatible", base_url="https://api.chutes.ai/v1",
143
- default_model="deepseek-ai/DeepSeek-V3", aliases=[], icon="🎯",
150
+ default_model="deepseek-ai/DeepSeek-V3", aliases=[], icon="CHT",
144
151
  ),
145
152
  ProviderDefinition(
146
153
  id="aimlapi", name="AI/ML API", description="Unified API for 200+ AI models",
147
154
  kind="openai-compatible", base_url="https://api.aimlapi.com/v1",
148
- default_model="gpt-4o", aliases=["ai-ml-api"], icon="📊",
155
+ default_model="gpt-4o", aliases=["ai-ml-api"], icon="AML",
149
156
  ),
150
157
  ProviderDefinition(
151
158
  id="hyperbolic", name="Hyperbolic", description="GPU marketplace for AI inference",
152
159
  kind="openai-compatible", base_url="https://api.hyperbolic.xyz/v1",
153
- default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="📈",
160
+ default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="HYP",
154
161
  ),
155
162
  ProviderDefinition(
156
163
  id="deepseek", name="DeepSeek", description="DeepSeek-V3 and DeepSeek-R1 reasoning models",
157
164
  kind="openai-compatible", base_url="https://api.deepseek.com/v1",
158
- default_model="deepseek-chat", aliases=[], popular=True, icon="🐳",
165
+ default_model="deepseek-chat", aliases=[], popular=True, icon="DSK",
159
166
  ),
160
167
  ProviderDefinition(
161
168
  id="moonshot", name="Moonshot", description="Kimi K1.5 and Moonshot models",
162
169
  kind="openai-compatible", base_url="https://api.moonshot.cn/v1",
163
- default_model="moonshot-v1-8k", aliases=["kimi"], icon="🌙",
170
+ default_model="moonshot-v1-8k", aliases=["kimi"], icon="MSH",
164
171
  ),
165
172
  ProviderDefinition(
166
173
  id="voyage-ai", name="Voyage AI", description="Embeddings and reranking models",
167
174
  kind="openai-compatible", base_url="https://api.voyageai.com/v1",
168
- default_model="voyage-large-2", aliases=[], icon="🧭",
175
+ default_model="voyage-large-2", aliases=[], icon="VOY",
169
176
  ),
170
177
  ProviderDefinition(
171
178
  id="nvidia-nim", name="NVIDIA NIM", description="NVIDIA inference microservices for open models",
172
179
  kind="openai-compatible", base_url="https://integrate.api.nvidia.com/v1",
173
- default_model="meta/llama-3.1-70b-instruct", aliases=["nvidia"], icon="💚",
180
+ default_model="meta/llama-3.1-70b-instruct", aliases=["nvidia"], icon="NVD",
174
181
  ),
175
182
  ProviderDefinition(
176
183
  id="ai21", name="AI21 Labs", description="Jamba models with long context windows",
177
184
  kind="openai-compatible", base_url="https://api.ai21.com/v1",
178
- default_model="jamba-1.5-large", aliases=[], icon="🃏",
185
+ default_model="jamba-1.5-large", aliases=[], icon="A21",
179
186
  ),
180
187
  ProviderDefinition(
181
188
  id="databricks", name="Databricks", description="Serving endpoints for fine-tuned models (set base URL to your workspace)",
182
189
  kind="openai-compatible", base_url="https://dbc-xxxxxxxx.cloud.databricks.com/serving-endpoints",
183
190
  default_model="databricks-dbrx-instruct",
184
- aliases=[], icon="🧱",
191
+ aliases=[], icon="DBS",
185
192
  ),
186
193
  ProviderDefinition(
187
194
  id="replicate", name="Replicate", description="Run open-source models in the cloud",
188
195
  kind="openai-compatible", base_url="https://api.replicate.com/v1",
189
- default_model="meta/llama-2-70b-chat", aliases=[], icon="🔁",
196
+ default_model="meta/llama-2-70b-chat", aliases=[], icon="RPL",
190
197
  ),
191
198
  ProviderDefinition(
192
199
  id="siliconflow", name="SiliconFlow", description="Cost-effective inference for 200+ models",
193
200
  kind="openai-compatible", base_url="https://api.siliconflow.cn/v1",
194
- default_model="deepseek-ai/DeepSeek-V3", aliases=[], icon="🌊",
201
+ default_model="deepseek-ai/DeepSeek-V3", aliases=[], icon="SFL",
195
202
  ),
196
203
  ProviderDefinition(
197
204
  id="nebius", name="Nebius", description="Cloud GPU platform for AI inference",
198
205
  kind="openai-compatible", base_url="https://api.studio.nebius.ai/v1",
199
- default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="☁️",
206
+ default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="NEB",
200
207
  ),
201
208
  ProviderDefinition(
202
209
  id="lepton", name="Lepton AI", description="Serverless inference with auto-scaling",
203
210
  kind="openai-compatible", base_url="https://api.lepton.ai/v1",
204
- default_model="meta-llama/Llama-3.2-70B-Instruct", aliases=[], icon="⚛️",
211
+ default_model="meta-llama/Llama-3.2-70B-Instruct", aliases=[], icon="LPT",
205
212
  ),
206
213
  ProviderDefinition(
207
214
  id="lambda", name="Lambda Labs", description="GPU cloud for AI training and inference",
208
215
  kind="openai-compatible", base_url="https://api.lambdalabs.com/v1",
209
- default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=["lambda-labs"], icon="λ",
216
+ default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=["lambda-labs"], icon="LDA",
210
217
  ),
211
218
  ProviderDefinition(
212
219
  id="kluster", name="Kluster AI", description="Distributed GPU network for inference",
213
220
  kind="openai-compatible", base_url="https://api.kluster.ai/v1",
214
- default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="🔗",
221
+ default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="KLU",
215
222
  ),
216
223
  ProviderDefinition(
217
224
  id="upstage", name="Upstage", description="Solar Mini and Pro models for Korean and English",
218
225
  kind="openai-compatible", base_url="https://api.upstage.ai/v1",
219
- default_model="solar-pro", aliases=[], icon="☀️",
226
+ default_model="solar-pro", aliases=[], icon="UPS",
220
227
  ),
221
228
  ProviderDefinition(
222
229
  id="yi", name="Yi (01.AI)", description="Yi-Large and Yi-34B models",
223
230
  kind="openai-compatible", base_url="https://api.01.ai/v1",
224
- default_model="yi-large", aliases=["01-ai", "yi-ai"], icon="🎯",
231
+ default_model="yi-large", aliases=["01-ai", "yi-ai"], icon="YI",
225
232
  ),
226
233
  ProviderDefinition(
227
234
  id="volcengine", name="Volcengine", description="ByteDance's Ark platform for Doubao models",
228
235
  kind="openai-compatible", base_url="https://ark.cn-beijing.volces.com/api/v3",
229
- default_model="doubao-pro-32k", aliases=["ark", "bytedance"], icon="🌋",
236
+ default_model="doubao-pro-32k", aliases=["ark", "bytedance"], icon="VOL",
230
237
  ),
231
238
  ProviderDefinition(
232
239
  id="hunyuan", name="Tencent Hunyuan", description="Tencent's Hunyuan large language models",
233
240
  kind="openai-compatible", base_url="https://api.hunyuan.cloud.tencent.com/v1",
234
- default_model="hunyuan-turbos-latest", aliases=["tencent"], icon="🐧",
241
+ default_model="hunyuan-turbos-latest", aliases=["tencent"], icon="HUN",
235
242
  ),
236
243
  ProviderDefinition(
237
244
  id="cloudflare-ai", name="Cloudflare Workers AI", description="Serverless AI inference at the edge",
238
245
  kind="openai-compatible", base_url="https://api.cloudflare.com/client/v4",
239
246
  default_model="@cf/meta/llama-3.1-70b-instruct",
240
- aliases=["cf-ai"], icon="orange",
247
+ aliases=["cf-ai"], icon="CFL",
241
248
  ),
242
249
  ProviderDefinition(
243
250
  id="monsterapi", name="Monster API", description="Affordable GPU inference for open models",
244
251
  kind="openai-compatible", base_url="https://api.monsterapi.ai/v1",
245
- default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="👾",
252
+ default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="MST",
246
253
  ),
247
254
  ProviderDefinition(
248
255
  id="baseten", name="Baseten", description="Model deployment and serving platform",
249
256
  kind="openai-compatible", base_url="https://inference.baseten.co/v1",
250
- default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="🏗️",
257
+ default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="BST",
251
258
  ),
252
259
  ProviderDefinition(
253
260
  id="anyscale", name="Anyscale", description="Ray-based scalable model serving",
254
261
  kind="openai-compatible", base_url="https://api.endpoints.anyscale.com/v1",
255
- default_model="meta-llama/Llama-3.1-70B-Instruct", aliases=[], icon="📐",
262
+ default_model="meta-llama/Llama-3.1-70B-Instruct", aliases=[], icon="ANY",
256
263
  ),
257
264
  ProviderDefinition(
258
265
  id="jan", name="Jan", description="Local AI model server with web UI",
259
266
  kind="openai-compatible", base_url="http://localhost:1337/v1",
260
- default_model="local", aliases=[], auth_method="none", icon="🖥️",
267
+ default_model="local", aliases=[], auth_method="none", icon="JAN",
261
268
  ),
262
269
  ProviderDefinition(
263
270
  id="llamacpp", name="Llama.cpp Server", description="Local C++ inference server",
264
271
  kind="openai-compatible", base_url="http://localhost:8080",
265
- default_model="local", aliases=["llama-cpp"], auth_method="none", icon="🦙",
272
+ default_model="local", aliases=["llama-cpp"], auth_method="none", icon="LPP",
266
273
  ),
267
274
  ProviderDefinition(
268
275
  id="watsonx", name="IBM Watsonx", description="Enterprise AI platform with Granite models",
269
276
  kind="openai-compatible", base_url="https://us-south.ml.cloud.ibm.com",
270
277
  default_model="ibm/granite-3-8b-instruct",
271
- aliases=["ibm"], icon="🔷",
278
+ aliases=["ibm"], icon="IBM",
272
279
  ),
273
280
  ProviderDefinition(
274
- id="aleph-alpha", name="Aleph Alpha", description="European sovereign AI Luminous models",
281
+ id="aleph-alpha", name="Aleph Alpha", description="European sovereign AI Luminous models",
275
282
  kind="openai-compatible", base_url="https://api.aleph-alpha.eu/v1",
276
- default_model="luminous-supreme-control", aliases=[], icon="α",
283
+ default_model="luminous-supreme-control", aliases=[], icon="ALP",
277
284
  ),
278
285
  ProviderDefinition(
279
286
  id="predibase", name="Predibase", description="Fine-tune and serve open-source LLMs",
280
287
  kind="openai-compatible", base_url="https://serving.app.predibase.com/v1",
281
- default_model="meta-llama/Meta-Llama-3.1-8B-Instruct", aliases=[], icon="🎯",
288
+ default_model="meta-llama/Meta-Llama-3.1-8B-Instruct", aliases=[], icon="PDB",
282
289
  ),
283
290
  ProviderDefinition(
284
291
  id="gravity", name="Gravity API", description="Multi-model API gateway",
285
292
  kind="openai-compatible", base_url="https://api.gravityapi.com/v1",
286
- default_model="auto", aliases=[], icon="🌌",
293
+ default_model="auto", aliases=[], icon="GRV",
287
294
  ),
288
295
  ProviderDefinition(
289
296
  id="infermatic", name="Infermatic", description="Fast inference for popular open models",
290
297
  kind="openai-compatible", base_url="https://api.infermatic.ai/v1",
291
- default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="💡",
298
+ default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="INF",
292
299
  ),
293
300
  ]
294
301
 
302
+
295
303
  _BY_ID: dict[str, ProviderDefinition] = {p.id: p for p in _BUILTIN}
296
304
 
297
305
 
@@ -330,4 +338,4 @@ def search(query: str) -> list[ProviderDefinition]:
330
338
  if score > 0:
331
339
  results.append((score, p))
332
340
  results.sort(key=lambda x: (-x[0], x[1].name))
333
- return [p for _, p in results]
341
+ return [p for _, p in results]
@@ -96,6 +96,7 @@ class ProviderPalette:
96
96
  CONFIRM = "confirm"
97
97
  WIZARD = "wizard"
98
98
  MESSAGE = "message"
99
+ MODELS = "models"
99
100
 
100
101
  def __init__(
101
102
  self,
@@ -163,7 +164,7 @@ class ProviderPalette:
163
164
  def is_list_screen(self) -> bool:
164
165
  return self._active and self.screen in (
165
166
  self.LIST, self.KEYS, self.MANAGE, self.RENAME_SEL,
166
- self.DELETE_SEL, self.CONFIRM, self.WIZARD,
167
+ self.DELETE_SEL, self.CONFIRM, self.WIZARD, self.MODELS,
167
168
  ) and not self.is_input_screen()
168
169
 
169
170
  def activate(self) -> None:
@@ -221,6 +222,10 @@ class ProviderPalette:
221
222
  self.screen = self._message_next
222
223
  self.selected_index = 0
223
224
  self._scroll_offset = 0
225
+ elif self.screen == self.MODELS:
226
+ self.screen = self.MANAGE
227
+ self.selected_index = 0
228
+ self._scroll_offset = 0
224
229
  elif self.screen == self.WIZARD:
225
230
  if self.wizard_step_idx > 0:
226
231
  self.wizard_step_idx -= 1
@@ -311,6 +316,8 @@ class ProviderPalette:
311
316
  if step.field == "auth_method":
312
317
  return AUTH_OPTIONS
313
318
  return []
319
+ if self.screen == self.MODELS:
320
+ return self._models_items()
314
321
  return []
315
322
 
316
323
  def _list_items(self) -> list[PaletteItem]:
@@ -327,14 +334,14 @@ class ProviderPalette:
327
334
  if d.name.lower() in stored:
328
335
  sp = stored[d.name.lower()]
329
336
  active_key = sp.active_key()
330
- icon = "" if active_key else ""
337
+ icon = "?" if active_key else " "
331
338
  items.append(PaletteItem(
332
339
  label=d.name, description=d.description, icon=icon,
333
340
  kind="stored", data=sp,
334
341
  ))
335
342
  else:
336
343
  items.append(PaletteItem(
337
- label=d.name, description=d.description, icon="",
344
+ label=d.name, description=d.description, icon="?",
338
345
  kind="catalog", data=d,
339
346
  ))
340
347
 
@@ -342,7 +349,7 @@ class ProviderPalette:
342
349
  if sp.name.lower() not in {d.name.lower() for d in defs}:
343
350
  if not q or q in sp.name.lower() or q in sp.kind.lower() or q in sp.base_url.lower():
344
351
  active_key = sp.active_key()
345
- icon = "" if active_key else ""
352
+ icon = "?" if active_key else " "
346
353
  items.append(PaletteItem(
347
354
  label=sp.name, description=f"{sp.kind} {sp.base_url}", icon=icon,
348
355
  kind="stored", data=sp,
@@ -359,7 +366,7 @@ class ProviderPalette:
359
366
  if self.current_provider:
360
367
  for k in self.current_provider.keys:
361
368
  is_active = k.id == self.current_provider.active_key_id
362
- icon = "" if is_active else ""
369
+ icon = "?" if is_active else " "
363
370
  desc = "active" if is_active else ""
364
371
  items.append(PaletteItem(label=k.label, description=desc, icon=icon, kind="action", data=k))
365
372
  items.append(PaletteItem(label="Add API Key", icon="+", kind="action"))
@@ -369,13 +376,14 @@ class ProviderPalette:
369
376
  items: list[PaletteItem] = []
370
377
  sp = self.current_provider
371
378
  if sp and len(sp.keys) > 1:
372
- items.append(PaletteItem("Switch Key", "Change the active API key", icon=""))
379
+ items.append(PaletteItem("Switch Key", "Change the active API key", icon=""))
373
380
  items.append(PaletteItem("Add Key", "Add another API key", icon="+"))
374
381
  if sp and sp.keys:
375
- items.append(PaletteItem("Rename Key", "Rename a key label", icon=""))
376
- items.append(PaletteItem("Delete Key", "Remove a key", icon=""))
377
- items.append(PaletteItem("Edit Provider", "Change base URL, model, etc.", icon=""))
378
- items.append(PaletteItem("Remove Provider", "Delete provider and all keys", icon="", kind="action"))
382
+ items.append(PaletteItem("Rename Key", "Rename a key label", icon="?"))
383
+ items.append(PaletteItem("Delete Key", "Remove a key", icon="?"))
384
+ items.append(PaletteItem("View Models", "Browse and set default model", icon="M"))
385
+ items.append(PaletteItem("Edit Provider", "Change base URL, model, etc.", icon="?"))
386
+ items.append(PaletteItem("Remove Provider", "Delete provider and all keys", icon="?", kind="action"))
379
387
  return items
380
388
 
381
389
  def _key_select_items(self, _mode: str) -> list[PaletteItem]:
@@ -383,10 +391,43 @@ class ProviderPalette:
383
391
  if self.current_provider:
384
392
  for k in self.current_provider.keys:
385
393
  is_active = k.id == self.current_provider.active_key_id
386
- icon = "" if is_active else ""
394
+ icon = "?" if is_active else " "
387
395
  items.append(PaletteItem(label=k.label, icon=icon, kind="action", data=k))
388
396
  return items
389
397
 
398
+ def _models_items(self) -> list[PaletteItem]:
399
+ items: list[PaletteItem] = []
400
+ if not self.current_provider:
401
+ return items
402
+
403
+ sp = self.current_provider
404
+ try:
405
+ provider = build_provider(ProviderRecord(
406
+ name=sp.name, kind=sp.kind, base_url=sp.base_url,
407
+ api_key=sp.decrypted_api_key(), default_model=sp.default_model,
408
+ auth_method=sp.auth_method,
409
+ ))
410
+ models = provider.fetch_models()
411
+ except Exception as exc:
412
+ log.warning("failed to fetch models for %s: %s", sp.name, exc)
413
+ items.append(PaletteItem("Error loading models", str(exc), icon="!", kind="action"))
414
+ return items
415
+
416
+ if not models:
417
+ items.append(PaletteItem("No models available", "Provider returned empty model list", icon="!", kind="action"))
418
+ return items
419
+
420
+ current_model = sp.default_model or ""
421
+ for model in models:
422
+ is_current = model == current_model
423
+ icon = "?" if is_current else " "
424
+ desc = "current" if is_current else ""
425
+ items.append(PaletteItem(
426
+ label=model, description=desc, icon=icon, kind="model", data=model
427
+ ))
428
+
429
+ return items
430
+
390
431
  def _handle_list_enter(self, item: PaletteItem) -> None:
391
432
  if self.screen == self.LIST:
392
433
  self._on_list_select(item)
@@ -402,6 +443,8 @@ class ProviderPalette:
402
443
  self._on_confirm_select(item)
403
444
  elif self.screen == self.WIZARD:
404
445
  self._on_wizard_select(item)
446
+ elif self.screen == self.MODELS:
447
+ self._on_models_select(item)
405
448
 
406
449
  def _on_list_select(self, item: PaletteItem) -> None:
407
450
  if item.kind == "action":
@@ -472,6 +515,11 @@ class ProviderPalette:
472
515
  self.selected_index = 0
473
516
  self._scroll_offset = 0
474
517
  self._invalidate()
518
+ elif label == "View Models":
519
+ self.screen = self.MODELS
520
+ self.selected_index = 0
521
+ self._scroll_offset = 0
522
+ self._invalidate()
475
523
  elif label == "Edit Provider":
476
524
  self._start_wizard(edit=True)
477
525
  elif label == "Remove Provider":
@@ -503,6 +551,46 @@ class ProviderPalette:
503
551
  self._scroll_offset = 0
504
552
  self._invalidate()
505
553
 
554
+ def _models_items(self) -> list[PaletteItem]:
555
+ items: list[PaletteItem] = []
556
+ if not self.current_provider:
557
+ return items
558
+
559
+ sp = self.current_provider
560
+ try:
561
+ provider = build_provider(ProviderRecord(
562
+ name=sp.name, kind=sp.kind, base_url=sp.base_url,
563
+ api_key=sp.decrypted_api_key(), default_model=sp.default_model,
564
+ auth_method=sp.auth_method,
565
+ ))
566
+ models = provider.fetch_models()
567
+ except Exception as exc:
568
+ log.warning("failed to fetch models for %s: %s", sp.name, exc)
569
+ items.append(PaletteItem("Error loading models", str(exc), icon="!", kind="action"))
570
+ return items
571
+
572
+ if not models:
573
+ items.append(PaletteItem("No models available", "Provider returned empty model list", icon="!", kind="action"))
574
+ return items
575
+
576
+ current_model = sp.default_model or ""
577
+ for model in models:
578
+ is_current = model == current_model
579
+ icon = "?" if is_current else " "
580
+ desc = "current" if is_current else ""
581
+ items.append(PaletteItem(
582
+ label=model, description=desc, icon=icon, kind="model", data=model
583
+ ))
584
+
585
+ return items
586
+
587
+ def _on_models_select(self, item: PaletteItem) -> None:
588
+ if item.kind == "model" and item.data:
589
+ self.current_provider.default_model = item.data
590
+ self.db.save_provider(self.current_provider)
591
+ self._register_current()
592
+ self._show_message(f"Set default model to '{item.data}'", self.MANAGE)
593
+
506
594
  def _on_confirm_select(self, item: PaletteItem) -> None:
507
595
  if item.label == "Confirm":
508
596
  if self._pending_action:
@@ -524,6 +612,13 @@ class ProviderPalette:
524
612
  self.wizard_data[step.field] = item.label
525
613
  self._wizard_advance()
526
614
 
615
+ def _on_models_select(self, item: PaletteItem) -> None:
616
+ if item.kind == "model" and item.data:
617
+ self.current_provider.default_model = item.data
618
+ self.db.save_provider(self.current_provider)
619
+ self._register_current()
620
+ self._show_message(f"Set default model to '{item.data}'", self.MANAGE)
621
+
527
622
  def _handle_input_enter(self) -> None:
528
623
  text = self.input_buf.text
529
624
  if self.screen == self.KEY_LABEL:
@@ -820,6 +915,9 @@ class ProviderPalette:
820
915
  return [(f"bold {ACCENT}", " + Custom Provider"), ("", " "), (DIM, f"Step {self.wizard_step_idx + 1}/{total}: {step.prompt}")]
821
916
  if self.screen == self.MESSAGE:
822
917
  return [(f"bold {OK}", " ✓ Done")]
918
+ if self.screen == self.MODELS:
919
+ name = self.current_provider.name if self.current_provider else ""
920
+ return [(f"bold {ACCENT}", f" {name}"), ("", " "), (DIM, "Models")]
823
921
  return [(f"bold {ACCENT}", " Provider")]
824
922
 
825
923
  def content_fragments(self) -> list[tuple[str, str]]:
@@ -829,7 +927,7 @@ class ProviderPalette:
829
927
  return [(f"{WARN}", f" {self._confirm_text}")]
830
928
  if self.screen == self.WIZARD and self.is_input_screen():
831
929
  step = WIZARD_STEPS[self.wizard_step_idx]
832
- hint = " (optional press Enter to skip)" if step.optional else ""
930
+ hint = " (optional - press Enter to skip)" if step.optional else ""
833
931
  return [(DIM, f" {step.prompt}{hint}")]
834
932
  if self.is_input_screen():
835
933
  return [(DIM, f" {self.input_prompt_text}")]
@@ -850,7 +948,7 @@ class ProviderPalette:
850
948
  desc = item.description
851
949
  remaining = max(1, inner - len(label_part) - 1)
852
950
  if len(desc) > remaining:
853
- desc = desc[: remaining - 1] + ""
951
+ desc = desc[: remaining - 1] + "."
854
952
  desc_part = f" {desc}" if desc else ""
855
953
  pad = max(0, inner - len(label_part) - len(desc_part))
856
954
  if is_sel:
@@ -878,12 +976,14 @@ class ProviderPalette:
878
976
  if self.is_input_screen():
879
977
  return [(DIM, " Enter to continue Esc to go back")]
880
978
  if self.screen == self.CONFIRM:
881
- return [(DIM, " ↑↓ Select Enter Confirm Esc Cancel")]
979
+ return [(DIM, "  Select Enter Confirm Esc Cancel")]
882
980
  if self.screen == self.MESSAGE:
883
981
  return [(DIM, " Enter to continue")]
884
982
  if self.screen == self.LIST:
885
- return [(DIM, " ↑↓ Navigate Enter Select Esc Close Type to search")]
886
- return [(DIM, " ↑↓ Navigate Enter Select Esc Back")]
983
+ return [(DIM, "  Navigate Enter Select Esc Close Type to search")]
984
+ if self.screen == self.MODELS:
985
+ return [(DIM, "  Navigate Enter Select Esc Back")]
986
+ return [(DIM, "  Navigate Enter Select Esc Back")]
887
987
 
888
988
  def input_label(self) -> str:
889
989
  if self.screen == self.KEY_LABEL:
@@ -899,8 +999,8 @@ class ProviderPalette:
899
999
 
900
1000
  def search_label(self) -> str:
901
1001
  if self.screen == self.LIST:
902
- return " "
903
- return " "
1002
+ return " ? "
1003
+ return " > "
904
1004
 
905
1005
  def content_height(self) -> int:
906
1006
  if self.screen == self.MESSAGE:
@@ -913,4 +1013,4 @@ class ProviderPalette:
913
1013
  return max(1, min(len(items), self.max_content_lines()))
914
1014
 
915
1015
  def total_height(self) -> int:
916
- return self.content_height() + 6
1016
+ return self.content_height() + 6