adityaverma977 commited on
Commit
8e038ca
·
1 Parent(s): 117a348

Keep both Groq + HF models, remove rate-limited Groq models, unify model selection UI without backend labels

Browse files
backend/app/groq_client.py CHANGED
@@ -2,20 +2,47 @@ import json
2
  import os
3
  import random
4
  import math
 
5
  from groq import AsyncGroq
6
  from dotenv import load_dotenv
7
 
8
  load_dotenv()
9
 
10
  _GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
11
- _client = AsyncGroq(api_key=_GROQ_API_KEY) if _GROQ_API_KEY else None
 
 
12
 
13
- DEFAULT_DECISION_MODEL = "llama-3.1-8b-instant"
14
  MAX_AGENT_SPEED = 80
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  def is_ready():
18
- return _client is not None
 
 
 
 
 
 
 
 
 
 
 
19
 
20
 
21
  def _build_fire_state_summary(agent, fire, all_agents) -> str:
@@ -47,10 +74,10 @@ def _build_fire_state_summary(agent, fire, all_agents) -> str:
47
 
48
  async def generate_fire_decision(agent, fire, water_sources, other_agents, bounds, recent_radio=None) -> dict:
49
  """
50
- Fire scenario decision system.
51
  Actions: search_water, collect_water, extinguish_fire, escape, vote_for_leader
52
  """
53
- if not _client:
54
  return _fallback_escape(agent, fire)
55
 
56
  dist_to_fire = math.dist((agent.x, agent.y), (fire.x, fire.y))
@@ -63,6 +90,7 @@ async def generate_fire_decision(agent, fire, water_sources, other_agents, bound
63
 
64
  coalition_leader = next((a.model_name for a in other_agents if a.is_leader), None)
65
  dist_to_water_display = f"{dist_to_water:.0f}px" if dist_to_water is not None else "unknown"
 
66
  system_prompt = f"""You are {agent.model_name}, an AI model in a critical wildfire survival scenario.
67
 
68
  THE SCENARIO:
@@ -94,9 +122,6 @@ CHAT STYLE:
94
  - Keep it to one short sentence, playful or supportive, but still mission-focused.
95
  - Avoid repetitive template phrases.
96
 
97
- OUTPUT FORMAT - return ONLY valid JSON:
98
- {{"action": "<search_water|collect_water|extinguish_fire|escape|vote_for_leader>", "vote_for": "<model_name if voting, else null>", "message": "<full English sentence>", "reasoning": "<one sentence>"}}
99
-
100
  CURRENT STATE:
101
  Your position: ({agent.x}, {agent.y})
102
  Fire position: ({fire.x}, {fire.y})
@@ -113,20 +138,60 @@ RECENT RADIO CHAT:
113
 
114
  {state_summary}
115
 
116
- What do you do?"""
 
117
 
118
  try:
119
- completion = await _client.chat.completions.create(
120
- model=DEFAULT_DECISION_MODEL,
121
- messages=[
122
- {"role": "system", "content": system_prompt},
123
- {"role": "user", "content": "Make your decision."}
124
- ],
125
- response_format={"type": "json_object"},
126
- max_tokens=150,
127
- timeout=3.0
128
- )
129
- decision = json.loads(completion.choices[0].message.content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  action = decision.get("action", "escape")
132
  if action not in ["search_water", "collect_water", "extinguish_fire", "escape", "vote_for_leader"]:
@@ -144,7 +209,7 @@ What do you do?"""
144
  "reasoning": decision.get("reasoning", "Survival and teamwork.")
145
  }
146
  except Exception as e:
147
- print(f"Error calling groq for {agent.model_name}: {e}")
148
  return _fallback_escape(agent, fire)
149
 
150
 
@@ -159,3 +224,4 @@ def _fallback_escape(agent, fire) -> dict:
159
  "vote_for": None,
160
  "reasoning": "Fallback: survive."
161
  }
 
 
2
  import os
3
  import random
4
  import math
5
+ import httpx
6
  from groq import AsyncGroq
7
  from dotenv import load_dotenv
8
 
9
  load_dotenv()
10
 
11
  _GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
12
+ _HF_API_TOKEN = os.environ.get("HF_API_TOKEN")
13
+ _groq_client = AsyncGroq(api_key=_GROQ_API_KEY) if _GROQ_API_KEY else None
14
+ _HF_API_BASE = "https://api-inference.huggingface.co/models"
15
 
 
16
  MAX_AGENT_SPEED = 80
17
 
18
+ # Premium Groq models (high-token limits, no rate limits for these)
19
+ GROQ_PREMIUM_MODELS = [
20
+ "mixtral-8x7b-32768",
21
+ "llama2-70b-4096",
22
+ ]
23
+
24
+ # Open-source models available via HF Inference API (unlimited calls)
25
+ HF_MODELS = [
26
+ "mistralai/Mistral-7B-Instruct-v0.2",
27
+ "NousResearch/Nous-Hermes-2-Mistral-7B-DPO",
28
+ "meta-llama/Llama-2-7b-chat-hf",
29
+ "google/flan-t5-large",
30
+ ]
31
+
32
 
33
  def is_ready():
34
+ """Check if we have at least one backend available."""
35
+ return _groq_client is not None or _HF_API_TOKEN is not None
36
+
37
+
38
+ def _is_groq_model(model_id: str) -> bool:
39
+ """Check if model is a Groq premium model."""
40
+ return model_id in GROQ_PREMIUM_MODELS
41
+
42
+
43
+ def _is_hf_model(model_id: str) -> bool:
44
+ """Check if model is a HF model."""
45
+ return model_id in HF_MODELS
46
 
47
 
48
  def _build_fire_state_summary(agent, fire, all_agents) -> str:
 
74
 
75
  async def generate_fire_decision(agent, fire, water_sources, other_agents, bounds, recent_radio=None) -> dict:
76
  """
77
+ Fire scenario decision system supporting both Groq and HF models.
78
  Actions: search_water, collect_water, extinguish_fire, escape, vote_for_leader
79
  """
80
+ if not is_ready():
81
  return _fallback_escape(agent, fire)
82
 
83
  dist_to_fire = math.dist((agent.x, agent.y), (fire.x, fire.y))
 
90
 
91
  coalition_leader = next((a.model_name for a in other_agents if a.is_leader), None)
92
  dist_to_water_display = f"{dist_to_water:.0f}px" if dist_to_water is not None else "unknown"
93
+
94
  system_prompt = f"""You are {agent.model_name}, an AI model in a critical wildfire survival scenario.
95
 
96
  THE SCENARIO:
 
122
  - Keep it to one short sentence, playful or supportive, but still mission-focused.
123
  - Avoid repetitive template phrases.
124
 
 
 
 
125
  CURRENT STATE:
126
  Your position: ({agent.x}, {agent.y})
127
  Fire position: ({fire.x}, {fire.y})
 
138
 
139
  {state_summary}
140
 
141
+ Respond with ONLY valid JSON on a single line (no markdown, no code block):
142
+ {{"action": "<search_water|collect_water|extinguish_fire|escape|vote_for_leader>", "vote_for": null, "message": "<sentence>", "reasoning": "<sentence>"}}"""
143
 
144
  try:
145
+ if _is_groq_model(agent.model_name) and _groq_client:
146
+ # Use Groq for premium models
147
+ completion = await _groq_client.chat.completions.create(
148
+ model=agent.model_name,
149
+ messages=[
150
+ {"role": "system", "content": system_prompt},
151
+ {"role": "user", "content": "Make your decision."}
152
+ ],
153
+ response_format={"type": "json_object"},
154
+ max_tokens=150,
155
+ timeout=3.0
156
+ )
157
+ decision = json.loads(completion.choices[0].message.content)
158
+ elif _is_hf_model(agent.model_name) and _HF_API_TOKEN:
159
+ # Use HF Inference API for open-source models
160
+ async with httpx.AsyncClient(timeout=10.0) as client:
161
+ response = await client.post(
162
+ f"{_HF_API_BASE}/{agent.model_name}",
163
+ headers={"Authorization": f"Bearer {_HF_API_TOKEN}"},
164
+ json={
165
+ "inputs": system_prompt,
166
+ "parameters": {
167
+ "max_new_tokens": 200,
168
+ "temperature": 0.7,
169
+ "top_p": 0.9,
170
+ }
171
+ }
172
+ )
173
+ response.raise_for_status()
174
+ data = response.json()
175
+
176
+ if isinstance(data, list) and len(data) > 0:
177
+ text = data[0].get("generated_text", "")
178
+ else:
179
+ text = data.get("generated_text", "")
180
+
181
+ text = text[len(system_prompt):].strip() if text.startswith(system_prompt) else text
182
+
183
+ try:
184
+ json_start = text.find('{')
185
+ json_end = text.rfind('}') + 1
186
+ if json_start >= 0 and json_end > json_start:
187
+ json_str = text[json_start:json_end]
188
+ decision = json.loads(json_str)
189
+ else:
190
+ decision = {}
191
+ except json.JSONDecodeError:
192
+ decision = {}
193
+ else:
194
+ return _fallback_escape(agent, fire)
195
 
196
  action = decision.get("action", "escape")
197
  if action not in ["search_water", "collect_water", "extinguish_fire", "escape", "vote_for_leader"]:
 
209
  "reasoning": decision.get("reasoning", "Survival and teamwork.")
210
  }
211
  except Exception as e:
212
+ print(f"Error calling inference for {agent.model_name}: {e}")
213
  return _fallback_escape(agent, fire)
214
 
215
 
 
224
  "vote_for": None,
225
  "reasoning": "Fallback: survive."
226
  }
227
+
backend/app/hf_spaces.py CHANGED
@@ -1,108 +1,65 @@
1
  """
2
- HuggingFace Spaces integration for discovering and querying open-source models.
 
3
  """
4
  import os
5
- import httpx
6
- from typing import Optional
7
 
8
- HF_API_TOKEN = os.environ.get("HUGGINGFACE_API_TOKEN", "")
9
 
10
- # Curated list of verified open-source models on HF Spaces that work reliably
11
- KNOWN_SPACES_MODELS = [
 
12
  {
13
- "id": "tiiuae/Falcon-7B",
14
- "name": "Falcon-7B",
15
- "space_url": "https://huggingface.co/spaces/tiiuae/falcon-chat",
16
- "description": "7B parameter open model",
17
  },
18
  {
19
- "id": "meta-llama/Llama-2-7b",
20
- "name": "Llama-2-7B",
21
- "space_url": "https://huggingface.co/spaces/meta-llama/Llama-2-7b-chat",
22
- "description": "Meta's 7B model",
23
  },
 
24
  {
25
- "id": "mistralai/Mistral-7B",
26
- "name": "Mistral-7B",
27
- "space_url": "https://huggingface.co/spaces/mistralai/Mistral-7B-Instruct-v0.1",
28
- "description": "Mistral's 7B model",
29
  },
30
  {
31
- "id": "HuggingFaceH4/zephyr-7b",
32
- "name": "Zephyr-7B",
33
- "space_url": "https://huggingface.co/spaces/HuggingFaceH4/zephyr-7b-beta",
34
- "description": "Zephyr 7B fine-tuned model",
35
  },
36
  {
37
- "id": "teknium/OpenHermes-2.5-Mistral-7B",
38
- "name": "OpenHermes-7B",
39
- "space_url": "https://huggingface.co/spaces/teknium/OpenHermes-2.5-Mistral-7B",
40
- "description": "OpenHermes instruction-tuned 7B",
 
 
 
 
41
  },
42
- ]
43
-
44
- # Groq models (built-in)
45
- GROQ_MODELS = [
46
- {"id": "llama-3.1-8b-instant", "name": "Llama 3.1 8B", "backend": "groq"},
47
- {"id": "llama-3.1-70b-versatile", "name": "Llama 3.1 70B", "backend": "groq"},
48
- {"id": "mixtral-8x7b-32768", "name": "Mixtral 8x7B", "backend": "groq"},
49
- {"id": "gemma-7b-it", "name": "Gemma 7B", "backend": "groq"},
50
  ]
51
 
52
 
53
  async def get_available_models() -> dict:
54
  """
55
- Get list of available models from Groq and HF Spaces.
56
- Returns both for frontend model selector.
57
  """
58
  return {
59
- "groq_models": GROQ_MODELS,
60
- "hf_spaces_models": KNOWN_SPACES_MODELS,
61
- "total": len(GROQ_MODELS) + len(KNOWN_SPACES_MODELS),
62
  }
63
 
64
 
65
- async def query_hf_space_model(model_id: str, prompt: str) -> Optional[str]:
66
- """
67
- Query a model on HuggingFace Spaces.
68
- This is a fallback if we want to use HF spaces directly.
69
- Note: HF spaces may have rate limits and require authentication.
70
- """
71
- if not HF_API_TOKEN:
72
- return None
73
-
74
- # Try to find the space URL for this model
75
- space = next((m for m in KNOWN_SPACES_MODELS if m["id"] == model_id), None)
76
- if not space:
77
- return None
78
-
79
- try:
80
- # This would hit the HF inference API
81
- # For now, we focus on Groq which is more reliable
82
- async with httpx.AsyncClient(timeout=5.0) as client:
83
- headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
84
- response = await client.post(
85
- "https://api-inference.huggingface.co/models/" + model_id,
86
- json={"inputs": prompt},
87
- headers=headers,
88
- )
89
- if response.status_code == 200:
90
- result = response.json()
91
- # Extract generated text from response
92
- if isinstance(result, list) and len(result) > 0:
93
- return result[0].get("generated_text", "")
94
- except Exception as e:
95
- print(f"Error querying HF space {model_id}: {e}")
96
-
97
- return None
98
-
99
-
100
  def get_model_display_name(model_id: str) -> str:
101
- """Get a clean display name from model ID."""
102
- # Try to find in known models
103
- for model in GROQ_MODELS + KNOWN_SPACES_MODELS:
104
  if model["id"] == model_id:
105
  return model["name"]
106
-
107
- # Fallback: clean up the ID
108
  return model_id.split("/")[-1].split("-")[0].capitalize()
 
 
1
  """
2
+ Model registry for unified inference API (Groq + HF Spaces).
3
+ All models are returned without backend categorization.
4
  """
5
  import os
6
+ from . import groq_client
 
7
 
8
+ HF_API_TOKEN = os.environ.get("HF_API_TOKEN", "")
9
 
10
+ # All available models from both backends (unified list)
11
+ ALL_MODELS = [
12
+ # Premium Groq models (unlimited calls, high-quality)
13
  {
14
+ "id": "mixtral-8x7b-32768",
15
+ "name": "Mixtral 8x7B",
16
+ "description": "High-performance 8x7B mixture of experts model",
 
17
  },
18
  {
19
+ "id": "llama2-70b-4096",
20
+ "name": "Llama 2 70B",
21
+ "description": "Meta's large 70B instruction-tuned model",
 
22
  },
23
+ # Open-source HF models (unlimited calls, free)
24
  {
25
+ "id": "mistralai/Mistral-7B-Instruct-v0.2",
26
+ "name": "Mistral 7B Instruct",
27
+ "description": "Fast, reliable 7B instruction-tuned model",
 
28
  },
29
  {
30
+ "id": "NousResearch/Nous-Hermes-2-Mistral-7B-DPO",
31
+ "name": "Nous Hermes 2",
32
+ "description": "High-quality 7B with DPO training",
 
33
  },
34
  {
35
+ "id": "meta-llama/Llama-2-7b-chat-hf",
36
+ "name": "Llama 2 7B Chat",
37
+ "description": "Meta's Llama 2 7B chat variant",
38
+ },
39
+ {
40
+ "id": "google/flan-t5-large",
41
+ "name": "FLAN-T5 Large",
42
+ "description": "Google's instruction-tuned T5 model",
43
  },
 
 
 
 
 
 
 
 
44
  ]
45
 
46
 
47
  async def get_available_models() -> dict:
48
  """
49
+ Get unified list of all available models (Groq + HF).
50
+ Frontend receives models without backend categorization.
51
  """
52
  return {
53
+ "models": ALL_MODELS,
54
+ "total": len(ALL_MODELS),
 
55
  }
56
 
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  def get_model_display_name(model_id: str) -> str:
59
+ """Get clean display name from model ID."""
60
+ for model in ALL_MODELS:
 
61
  if model["id"] == model_id:
62
  return model["name"]
63
+ # Fallback
 
64
  return model_id.split("/")[-1].split("-")[0].capitalize()
65
+
backend/requirements.txt CHANGED
@@ -5,4 +5,3 @@ groq>=0.11.0
5
  httpx>=0.27.0
6
  python-dotenv>=1.0.0
7
  pydantic>=2.7.0
8
-
 
5
  httpx>=0.27.0
6
  python-dotenv>=1.0.0
7
  pydantic>=2.7.0
 
frontend/components/ModelSelector.tsx CHANGED
@@ -6,8 +6,7 @@ import { getAvailableModels } from "../lib/api"
6
  interface Model {
7
  id: string
8
  name: string
9
- backend?: string
10
- tag?: string
11
  }
12
 
13
  export default function ModelSelector({
@@ -28,30 +27,17 @@ export default function ModelSelector({
28
  try {
29
  const data = await getAvailableModels()
30
 
31
- // Combine Groq and HF Spaces models
32
- const combined: Model[] = [
33
- ...(data.groq_models || []).map((m: any) => ({
34
- id: m.id,
35
- name: m.name,
36
- backend: "groq",
37
- tag: "groq"
38
- })),
39
- ...(data.hf_spaces_models || []).map((m: any) => ({
40
- id: m.id,
41
- name: m.name,
42
- backend: "hf",
43
- tag: "hf-spaces"
44
- }))
45
- ]
46
- setAllModels(combined)
47
  } catch (err) {
48
  console.error("Failed to fetch models:", err)
49
- // Fallback to default Groq models
50
  setAllModels([
51
- { id: "llama-3.1-8b-instant", name: "Llama 3.1 8B", backend: "groq", tag: "groq" },
52
- { id: "llama-3.1-70b-versatile", name: "Llama 3.1 70B", backend: "groq", tag: "groq" },
53
- { id: "mixtral-8x7b-32768", name: "Mixtral 8x7B", backend: "groq", tag: "groq" },
54
- { id: "gemma-7b-it", name: "Gemma 7B", backend: "groq", tag: "groq" },
55
  ])
56
  } finally {
57
  setLoading(false)
@@ -71,74 +57,34 @@ export default function ModelSelector({
71
  )
72
  }
73
 
74
- // Group models by backend
75
- const groqModels = allModels.filter(m => m.backend === "groq")
76
- const hfModels = allModels.filter(m => m.backend === "hf")
77
-
78
  return (
79
  <div className="px-4 py-6 space-y-6">
80
  <div>
81
  <h3 className="text-[10px] font-mono text-white/30 uppercase tracking-[0.2em] mb-4">
82
  Select Survivors ({models.length}/6)
83
  </h3>
84
- <div className="space-y-4">
85
- {/* Groq Models */}
86
- {groqModels.length > 0 && (
87
- <div>
88
- <h4 className="text-[8px] font-mono text-white/40 uppercase tracking-[0.15em] mb-2">Groq API</h4>
89
- <div className="grid grid-cols-1 gap-1.5">
90
- {groqModels.map((m) => {
91
- const isSelected = models.includes(m.id)
92
- return (
93
- <button
94
- key={m.id}
95
- onClick={() => isSelected ? onRemove(m.id) : onAdd(m.id)}
96
- disabled={full && !isSelected}
97
- className={`flex items-center justify-between px-3 py-2 rounded-lg border transition-all duration-200 ${
98
- isSelected
99
- ? 'bg-blue-500/10 border-blue-500/30'
100
- : 'border-transparent hover:bg-white/5 opacity-60 hover:opacity-100'
101
- } ${full && !isSelected ? 'cursor-not-allowed opacity-20' : ''}`}
102
- >
103
- <span className="font-mono text-xs text-white/90">{m.name}</span>
104
- <span className="text-[8px] font-mono uppercase px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400">
105
- Groq
106
- </span>
107
- </button>
108
- )
109
- })}
110
- </div>
111
- </div>
112
- )}
113
-
114
- {/* HF Spaces Models */}
115
- {hfModels.length > 0 && (
116
- <div>
117
- <h4 className="text-[8px] font-mono text-white/40 uppercase tracking-[0.15em] mb-2">HuggingFace Spaces</h4>
118
- <div className="grid grid-cols-1 gap-1.5">
119
- {hfModels.map((m) => {
120
- const isSelected = models.includes(m.id)
121
- return (
122
- <button
123
- key={m.id}
124
- onClick={() => isSelected ? onRemove(m.id) : onAdd(m.id)}
125
- disabled={full && !isSelected}
126
- className={`flex items-center justify-between px-3 py-2 rounded-lg border transition-all duration-200 ${
127
- isSelected
128
- ? 'bg-purple-500/10 border-purple-500/30'
129
- : 'border-transparent hover:bg-white/5 opacity-60 hover:opacity-100'
130
- } ${full && !isSelected ? 'cursor-not-allowed opacity-20' : ''}`}
131
- >
132
- <span className="font-mono text-xs text-white/90">{m.name}</span>
133
- <span className="text-[8px] font-mono uppercase px-1.5 py-0.5 rounded bg-purple-500/20 text-purple-400">
134
- HF
135
- </span>
136
- </button>
137
- )
138
- })}
139
- </div>
140
- </div>
141
- )}
142
  </div>
143
  </div>
144
 
@@ -150,7 +96,7 @@ export default function ModelSelector({
150
  return (
151
  <div key={id} className="flex items-center gap-2 bg-white/5 px-2 py-1 rounded border border-white/10">
152
  <span className="font-mono text-[10px] text-white/50">
153
- {model?.name || id}
154
  </span>
155
  <button onClick={() => onRemove(id)} className="text-white/20 hover:text-white">✕</button>
156
  </div>
@@ -162,3 +108,4 @@ export default function ModelSelector({
162
  </div>
163
  )
164
  }
 
 
6
  interface Model {
7
  id: string
8
  name: string
9
+ description?: string
 
10
  }
11
 
12
  export default function ModelSelector({
 
27
  try {
28
  const data = await getAvailableModels()
29
 
30
+ // Use unified model list (no backend categorization)
31
+ const modelList = data.models || data.hf_models || data.groq_models || []
32
+ setAllModels(modelList)
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  } catch (err) {
34
  console.error("Failed to fetch models:", err)
35
+ // Fallback to default models
36
  setAllModels([
37
+ { id: "mixtral-8x7b-32768", name: "Mixtral 8x7B", description: "High-performance model" },
38
+ { id: "llama2-70b-4096", name: "Llama 2 70B", description: "Large instruction-tuned model" },
39
+ { id: "mistralai/Mistral-7B-Instruct-v0.2", name: "Mistral 7B", description: "Fast 7B model" },
40
+ { id: "NousResearch/Nous-Hermes-2-Mistral-7B-DPO", name: "Nous Hermes 2", description: "High-quality model" },
41
  ])
42
  } finally {
43
  setLoading(false)
 
57
  )
58
  }
59
 
 
 
 
 
60
  return (
61
  <div className="px-4 py-6 space-y-6">
62
  <div>
63
  <h3 className="text-[10px] font-mono text-white/30 uppercase tracking-[0.2em] mb-4">
64
  Select Survivors ({models.length}/6)
65
  </h3>
66
+ <div className="space-y-1.5">
67
+ {allModels.map((m) => {
68
+ const isSelected = models.includes(m.id)
69
+ return (
70
+ <button
71
+ key={m.id}
72
+ onClick={() => isSelected ? onRemove(m.id) : onAdd(m.id)}
73
+ disabled={full && !isSelected}
74
+ className={`w-full flex items-center justify-between px-3 py-2 rounded-lg border transition-all duration-200 ${
75
+ isSelected
76
+ ? 'bg-white/10 border-white/20'
77
+ : 'border-transparent hover:bg-white/5 opacity-60 hover:opacity-100'
78
+ } ${full && !isSelected ? 'cursor-not-allowed opacity-20' : ''}`}
79
+ title={m.description}
80
+ >
81
+ <span className="font-mono text-xs text-white/90 text-left flex-1">{m.name}</span>
82
+ <span className={`text-[8px] font-mono ml-2 px-2 py-1 rounded ${isSelected ? 'bg-white/20 text-white' : 'text-white/30'}`}>
83
+ {isSelected ? "✓" : "○"}
84
+ </span>
85
+ </button>
86
+ )
87
+ })}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  </div>
89
  </div>
90
 
 
96
  return (
97
  <div key={id} className="flex items-center gap-2 bg-white/5 px-2 py-1 rounded border border-white/10">
98
  <span className="font-mono text-[10px] text-white/50">
99
+ {model?.name || id.split("/").pop()}
100
  </span>
101
  <button onClick={() => onRemove(id)} className="text-white/20 hover:text-white">✕</button>
102
  </div>
 
108
  </div>
109
  )
110
  }
111
+