ltcai 2.1.0 → 2.2.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/llm_router.py CHANGED
@@ -29,16 +29,14 @@ executor = ThreadPoolExecutor(max_workers=1)
29
29
 
30
30
  try:
31
31
  import mlx.core as mx
32
- from mlx_lm import load as lm_load
33
32
  from mlx_vlm import load as vlm_load
34
33
  VLM_AVAILABLE = True
35
- print("✅ MLX-VLM and MLX-LM are ready for Gemma 4.")
34
+ print("✅ MLX-VLM is ready for multimodal models.")
36
35
  except Exception as e:
37
36
  mx = None
38
- lm_load = None
39
37
  vlm_load = None
40
38
  VLM_AVAILABLE = False
41
- print(f"⚠️ MLX libraries unavailable: {e}")
39
+ print(f"⚠️ MLX-VLM unavailable: {e}")
42
40
 
43
41
  BRAND_NAME = "Lattice AI"
44
42
  LEGACY_BRAND_PATTERNS = [
@@ -77,12 +75,12 @@ OPENAI_COMPATIBLE_PROVIDERS = {
77
75
  "groq": {
78
76
  "env_key": "GROQ_API_KEY",
79
77
  "base_url": "https://api.groq.com/openai/v1",
80
- "default_model": "llama-3.1-8b-instant",
78
+ "default_model": "meta-llama/llama-4-scout-17b-16e-instruct",
81
79
  },
82
80
  "together": {
83
81
  "env_key": "TOGETHER_API_KEY",
84
82
  "base_url": "https://api.together.xyz/v1",
85
- "default_model": "meta-llama/Llama-3.3-70B-Instruct-Turbo",
83
+ "default_model": "Qwen/Qwen3-VL-32B-Instruct",
86
84
  },
87
85
  "xai": {
88
86
  "env_key": "XAI_API_KEY",
@@ -93,14 +91,14 @@ OPENAI_COMPATIBLE_PROVIDERS = {
93
91
  "env_key": "OLLAMA_API_KEY",
94
92
  "base_url_env": "OLLAMA_BASE_URL",
95
93
  "base_url": "http://localhost:11434/v1",
96
- "default_model": "llama3.1",
94
+ "default_model": "hf.co/ggml-org/gemma-4-12B-it-GGUF:Q4_K_M",
97
95
  "api_key_fallback": "ollama",
98
96
  },
99
97
  "vllm": {
100
98
  "env_key": "VLLM_API_KEY",
101
99
  "base_url_env": "VLLM_BASE_URL",
102
100
  "base_url": "http://localhost:8000/v1",
103
- "default_model": "meta-llama/Llama-3.1-8B-Instruct",
101
+ "default_model": "Qwen/Qwen3-VL-8B-Instruct",
104
102
  "api_key_fallback": "vllm",
105
103
  },
106
104
  "lmstudio": {
@@ -137,20 +135,18 @@ PROVIDER_MODEL_CATALOG = {
137
135
  {"id": "anthropic/claude-sonnet-4.6", "name": "Claude Sonnet 4.6 via OpenRouter", "family": "Claude"},
138
136
  {"id": "anthropic/claude-haiku-4.5", "name": "Claude Haiku 4.5 via OpenRouter", "family": "Claude"},
139
137
  {"id": "qwen/qwen3-vl-235b-a22b-instruct", "name": "Qwen3-VL 235B A22B via OpenRouter", "family": "Qwen"},
140
- {"id": "qwen/qwen3-coder", "name": "Qwen3 Coder via OpenRouter", "family": "Qwen"},
138
+ {"id": "google/gemma-4-12b-it", "name": "Gemma 4 12B via OpenRouter", "family": "Gemma"},
141
139
  {"id": "x-ai/grok-2", "name": "Grok 2 via OpenRouter", "family": "Grok"},
142
- {"id": "meta-llama/llama-3.3-70b-instruct", "name": "Llama 3.3 70B via OpenRouter", "family": "Llama"},
140
+ {"id": "meta-llama/llama-4-scout-17b-16e-instruct", "name": "Llama 4 Scout via OpenRouter", "family": "Llama"},
143
141
  {"id": "google/gemini-2.5-flash", "name": "Gemini 2.5 Flash via OpenRouter", "family": "Gemini"},
144
142
  ],
145
143
  "groq": [
146
- {"id": "qwen/qwen3-32b", "name": "Qwen3 32B", "family": "Qwen"},
147
- {"id": "llama-3.1-8b-instant", "name": "Llama 3.1 8B Instant", "family": "Llama"},
148
- {"id": "llama-3.3-70b-versatile", "name": "Llama 3.3 70B Versatile", "family": "Llama"},
144
+ {"id": "meta-llama/llama-4-scout-17b-16e-instruct", "name": "Llama 4 Scout", "family": "Llama"},
149
145
  ],
150
146
  "together": [
151
147
  {"id": "Qwen/Qwen3-VL-32B-Instruct", "name": "Qwen3-VL 32B", "family": "Qwen"},
152
- {"id": "meta-llama/Llama-3.3-70B-Instruct-Turbo", "name": "Llama 3.3 70B Turbo", "family": "Llama"},
153
- {"id": "mistralai/Mixtral-8x22B-Instruct-v0.1", "name": "Mixtral 8x22B", "family": "Mistral"},
148
+ {"id": "google/gemma-4-12b-it", "name": "Gemma 4 12B", "family": "Gemma"},
149
+ {"id": "meta-llama/Llama-4-Scout-17B-16E-Instruct", "name": "Llama 4 Scout", "family": "Llama"},
154
150
  ],
155
151
  "xai": [
156
152
  {"id": "grok-beta", "name": "Grok Beta", "family": "Grok"},
@@ -158,6 +154,40 @@ PROVIDER_MODEL_CATALOG = {
158
154
  ],
159
155
  }
160
156
 
157
+ MODEL_SOURCE_BY_FAMILY = {
158
+ "GPT": ("미국", "OpenAI"),
159
+ "Claude": ("미국", "Anthropic"),
160
+ "Qwen": ("중국", "Alibaba"),
161
+ "Llama": ("미국", "Meta"),
162
+ "Gemini": ("미국", "Google"),
163
+ "Grok": ("미국", "xAI"),
164
+ }
165
+
166
+
167
+ def source_metadata_for_model(provider: str, model: Dict[str, str], *, local_server: bool) -> Dict[str, str]:
168
+ family = str(model.get("family") or "")
169
+ country, company = MODEL_SOURCE_BY_FAMILY.get(family, ("미상", provider.title()))
170
+ if local_server:
171
+ execution_method = "내 컴퓨터에서만 실행"
172
+ internet_requirement = "모델을 다운로드할 때만 인터넷 필요; 실행 중에는 필요 없음"
173
+ else:
174
+ execution_method = "인터넷 연결 후 사용"
175
+ internet_requirement = "내 파일이 인터넷으로 전송될 수 있음"
176
+ return {
177
+ "source_country": country,
178
+ "source_company": company,
179
+ "execution_method": execution_method,
180
+ "internet_requirement": internet_requirement,
181
+ "model_name": model.get("name") or model.get("id") or "",
182
+ "source_display_order": [
183
+ "source_country",
184
+ "source_company",
185
+ "execution_method",
186
+ "internet_requirement",
187
+ "model_name",
188
+ ],
189
+ }
190
+
161
191
  @dataclass
162
192
  class CloudModel:
163
193
  provider: str
@@ -207,37 +237,29 @@ def _resolve_local_hf_model(model_id: str) -> str:
207
237
  return model_id
208
238
 
209
239
  def ensure_mlx_runtime() -> None:
210
- global mx, lm_load, vlm_load, VLM_AVAILABLE
211
- if mx is not None and lm_load is not None:
240
+ global mx, vlm_load, VLM_AVAILABLE
241
+ if mx is not None and vlm_load is not None:
212
242
  return
213
243
  try:
214
244
  import mlx.core as mlx_core
215
- from mlx_lm import load as mlx_lm_load
245
+ from mlx_vlm import load as mlx_vlm_load
216
246
 
217
247
  mx = mlx_core
218
- lm_load = mlx_lm_load
219
- try:
220
- from mlx_vlm import load as mlx_vlm_load
221
- vlm_load = mlx_vlm_load
222
- VLM_AVAILABLE = True
223
- except Exception:
224
- vlm_load = None
225
- VLM_AVAILABLE = False
248
+ vlm_load = mlx_vlm_load
249
+ VLM_AVAILABLE = True
226
250
  mx.set_default_device(mx.gpu)
227
251
  except Exception as e:
228
- raise RuntimeError(f"MLX runtime is not available after install: {e}") from e
252
+ raise RuntimeError(f"MLX-VLM runtime is not available after install: {e}") from e
229
253
 
230
254
  def _mlx_sampler(temperature: float):
231
255
  """Build an MLX sampler callable for the given temperature.
232
256
 
233
- mlx_lm >= 0.20 removed the ``temp`` keyword from generate_step in favour of a
234
- ``sampler`` callable, and mlx_vlm follows the same convention. Passing
235
- ``temp=`` to generate/stream_generate now raises
236
- ``generate_step() got an unexpected keyword argument 'temp'``. Both libraries
237
- accept ``sampler=`` and share make_sampler from mlx_lm.sample_utils.
257
+ Lattice v2.2 keeps local execution on MLX-VLM only. Returning ``None`` lets
258
+ MLX-VLM use its bundled default sampler without pulling another generation
259
+ package into the runtime contract.
238
260
  """
239
- from mlx_lm.sample_utils import make_sampler
240
- return make_sampler(temp=temperature)
261
+ _ = temperature
262
+ return None
241
263
 
242
264
  class LLMRouter:
243
265
  def __init__(self):
@@ -331,8 +353,8 @@ class LLMRouter:
331
353
  return self._load_cloud_model(provider, provider_model, api_key_override=api_key_override, owner=owner)
332
354
 
333
355
  ensure_mlx_runtime()
334
- if mx is None or lm_load is None:
335
- raise RuntimeError("MLX is not available in this process. Run on Apple Silicon with Metal access.")
356
+ if mx is None or vlm_load is None:
357
+ raise RuntimeError("MLX-VLM is not available in this process. Run on Apple Silicon with Metal access.")
336
358
 
337
359
  cache_key = f"{model_id}_{draft_model_id}" if draft_model_id else model_id
338
360
  if cache_key in self._cache:
@@ -348,24 +370,13 @@ class LLMRouter:
348
370
 
349
371
  def _load():
350
372
  mx.set_default_device(mx.gpu)
351
- is_gemma4 = "gemma-4" in model_id.lower() or "gemma4" in model_id.lower()
352
-
353
- # 1. Target 로드 (Gemma 4는 항상 vlm_load 사용)
354
- if is_gemma4 and VLM_AVAILABLE:
355
- print(f"🔄 Loading Target (VLM Mode): {target_model_id}...")
356
- model, tokenizer = vlm_load(target_model_id)
357
- else:
358
- print(f"🔄 Loading Target (LM Mode): {target_model_id}...")
359
- model, tokenizer = lm_load(target_model_id)
360
-
361
- # 2. Draft 로드 (Gemma 4는 항상 vlm_load 사용)
373
+ print(f"🔄 Loading Target (VLM Mode): {target_model_id}...")
374
+ model, tokenizer = vlm_load(target_model_id)
375
+
362
376
  draft_model = None
363
377
  if target_draft_model_id:
364
378
  print(f"🔄 Loading Assistant (VLM Mode): {target_draft_model_id}...")
365
- if is_gemma4 and VLM_AVAILABLE:
366
- draft_model, _ = vlm_load(target_draft_model_id)
367
- else:
368
- draft_model, _ = lm_load(target_draft_model_id)
379
+ draft_model, _ = vlm_load(target_draft_model_id)
369
380
  print(f"✅ Assistant Ready.")
370
381
 
371
382
  return model, tokenizer, draft_model
@@ -418,14 +429,16 @@ class LLMRouter:
418
429
  }]
419
430
  for model in provider_models:
420
431
  model_id = model["id"]
432
+ local_server = provider in local_server_providers
421
433
  items.append({
422
434
  "id": f"{provider}:{model_id}",
423
435
  "name": model.get("name") or f"{provider.title()} · {model_id}",
424
436
  "provider": provider,
425
437
  "family": model.get("family"),
426
- "tag": "local-server" if provider in local_server_providers else "cloud",
438
+ "tag": "local-server" if local_server else "cloud",
427
439
  "available": has_key,
428
440
  "requires": config["env_key"] if not has_key else None,
441
+ **source_metadata_for_model(provider, model, local_server=local_server),
429
442
  })
430
443
  custom = os.getenv("LATTICEAI_CLOUD_MODELS") or ""
431
444
  for raw in [item.strip() for item in custom.split(",") if item.strip()]:
@@ -439,6 +452,11 @@ class LLMRouter:
439
452
  "tag": "cloud",
440
453
  "available": bool(os.getenv(config["env_key"]) or config.get("api_key_fallback")),
441
454
  "requires": None,
455
+ **source_metadata_for_model(
456
+ provider,
457
+ {"id": model, "name": f"{provider.title()} · {model}", "family": provider.title()},
458
+ local_server=provider in local_server_providers,
459
+ ),
442
460
  })
443
461
  return items
444
462
 
@@ -511,25 +529,15 @@ class LLMRouter:
511
529
  return await self._cloud_generate(cached, message, context, max_tokens, temperature)
512
530
 
513
531
  model, tokenizer, draft_model = self._cache[self._current]
514
- is_gemma4 = "gemma-4" in self._current.lower() or "gemma4" in self._current.lower()
515
- prompt = (
516
- self._build_vlm_prompt(model, tokenizer, message, context, 1)
517
- if image_data and is_gemma4 and VLM_AVAILABLE
518
- else self._build_prompt(message, context, tokenizer)
519
- )
532
+ prompt = self._build_vlm_prompt(model, tokenizer, message, context, 1 if image_data else 0)
520
533
 
521
534
  loop = asyncio.get_event_loop()
522
535
 
523
536
  def _gen():
524
537
  import mlx.core as mx
525
538
  mx.set_default_device(mx.gpu)
526
- is_gemma4 = "gemma-4" in self._current.lower() or "gemma4" in self._current.lower()
527
- if is_gemma4 and VLM_AVAILABLE:
528
- from mlx_vlm import generate as vlm_gen
529
- return vlm_gen(model, tokenizer, prompt=prompt, image=self._prep_image(image_data), max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
530
- else:
531
- from mlx_lm import generate as lm_gen
532
- return lm_gen(model, tokenizer, prompt=prompt, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model)
539
+ from mlx_vlm import generate as vlm_gen
540
+ return vlm_gen(model, tokenizer, prompt=prompt, image=self._prep_image(image_data) if image_data else None, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
533
541
  result = await loop.run_in_executor(executor, _gen)
534
542
  # mlx-vlm might return a GenerationResult object; extract the text
535
543
  if hasattr(result, "text"):
@@ -567,12 +575,7 @@ class LLMRouter:
567
575
  return
568
576
 
569
577
  model, tokenizer, draft_model = self._cache[self._current]
570
- is_gemma4 = "gemma-4" in self._current.lower() or "gemma4" in self._current.lower()
571
- prompt = (
572
- self._build_vlm_prompt(model, tokenizer, message, context, 1)
573
- if image_data and is_gemma4 and VLM_AVAILABLE
574
- else self._build_prompt(message, context, tokenizer)
575
- )
578
+ prompt = self._build_vlm_prompt(model, tokenizer, message, context, 1 if image_data else 0)
576
579
  loop = asyncio.get_event_loop()
577
580
  queue = asyncio.Queue()
578
581
 
@@ -580,13 +583,8 @@ class LLMRouter:
580
583
  import mlx.core as mx
581
584
  mx.set_default_device(mx.gpu)
582
585
  try:
583
- is_gemma4 = "gemma-4" in self._current.lower() or "gemma4" in self._current.lower()
584
- if is_gemma4 and VLM_AVAILABLE:
585
- from mlx_vlm import stream_generate as vlm_stream
586
- gen = vlm_stream(model, tokenizer, prompt=prompt, image=self._prep_image(image_data), max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
587
- else:
588
- from mlx_lm import stream_generate as lm_stream
589
- gen = lm_stream(model, tokenizer, prompt=prompt, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model)
586
+ from mlx_vlm import stream_generate as vlm_stream
587
+ gen = vlm_stream(model, tokenizer, prompt=prompt, image=self._prep_image(image_data) if image_data else None, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
590
588
 
591
589
  for chunk in gen:
592
590
  text = chunk.text if hasattr(chunk, "text") else (chunk[0] if isinstance(chunk, tuple) else str(chunk))
@@ -675,13 +673,8 @@ class LLMRouter:
675
673
  def _gen():
676
674
  import mlx.core as mx
677
675
  mx.set_default_device(mx.gpu)
678
- is_gemma4 = "gemma-4" in self._current.lower() or "gemma4" in self._current.lower()
679
- if is_gemma4 and VLM_AVAILABLE:
680
- from mlx_vlm import generate as vlm_gen
681
- return vlm_gen(model, tokenizer, prompt=prompt, image=None, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
682
- else:
683
- from mlx_lm import generate as lm_gen
684
- return lm_gen(model, tokenizer, prompt=prompt, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model)
676
+ from mlx_vlm import generate as vlm_gen
677
+ return vlm_gen(model, tokenizer, prompt=prompt, image=None, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
685
678
  result = await loop.run_in_executor(executor, _gen)
686
679
  if hasattr(result, "text"):
687
680
  return normalize_branding(result.text)
@@ -742,13 +735,8 @@ class LLMRouter:
742
735
  import mlx.core as mx
743
736
  mx.set_default_device(mx.gpu)
744
737
  try:
745
- is_gemma4 = "gemma-4" in self._current.lower() or "gemma4" in self._current.lower()
746
- if is_gemma4 and VLM_AVAILABLE:
747
- from mlx_vlm import stream_generate as vlm_stream
748
- gen = vlm_stream(model, tokenizer, prompt=prompt, image=None, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
749
- else:
750
- from mlx_lm import stream_generate as lm_stream
751
- gen = lm_stream(model, tokenizer, prompt=prompt, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model)
738
+ from mlx_vlm import stream_generate as vlm_stream
739
+ gen = vlm_stream(model, tokenizer, prompt=prompt, image=None, max_tokens=max_tokens, sampler=_mlx_sampler(temperature), draft_model=draft_model, draft_kind="mtp")
752
740
  for chunk in gen:
753
741
  text = chunk.text if hasattr(chunk, "text") else (chunk[0] if isinstance(chunk, tuple) else str(chunk))
754
742
  loop.call_soon_threadsafe(queue.put_nowait, text)
package/ltcai_cli.py CHANGED
@@ -97,9 +97,8 @@ def doctor() -> int:
97
97
  ("FastAPI", _has_module("fastapi"), "required server dependency", True),
98
98
  ("Uvicorn", _has_module("uvicorn"), "required server dependency", True),
99
99
  ("OpenAI SDK", _has_module("openai"), "required for cloud providers", False),
100
- ("MLX", _has_module("mlx"), "required for Apple Silicon local models", False),
101
- ("MLX-LM", _has_module("mlx_lm"), "required for local text models", False),
102
- ("MLX-VLM", _has_module("mlx_vlm"), "required for Gemma/VLM models", False),
100
+ ("MLX", _has_module("mlx"), "required for Apple Silicon multimodal models", False),
101
+ ("MLX-VLM", _has_module("mlx_vlm"), "required for Gemma-4/VLM models", False),
103
102
  ("Ollama binary", shutil.which("ollama") is not None, "optional local-server engine", False),
104
103
  ]
105
104
  data_dir = Path(os.getenv("LATTICEAI_DATA_DIR") or Path.home() / ".ltcai")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Lattice AI Workspace OS for local-first graph, memory, agent, workflow, and skill operations",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
package/static/chat.html CHANGED
@@ -237,8 +237,7 @@
237
237
  <button class="hdc-btn" onclick="openDataGraph()"><i class="ti ti-arrow-right"></i> 그래프 보기</button>
238
238
  </div>
239
239
 
240
- <!-- 자동 설정 (고급/관리자 모드만) -->
241
- <div class="hdc-card hdc-setup" id="home-setup-card" style="display:none">
240
+ <div class="hdc-card hdc-setup" id="home-setup-card">
242
241
  <div class="hdc-title"><i class="ti ti-settings-automation"></i> 자동 설정</div>
243
242
  <div class="hdc-setup-count">
244
243
  <span id="home-setup-num">—</span>
@@ -279,7 +278,7 @@
279
278
  <div class="chat-capability-row" id="chat-capability-row">
280
279
  <span>파일 생성</span>
281
280
  <span>지식 정리</span>
282
- <span>로컬 런타임</span>
281
+ <span>내 컴퓨터에서 실행</span>
283
282
  </div>
284
283
  </div>
285
284
  </section>
@@ -359,12 +358,12 @@
359
358
  <button class="mode-card" id="mode-card-advanced" onclick="selectMode('advanced')">
360
359
  <div class="mode-icon"><i class="ti ti-terminal-2"></i></div>
361
360
  <h3 data-i18n="mode_advanced">고급 모드</h3>
362
- <span data-i18n="mode_advanced_sub">모델 상태, 런타임 설정, 고급 도구 관리</span>
361
+ <span data-i18n="mode_advanced_sub">같은 기능을 자세한 설명으로 표시</span>
363
362
  </button>
364
363
  <button class="mode-card" id="mode-card-admin" onclick="selectMode('admin')">
365
364
  <div class="mode-icon"><i class="ti ti-shield-lock"></i></div>
366
365
  <h3 data-i18n="mode_admin">관리자 모드</h3>
367
- <span data-i18n="mode_admin_sub">운영자용 관리자 대시보드</span>
366
+ <span data-i18n="mode_admin_sub">사용자, 정책, 감사 로그 관리</span>
368
367
  </button>
369
368
  </div>
370
369
  </section>
@@ -375,7 +374,7 @@
375
374
  <div class="model-panel-header">
376
375
  <div>
377
376
  <h2 data-i18n="model_switcher">모델 스위처</h2>
378
- <p style="color: var(--muted); font-size: 12px; margin-top: 4px;" data-i18n="model_switcher_sub">실행 엔진을 설치하고, 엔진에 맞는 local/cloud LLM을 선택합니다.</p>
377
+ <p style="color: var(--muted); font-size: 12px; margin-top: 4px;" data-i18n="model_switcher_sub">제작 국가, 제작 회사, 실행 방식, 인터넷 사용 여부를 확인하고 모델을 선택합니다.</p>
379
378
  </div>
380
379
  <button class="admin-close" onclick="closeModelPanel()"><i class="ti ti-x"></i></button>
381
380
  </div>
@@ -227,7 +227,7 @@ const chatViewport = document.getElementById('chat-viewport');
227
227
  my_status: '내 상태 보기', auto_setup: '자동 설정',
228
228
  nav_home: '홈', nav_chat: '채팅', nav_workspace: 'Workspace OS', nav_knowledge: '지식 그래프',
229
229
  nav_pipeline: '파이프라인', nav_files: '내 컴퓨터',
230
- nav_model_status: '모델 상태', nav_runtime: '런타임 설정',
230
+ nav_model_status: '모델 상태', nav_runtime: '실행 방식 설정',
231
231
  nav_advanced_settings: '고급 설정',
232
232
  history_search_ph: '대화 검색...', new_chat: 'New Chat',
233
233
  history_section: '대화', history_empty: '아직 저장된 대화가 없습니다.',
@@ -235,7 +235,7 @@ const chatViewport = document.getElementById('chat-viewport');
235
235
  confirm_delete_chat: '이 대화를 삭제할까요?',
236
236
  home_greeting: '안녕하세요, {name}님',
237
237
  home_greeting_short: '안녕하세요',
238
- ops_ai_model: 'AI 모델', ops_local_runtime: '로컬 런타임',
238
+ ops_ai_model: 'AI 모델', ops_local_runtime: ' 컴퓨터에서 실행',
239
239
  ops_admin_network: '관리자 네트워크', ops_admin_security: '관리자 보안',
240
240
  ops_pipeline_value: '멀티 LLM 파이프라인',
241
241
  ops_pipeline_meta: 'Plan → Execute → Review 모델 설정',
@@ -248,7 +248,7 @@ const chatViewport = document.getElementById('chat-viewport');
248
248
  home_recent_files: '최근 파일', home_open_files: '파일 열기', home_no_files: '파일이 없습니다',
249
249
  chat_intro_title: 'Lattice AI',
250
250
  chat_intro_desc: '로컬 모델, 파일, 지식 그래프, 멀티모달 작업을 한 대화 흐름에서 연결하는 개인 AI 워크스페이스입니다.',
251
- chat_cap_file: '파일 생성', chat_cap_knowledge: '지식 정리', chat_cap_runtime: '로컬 런타임',
251
+ chat_cap_file: '파일 생성', chat_cap_knowledge: '지식 정리', chat_cap_runtime: ' 컴퓨터에서 실행',
252
252
  // 계정 모달
253
253
  tab_profile: '프로필', tab_password: '비밀번호',
254
254
  label_name: '이름', label_nickname: '닉네임',
@@ -283,12 +283,12 @@ const chatViewport = document.getElementById('chat-viewport');
283
283
  mode_default: '기본 모드',
284
284
  mode_default_sub: '대화, 파일 생성, 지식 정리를 한 화면에서',
285
285
  mode_advanced: '고급 모드',
286
- mode_advanced_sub: '모델 상태, 런타임 설정, 고급 설정',
286
+ mode_advanced_sub: '같은 기능을 자세한 설명으로 표시',
287
287
  mode_admin: '관리자 모드',
288
- mode_admin_sub: '운영자용 관리자 대시보드',
288
+ mode_admin_sub: '사용자, 정책, 감사 로그 관리',
289
289
  // 패널 제목
290
290
  model_switcher: '모델 스위처',
291
- model_switcher_sub: '실행 엔진을 설치하고, 엔진에 맞는 local/cloud LLM을 선택합니다.',
291
+ model_switcher_sub: '제작 국가, 제작 회사, 실행 방식, 인터넷 사용 여부를 확인하고 모델을 선택합니다.',
292
292
  // 권한 다이얼로그
293
293
  perm_title: '파일 접근 요청', btn_deny: '거부', btn_allow: '허용',
294
294
  },
@@ -306,7 +306,7 @@ const chatViewport = document.getElementById('chat-viewport');
306
306
  my_status: 'My Status', auto_setup: 'Auto Setup',
307
307
  nav_home: 'Home', nav_chat: 'Chat', nav_workspace: 'Workspace OS', nav_knowledge: 'Knowledge Graph',
308
308
  nav_pipeline: 'Pipeline', nav_files: 'My Computer',
309
- nav_model_status: 'Model Status', nav_runtime: 'Runtime Settings',
309
+ nav_model_status: 'Model Status', nav_runtime: 'Execution Settings',
310
310
  nav_advanced_settings: 'Advanced Settings',
311
311
  history_search_ph: 'Search chats...', new_chat: 'New Chat',
312
312
  history_section: 'Chats', history_empty: 'No saved chats yet.',
@@ -314,7 +314,7 @@ const chatViewport = document.getElementById('chat-viewport');
314
314
  confirm_delete_chat: 'Delete this chat?',
315
315
  home_greeting: 'Hello, {name}',
316
316
  home_greeting_short: 'Hello',
317
- ops_ai_model: 'AI model', ops_local_runtime: 'Local runtime',
317
+ ops_ai_model: 'AI model', ops_local_runtime: 'Runs on this computer',
318
318
  ops_admin_network: 'Admin Network', ops_admin_security: 'Admin Security',
319
319
  ops_pipeline_value: 'Multi-LLM Pipeline',
320
320
  ops_pipeline_meta: 'Plan → Execute → Review model setup',
@@ -327,7 +327,7 @@ const chatViewport = document.getElementById('chat-viewport');
327
327
  home_recent_files: 'Recent Files', home_open_files: 'Open Files', home_no_files: 'No files yet',
328
328
  chat_intro_title: 'Lattice AI',
329
329
  chat_intro_desc: 'A personal AI workspace that connects local models, files, knowledge graphs, and multimodal work in one conversation flow.',
330
- chat_cap_file: 'File creation', chat_cap_knowledge: 'Knowledge organizing', chat_cap_runtime: 'Local runtime',
330
+ chat_cap_file: 'File creation', chat_cap_knowledge: 'Knowledge organizing', chat_cap_runtime: 'Runs on this computer',
331
331
  // Account modal
332
332
  tab_profile: 'Profile', tab_password: 'Password',
333
333
  label_name: 'Name', label_nickname: 'Nickname',
@@ -367,7 +367,7 @@ const chatViewport = document.getElementById('chat-viewport');
367
367
  mode_admin_sub: 'Admin dashboard for operators',
368
368
  // Panel titles
369
369
  model_switcher: 'Model Switcher',
370
- model_switcher_sub: 'Install a runtime engine and select a local/cloud LLM.',
370
+ model_switcher_sub: 'Check maker country, maker company, execution method, internet use, then select a model.',
371
371
  // Permission dialog
372
372
  perm_title: 'File Access Request', btn_deny: 'Deny', btn_allow: 'Allow',
373
373
  }
@@ -650,11 +650,8 @@ const chatViewport = document.getElementById('chat-viewport');
650
650
  }
651
651
 
652
652
  async function _loadHomeDashboard() {
653
- const mode = getCurrentMode();
654
-
655
- // 자동 설정 카드: 고급/관리자 모드만
656
653
  const setupCard = document.getElementById('home-setup-card');
657
- if (setupCard) setupCard.style.display = (mode === 'advanced' || mode === 'admin') ? 'flex' : 'none';
654
+ if (setupCard) setupCard.style.display = 'flex';
658
655
 
659
656
  // 모델 + sysinfo 병렬 fetch
660
657
  try {
@@ -953,7 +950,7 @@ const chatViewport = document.getElementById('chat-viewport');
953
950
  const selected = models.find(item => item.checked && !item.disabled && (item.model_id || item.action?.model_id))
954
951
  || models.find(item => !item.disabled && (item.model_id || item.action?.model_id));
955
952
  const zero = onboardingRecs?.summary?.zero_config || onboardingEnv?.zero_config?.recommend || {};
956
- const modelId = selected?.model_id || selected?.action?.model_id || zero.model_id || 'mlx-community/Llama-3.2-3B-Instruct-4bit';
953
+ const modelId = selected?.model_id || selected?.action?.model_id || zero.model_id || 'mlx-community/gemma-4-12b-it-4bit';
957
954
  const engineItem = (onboardingRecs?.engines || []).find(item => item.checked && !item.disabled);
958
955
  const runtime = engineItem?.name || (zero.runtime === 'mlx' ? 'MLX' : zero.runtime) || 'MLX';
959
956
  return {
@@ -1351,7 +1348,7 @@ const chatViewport = document.getElementById('chat-viewport');
1351
1348
  <button class="onboarding-mode" onclick="finishOnboarding('advanced')">
1352
1349
  <i class="ti ti-terminal-2"></i>
1353
1350
  <h3>고급 모드</h3>
1354
- <p>모델 상태, 런타임 설정, 고급 설정까지 함께 다룹니다.</p>
1351
+ <p>같은 기능을 유지하면서 모델, 메모리, 실행 방식 설명을 더 자세히 표시합니다.</p>
1355
1352
  </button>
1356
1353
  ${adminCard}
1357
1354
  </div>
@@ -1528,7 +1525,7 @@ const chatViewport = document.getElementById('chat-viewport');
1528
1525
  const isUnavailable = unsupported || (!isLocalEngine && engineMissing) || keyMissing || verifyFailed;
1529
1526
  const badge = unsupported ? '현재 환경 미지원'
1530
1527
  : engineMissing && isLocalEngine ? '설치 후 자동 로드'
1531
- : engineMissing ? '엔진 설치 필요'
1528
+ : engineMissing ? '실행 도구 설치 필요'
1532
1529
  : needsPull ? '다운로드 후 자동 로드'
1533
1530
  : keyMissing ? `필요: ${model.requires || 'API key'}`
1534
1531
  : verifyFailed ? `실패: ${model.verify_reason || '검증 실패'}`
@@ -1539,11 +1536,19 @@ const chatViewport = document.getElementById('chat-viewport');
1539
1536
  const action = isLocalEngine
1540
1537
  ? `selectModelByCard('${encodeURIComponent(model.id)}', '${engine?.id || ''}')`
1541
1538
  : `loadSelectedModel('${encodeURIComponent(model.id)}', '${engine?.id || ''}')`;
1539
+ const sourceLine = [
1540
+ model.source_country,
1541
+ model.source_company,
1542
+ model.execution_method,
1543
+ model.internet_requirement,
1544
+ model.model_name || model.name,
1545
+ ].filter(Boolean).join(' · ');
1546
+ const detailLine = sourceLine || `${model.id} · ${badge}`;
1542
1547
  return `
1543
1548
  <button class="model-option${cls}" ${isUnavailable ? 'disabled' : ''} onclick="${action}">
1544
1549
  <div>
1545
1550
  <strong>${escapeHtml(model.name || compactModelName(model.id))}</strong>
1546
- <span>${escapeHtml(model.id)} · ${escapeHtml(badge)}</span>
1551
+ <span>${escapeHtml(detailLine)}${sourceLine ? `<br>${escapeHtml(model.id)} · ${escapeHtml(badge)}` : ''}</span>
1547
1552
  </div>
1548
1553
  <i class="ti ${icon}"></i>
1549
1554
  </button>
@@ -1556,12 +1561,9 @@ const chatViewport = document.getElementById('chat-viewport');
1556
1561
  if (raw.includes('claude')) return 'Claude';
1557
1562
  if (raw.includes('grok')) return 'Grok';
1558
1563
  if (raw.includes('gemini')) return 'Gemini';
1559
- if (raw.includes('mistral') || raw.includes('mixtral')) return 'Mistral';
1560
1564
  if (raw.includes('qwen')) return 'Qwen';
1561
1565
  if (raw.includes('llama')) return 'Llama';
1562
1566
  if (raw.includes('gemma')) return 'Gemma';
1563
- if (raw.includes('phi')) return 'Phi';
1564
- if (raw.includes('deepseek')) return 'DeepSeek';
1565
1567
  return (model?.family || '기타');
1566
1568
  }
1567
1569
 
@@ -1642,17 +1644,17 @@ const chatViewport = document.getElementById('chat-viewport');
1642
1644
  const cloudEngines = cachedEngineList.filter(engine => engine.kind === 'cloud');
1643
1645
  const isLocal = modelPanelFilter === 'local';
1644
1646
  const target = isLocal ? localEngines : cloudEngines;
1645
- const emptyText = isLocal ? '등록된 로컬 엔진이 없습니다.' : '등록된 클라우드 엔진이 없습니다.';
1647
+ const emptyText = isLocal ? ' 컴퓨터에서 실행할 수 있는 항목이 없습니다.' : '인터넷 연결 사용할 수 있는 항목이 없습니다.';
1646
1648
 
1647
1649
  modelList.innerHTML = `
1648
- <div class="model-group-title">EXECUTION ENGINES</div>
1650
+ <div class="model-group-title">실행 방식</div>
1649
1651
  <div class="model-filter">
1650
- <button class="model-filter-btn ${isLocal ? 'active' : ''}" onclick="setModelPanelFilter('local')">Local LLM</button>
1651
- <button class="model-filter-btn ${!isLocal ? 'active' : ''}" onclick="setModelPanelFilter('cloud')">Cloud LLM</button>
1652
+ <button class="model-filter-btn ${isLocal ? 'active' : ''}" onclick="setModelPanelFilter('local')">내 컴퓨터에서만 실행</button>
1653
+ <button class="model-filter-btn ${!isLocal ? 'active' : ''}" onclick="setModelPanelFilter('cloud')">인터넷 연결 후 사용</button>
1652
1654
  </div>
1653
1655
  ${!isLocal ? `
1654
1656
  <div style="display:flex;justify-content:flex-end;margin:-2px 0 8px;">
1655
- <button class="admin-action" onclick="verifyCloudModels(true)"><i class="ti ti-activity"></i> Cloud 실사용 테스트</button>
1657
+ <button class="admin-action" onclick="verifyCloudModels(true)"><i class="ti ti-activity"></i> 인터넷 모델 실사용 테스트</button>
1656
1658
  </div>
1657
1659
  ` : ''}
1658
1660
  ${target.length ? target.map(engineCardHtml).join('') : `<div class="sensitivity-preview">${emptyText}</div>`}
@@ -1680,7 +1682,7 @@ const chatViewport = document.getElementById('chat-viewport');
1680
1682
 
1681
1683
  async function verifyCloudModels(force = true) {
1682
1684
  const modelList = document.getElementById('model-list');
1683
- modelList.innerHTML = `<div class="sensitivity-preview">Cloud 모델 실사용 테스트 중입니다... (provider별로 수 초~수십 초)</div>`;
1685
+ modelList.innerHTML = `<div class="sensitivity-preview">인터넷 모델 실사용 테스트 중입니다... (연결 방식별로 수 초~수십 초)</div>`;
1684
1686
  try {
1685
1687
  const res = await apiFetch('/engines/verify-cloud', {
1686
1688
  method: 'POST',
@@ -1688,9 +1690,9 @@ const chatViewport = document.getElementById('chat-viewport');
1688
1690
  body: JSON.stringify({ force })
1689
1691
  });
1690
1692
  const data = await res.json();
1691
- if (!res.ok) throw new Error(data.detail || 'Cloud 실사용 테스트 실패');
1693
+ if (!res.ok) throw new Error(data.detail || '인터넷 모델 실사용 테스트 실패');
1692
1694
  await openModelPanel();
1693
- addMessage('ai', `Cloud 모델 실사용 테스트를 완료했습니다. 실패한 모델은 잠금 상태로 표시됩니다.`);
1695
+ addMessage('ai', `인터넷 모델 실사용 테스트를 완료했습니다. 실패한 모델은 잠금 상태로 표시됩니다.`);
1694
1696
  } catch (e) {
1695
1697
  modelList.innerHTML = `
1696
1698
  <div class="sensitivity-preview">${escapeHtml(e.message)}</div>
@@ -1832,7 +1834,7 @@ const chatViewport = document.getElementById('chat-viewport');
1832
1834
  </div>
1833
1835
  </div>
1834
1836
  <div id="model-download-detail" class="model-download-detail">
1835
- 엔진 설치, 모델 다운로드, 서버 시작, 로드까지 자동으로 진행합니다. 첫 실행은 수 분이 걸릴 수 있습니다.
1837
+ 실행 도구 설치, 모델 다운로드, 연결 준비, 로드까지 자동으로 진행합니다. 첫 실행은 수 분이 걸릴 수 있습니다.
1836
1838
  </div>
1837
1839
  </div>
1838
1840
  `;
@@ -4363,7 +4365,7 @@ const chatViewport = document.getElementById('chat-viewport');
4363
4365
  const keys = env.api_keys || {};
4364
4366
 
4365
4367
  const mlxLabel = mlx.available
4366
- ? (mlx.mlx_lm && mlx.mlx_vlm ? 'MLX-LM · MLX-VLM 설치됨' : mlx.mlx_lm ? 'MLX-LM 설치됨' : '부분 설치')
4368
+ ? (mlx.mlx_vlm ? 'MLX-VLM 설치됨' : 'MLX 설치됨 · MLX-VLM 필요')
4367
4369
  : '미설치';
4368
4370
 
4369
4371
  const cloudKeys = Object.entries(keys).filter(([,v]) => v).map(([k]) => k.toUpperCase());
@@ -4378,7 +4380,7 @@ const chatViewport = document.getElementById('chat-viewport');
4378
4380
  { icon: mlx.available ? '✅' : '⚠️', label: 'MLX', value: mlxLabel, ok: mlx.available },
4379
4381
  { icon: tools.ollama ? '✅' : '○', label: 'Ollama', value: tools.ollama ? '설치됨' : '미설치', ok: true },
4380
4382
  { icon: tools.brew ? '✅' : '○', label: 'Homebrew', value: tools.brew ? '설치됨' : '미설치', ok: true },
4381
- { icon: cloudKeys.length ? '✅' : '○', label: 'Cloud API',
4383
+ { icon: cloudKeys.length ? '✅' : '○', label: '인터넷 AI',
4382
4384
  value: cloudKeys.length ? cloudKeys.join(', ') : '없음', ok: true },
4383
4385
  { icon: env.os === 'Darwin' ? '🍎' : '🐧',
4384
4386
  label: '운영체제',
@@ -4607,10 +4609,6 @@ const chatViewport = document.getElementById('chat-viewport');
4607
4609
  let _mcpCurrentTab = 'registry';
4608
4610
 
4609
4611
  async function openMcpModal() {
4610
- if (getCurrentMode() === 'default') {
4611
- showToast('고급 모드에서 사용할 수 있습니다.');
4612
- return;
4613
- }
4614
4612
  document.getElementById('mcp-modal-overlay').classList.add('open');
4615
4613
  await renderMcpModal(_mcpCurrentTab);
4616
4614
  }