ltcai 8.9.0 → 9.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.
Files changed (208) hide show
  1. package/README.md +81 -58
  2. package/auto_setup.py +7 -904
  3. package/desktop/electron/README.md +9 -0
  4. package/desktop/electron/main.cjs +5 -3
  5. package/docs/CHANGELOG.md +221 -238
  6. package/docs/COMMUNITY_AND_PLUGINS.md +3 -2
  7. package/docs/DEVELOPMENT.md +53 -14
  8. package/docs/LEGACY_COMPATIBILITY.md +4 -4
  9. package/docs/ONBOARDING.md +10 -2
  10. package/docs/OPERATIONS.md +8 -4
  11. package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
  12. package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
  13. package/docs/TRUST_MODEL.md +5 -2
  14. package/docs/WHY_LATTICE.md +15 -10
  15. package/docs/WORKFLOW_DESIGNER.md +22 -0
  16. package/docs/kg-schema.md +13 -2
  17. package/docs/mcp-tools.md +17 -6
  18. package/docs/privacy.md +19 -3
  19. package/docs/public-deploy.md +32 -3
  20. package/docs/security-model.md +46 -11
  21. package/lattice_brain/__init__.py +1 -1
  22. package/lattice_brain/archive.py +4 -14
  23. package/lattice_brain/context.py +66 -9
  24. package/lattice_brain/embeddings.py +38 -2
  25. package/lattice_brain/graph/_kg_common.py +27 -462
  26. package/lattice_brain/graph/_kg_constants.py +243 -0
  27. package/lattice_brain/graph/_kg_fsutil.py +293 -0
  28. package/lattice_brain/graph/discovery.py +0 -948
  29. package/lattice_brain/graph/discovery_index.py +1126 -0
  30. package/lattice_brain/graph/documents.py +44 -9
  31. package/lattice_brain/graph/ingest.py +47 -20
  32. package/lattice_brain/graph/provenance.py +13 -6
  33. package/lattice_brain/graph/retrieval.py +141 -610
  34. package/lattice_brain/graph/retrieval_docgen.py +243 -0
  35. package/lattice_brain/graph/retrieval_vector.py +460 -0
  36. package/lattice_brain/graph/store.py +6 -0
  37. package/lattice_brain/ingestion.py +69 -4
  38. package/lattice_brain/portability.py +29 -23
  39. package/lattice_brain/quality.py +98 -4
  40. package/lattice_brain/runtime/agent_runtime.py +169 -7
  41. package/lattice_brain/runtime/hooks.py +30 -13
  42. package/lattice_brain/runtime/multi_agent.py +27 -8
  43. package/lattice_brain/runtime/statuses.py +10 -0
  44. package/lattice_brain/utils.py +46 -0
  45. package/lattice_brain/workflow.py +1 -5
  46. package/latticeai/__init__.py +1 -1
  47. package/latticeai/api/admin.py +1 -1
  48. package/latticeai/api/agent_registry.py +10 -8
  49. package/latticeai/api/agents.py +22 -3
  50. package/latticeai/api/auth.py +78 -16
  51. package/latticeai/api/browser.py +303 -34
  52. package/latticeai/api/chat.py +355 -952
  53. package/latticeai/api/chat_agent_http.py +356 -0
  54. package/latticeai/api/chat_contracts.py +54 -0
  55. package/latticeai/api/chat_documents.py +256 -0
  56. package/latticeai/api/chat_helpers.py +250 -0
  57. package/latticeai/api/chat_history.py +99 -0
  58. package/latticeai/api/chat_intents.py +302 -0
  59. package/latticeai/api/chat_stream.py +115 -0
  60. package/latticeai/api/computer_use.py +211 -40
  61. package/latticeai/api/health.py +9 -3
  62. package/latticeai/api/hooks.py +17 -6
  63. package/latticeai/api/knowledge_graph.py +78 -14
  64. package/latticeai/api/local_files.py +7 -2
  65. package/latticeai/api/marketplace.py +11 -0
  66. package/latticeai/api/mcp.py +102 -23
  67. package/latticeai/api/models.py +72 -33
  68. package/latticeai/api/network.py +5 -5
  69. package/latticeai/api/permissions.py +70 -29
  70. package/latticeai/api/plugins.py +21 -7
  71. package/latticeai/api/portability.py +5 -5
  72. package/latticeai/api/realtime.py +33 -5
  73. package/latticeai/api/setup.py +2 -2
  74. package/latticeai/api/static_routes.py +123 -24
  75. package/latticeai/api/tools.py +97 -14
  76. package/latticeai/api/workflow_designer.py +37 -0
  77. package/latticeai/api/workspace.py +2 -1
  78. package/latticeai/app_factory.py +185 -405
  79. package/latticeai/core/agent.py +19 -9
  80. package/latticeai/core/agent_registry.py +1 -5
  81. package/latticeai/core/config.py +23 -3
  82. package/latticeai/core/context_builder.py +21 -3
  83. package/latticeai/core/invitations.py +1 -4
  84. package/latticeai/core/io_utils.py +45 -0
  85. package/latticeai/core/legacy_compatibility.py +24 -3
  86. package/latticeai/core/local_embeddings.py +2 -4
  87. package/latticeai/core/marketplace.py +33 -2
  88. package/latticeai/core/mcp_catalog.py +450 -0
  89. package/latticeai/core/mcp_registry.py +2 -441
  90. package/latticeai/core/plugins.py +15 -0
  91. package/latticeai/core/policy.py +1 -1
  92. package/latticeai/core/realtime.py +38 -15
  93. package/latticeai/core/sessions.py +7 -2
  94. package/latticeai/core/timeutil.py +10 -0
  95. package/latticeai/core/tool_registry.py +90 -18
  96. package/latticeai/core/users.py +5 -14
  97. package/latticeai/core/workspace_graph_trace.py +31 -5
  98. package/latticeai/core/workspace_memory.py +3 -1
  99. package/latticeai/core/workspace_os.py +18 -7
  100. package/latticeai/core/workspace_os_utils.py +2 -21
  101. package/latticeai/core/workspace_permissions.py +2 -1
  102. package/latticeai/core/workspace_plugins.py +1 -1
  103. package/latticeai/core/workspace_runs.py +14 -5
  104. package/latticeai/core/workspace_skills.py +1 -1
  105. package/latticeai/core/workspace_snapshots.py +2 -1
  106. package/latticeai/core/workspace_timeline.py +2 -1
  107. package/latticeai/integrations/telegram_bot.py +96 -40
  108. package/latticeai/models/model_providers.py +111 -0
  109. package/latticeai/models/router.py +189 -173
  110. package/latticeai/runtime/access_runtime.py +62 -4
  111. package/latticeai/runtime/audit_runtime.py +27 -16
  112. package/latticeai/runtime/automation_runtime.py +22 -7
  113. package/latticeai/runtime/brain_runtime.py +19 -7
  114. package/latticeai/runtime/chat_wiring.py +2 -0
  115. package/latticeai/runtime/config_runtime.py +58 -26
  116. package/latticeai/runtime/context_runtime.py +16 -4
  117. package/latticeai/runtime/history_runtime.py +163 -0
  118. package/latticeai/runtime/hooks_runtime.py +1 -1
  119. package/latticeai/runtime/model_wiring.py +33 -5
  120. package/latticeai/runtime/namespace_runtime.py +163 -0
  121. package/latticeai/runtime/network_config_runtime.py +56 -0
  122. package/latticeai/runtime/platform_runtime_wiring.py +4 -3
  123. package/latticeai/runtime/review_wiring.py +1 -1
  124. package/latticeai/runtime/router_registration.py +34 -1
  125. package/latticeai/runtime/security_runtime.py +110 -13
  126. package/latticeai/runtime/sso_config_runtime.py +128 -0
  127. package/latticeai/runtime/stages.py +27 -0
  128. package/latticeai/runtime/user_key_runtime.py +106 -0
  129. package/latticeai/server_app.py +5 -1
  130. package/latticeai/services/architecture_readiness.py +74 -5
  131. package/latticeai/services/brain_automation.py +3 -26
  132. package/latticeai/services/chat_service.py +205 -15
  133. package/latticeai/services/local_knowledge.py +423 -0
  134. package/latticeai/services/memory_service.py +268 -21
  135. package/latticeai/services/model_engines.py +48 -33
  136. package/latticeai/services/model_errors.py +17 -0
  137. package/latticeai/services/model_loading.py +28 -25
  138. package/latticeai/services/model_runtime.py +228 -162
  139. package/latticeai/services/p_reinforce.py +22 -3
  140. package/latticeai/services/platform_runtime.py +92 -24
  141. package/latticeai/services/product_readiness.py +19 -17
  142. package/latticeai/services/review_queue.py +76 -11
  143. package/latticeai/services/router_context.py +1 -0
  144. package/latticeai/services/run_executor.py +25 -9
  145. package/latticeai/services/search_service.py +203 -28
  146. package/latticeai/services/setup_detection.py +80 -0
  147. package/latticeai/services/tool_dispatch.py +28 -5
  148. package/latticeai/services/triggers.py +53 -4
  149. package/latticeai/services/upload_service.py +13 -2
  150. package/latticeai/setup/__init__.py +25 -0
  151. package/latticeai/setup/auto_setup.py +857 -0
  152. package/latticeai/setup/wizard.py +1264 -0
  153. package/latticeai/tools/__init__.py +278 -0
  154. package/{tools → latticeai/tools}/commands.py +67 -7
  155. package/{tools → latticeai/tools}/computer.py +1 -1
  156. package/{tools → latticeai/tools}/documents.py +1 -1
  157. package/{tools → latticeai/tools}/filesystem.py +3 -3
  158. package/latticeai/tools/knowledge.py +178 -0
  159. package/{tools → latticeai/tools}/local_files.py +7 -1
  160. package/{tools → latticeai/tools}/network.py +0 -1
  161. package/local_knowledge_api.py +4 -342
  162. package/package.json +22 -2
  163. package/scripts/brain_quality_eval.py +3 -1
  164. package/scripts/bump_version.py +3 -0
  165. package/scripts/capture_release_evidence.mjs +180 -0
  166. package/scripts/check_current_release_docs.mjs +142 -0
  167. package/scripts/check_i18n_literals.mjs +107 -31
  168. package/scripts/check_openapi_drift.mjs +56 -0
  169. package/scripts/export_openapi.py +67 -7
  170. package/scripts/i18n_literal_allowlist.json +22 -24
  171. package/scripts/run_integration_tests.mjs +72 -10
  172. package/scripts/validate_release_artifacts.py +49 -7
  173. package/scripts/wheel_smoke.py +5 -0
  174. package/setup_wizard.py +3 -1304
  175. package/src-tauri/Cargo.lock +1 -1
  176. package/src-tauri/Cargo.toml +1 -1
  177. package/src-tauri/tauri.conf.json +1 -1
  178. package/static/app/asset-manifest.json +11 -11
  179. package/static/app/assets/Act-Bzz0bUyW.js +1 -0
  180. package/static/app/assets/Brain-Dj2J20YA.js +321 -0
  181. package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
  182. package/static/app/assets/Library-B03FP1Yx.js +1 -0
  183. package/static/app/assets/System-80lHW0Ux.js +1 -0
  184. package/static/app/assets/index-BiMofBTM.js +17 -0
  185. package/static/app/assets/index-Bmx9rzTc.css +2 -0
  186. package/static/app/assets/primitives-Q1A96_7v.js +1 -0
  187. package/static/app/assets/textarea-D13RtnTo.js +1 -0
  188. package/static/app/index.html +2 -2
  189. package/static/css/tokens.css +4 -2
  190. package/static/sw.js +1 -1
  191. package/tools/__init__.py +21 -269
  192. package/docs/CODE_REVIEW_2026-07-06.md +0 -764
  193. package/latticeai/runtime/app_context_runtime.py +0 -13
  194. package/latticeai/runtime/sso_runtime.py +0 -52
  195. package/latticeai/runtime/tail_wiring.py +0 -21
  196. package/scripts/com.pts.claudecode.discord.plist +0 -31
  197. package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
  198. package/scripts/start-pts-claudecode-discord.sh +0 -51
  199. package/static/app/assets/Act-fZokUnC0.js +0 -1
  200. package/static/app/assets/Brain-DtyuWubr.js +0 -321
  201. package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
  202. package/static/app/assets/Library-C9kyFkSt.js +0 -1
  203. package/static/app/assets/System-VbChmX7r.js +0 -1
  204. package/static/app/assets/index-DCh5AoXt.css +0 -2
  205. package/static/app/assets/index-DPdcPoF0.js +0 -17
  206. package/static/app/assets/primitives-DFeanEV6.js +0 -1
  207. package/static/app/assets/textarea-CD8UNKIy.js +0 -1
  208. package/tools/knowledge.py +0 -95
@@ -5,11 +5,11 @@ import base64
5
5
  import os
6
6
  import socket
7
7
  import tempfile
8
- import time
9
8
  import zipfile
10
9
  import json
11
10
  from pathlib import Path
12
11
 
12
+ from latticeai.core.io_utils import atomic_write_json
13
13
  from latticeai.core.logging_safety import install_sensitive_log_filter, safe_log_text
14
14
  from latticeai.cli.runtime import _load_env_file
15
15
 
@@ -26,7 +26,8 @@ def env_value(primary: str, default: str = "") -> str:
26
26
 
27
27
  TOKEN = env_value("LATTICEAI_TELEGRAM_BOT_TOKEN")
28
28
  API_URL = f"https://api.telegram.org/bot{TOKEN}"
29
- BASE_URL = "http://127.0.0.1:4825"
29
+ SERVER_PORT = int(env_value("LATTICEAI_SERVER_PORT", "4825"))
30
+ BASE_URL = env_value("LATTICEAI_BASE_URL", env_value("LATTICEAI_SERVER_URL", f"http://127.0.0.1:{SERVER_PORT}")).rstrip("/")
30
31
  CHAT_URL = f"{BASE_URL}/chat"
31
32
  AGENT_URL = f"{BASE_URL}/agent"
32
33
  MCP_TOOLS_URL = f"{BASE_URL}/mcp/tools"
@@ -42,8 +43,7 @@ AGENT_WORKSPACE = Path(env_value("LATTICEAI_AGENT_ROOT", "agent_workspace"
42
43
  # Pending plan approvals: context_id → (chat_id, executing_model, reviewing_model)
43
44
  _bot_pending_plans: dict[str, dict] = {}
44
45
  MAX_TELEGRAM_FILE_BYTES = 45 * 1024 * 1024
45
- SERVER_PORT = int(env_value("LATTICEAI_SERVER_PORT", "4825"))
46
- INVITE_CODE = env_value("LATTICEAI_INVITE_CODE", "gemma-lattice-ai")
46
+ INVITE_CODE = env_value("LATTICEAI_INVITE_CODE")
47
47
  PUBLIC_WEB_URL = env_value("LATTICEAI_PUBLIC_URL")
48
48
  DATA_DIR = Path(env_value("LATTICEAI_DATA_DIR", str(Path.home() / ".ltcai")))
49
49
  DATA_DIR.mkdir(parents=True, exist_ok=True)
@@ -55,40 +55,56 @@ logger = logging.getLogger(__name__)
55
55
  # ── Server session auth ───────────────────────────────────────────────────────
56
56
 
57
57
  def _get_server_session() -> str:
58
- """Read the most recent valid admin session from sessions.json (web login)."""
59
- sessions_file = DATA_DIR / "sessions.json"
60
- users_file = DATA_DIR / "users.json"
61
- try:
62
- if not sessions_file.exists():
63
- return ""
64
- sessions = json.loads(sessions_file.read_text())
65
- admin_emails: set[str] = set()
66
- if users_file.exists():
67
- users = json.loads(users_file.read_text())
68
- admin_emails = {e for e, u in users.items() if u.get("role") == "admin" and not u.get("disabled")}
69
- now = time.time()
70
- # Pick the newest non-expired admin session
71
- best_token, best_ts = "", 0.0
72
- for token, entry in sessions.items():
73
- email, created_at = entry[0], float(entry[1])
74
- if admin_emails and email not in admin_emails:
75
- continue
76
- if now - created_at > 7 * 86400:
77
- continue
78
- if created_at > best_ts:
79
- best_token, best_ts = token, created_at
80
- return best_token
81
- except Exception:
82
- return ""
58
+ """Return only the explicitly provisioned bot-to-server token.
59
+
60
+ Web sessions are hashed at rest and belong to interactive users. Scanning
61
+ ``sessions.json`` cannot recover a usable token and would couple the bot to
62
+ whichever administrator happened to be logged in most recently.
63
+ """
64
+ return env_value("LATTICEAI_SERVER_SESSION_TOKEN").strip()
83
65
 
84
66
  def _server_client(**kwargs) -> httpx.AsyncClient:
85
- """httpx client pre-loaded with the web session cookie."""
67
+ """Return a server client authenticated by an explicit bearer capability."""
86
68
  token = _get_server_session()
87
- cookies = {"session_token": token} if token else {}
88
- return httpx.AsyncClient(cookies=cookies, **kwargs)
69
+ if not token:
70
+ raise RuntimeError(
71
+ "LATTICEAI_SERVER_SESSION_TOKEN is required for Telegram server API calls."
72
+ )
73
+ headers = dict(kwargs.pop("headers", {}) or {})
74
+ headers["Authorization"] = f"Bearer {token}"
75
+ return httpx.AsyncClient(headers=headers, **kwargs)
89
76
 
90
77
  # ── Chat ID registry ─────────────────────────────────────────────────────────
91
78
 
79
+ def parse_allowed_chat_ids(raw: str) -> frozenset[int]:
80
+ """Parse a comma-separated Telegram chat-id allowlist."""
81
+ allowed: set[int] = set()
82
+ for value in str(raw or "").split(","):
83
+ value = value.strip()
84
+ if not value:
85
+ continue
86
+ try:
87
+ allowed.add(int(value))
88
+ except ValueError:
89
+ logger.warning(
90
+ "Invalid Telegram chat id ignored in allowlist: %r",
91
+ safe_log_text(value),
92
+ )
93
+ return frozenset(allowed)
94
+
95
+
96
+ def allowed_chat_ids() -> frozenset[int]:
97
+ """Return the configured allowlist; missing configuration denies all."""
98
+ return parse_allowed_chat_ids(env_value("LATTICEAI_TELEGRAM_ALLOWED_CHAT_IDS"))
99
+
100
+
101
+ def is_chat_allowed(chat_id) -> bool:
102
+ try:
103
+ return int(chat_id) in allowed_chat_ids()
104
+ except (TypeError, ValueError):
105
+ return False
106
+
107
+
92
108
  def load_chat_ids():
93
109
  try:
94
110
  if CHAT_IDS_FILE.exists():
@@ -100,19 +116,24 @@ def load_chat_ids():
100
116
 
101
117
  def save_chat_ids(chat_ids):
102
118
  try:
103
- CHAT_IDS_FILE.write_text(
104
- json.dumps({"chat_ids": sorted(chat_ids)}, ensure_ascii=False, indent=2),
105
- encoding="utf-8",
106
- )
119
+ atomic_write_json(CHAT_IDS_FILE, {"chat_ids": sorted(chat_ids)})
107
120
  except Exception as e:
108
121
  logger.error("텔레그램 채팅 목록 저장 실패: %s", safe_log_text(e))
109
122
 
110
123
  def register_chat_id(chat_id):
124
+ if not is_chat_allowed(chat_id):
125
+ logger.warning(
126
+ "허용되지 않은 텔레그램 채팅 등록 차단: %s",
127
+ safe_log_text(chat_id),
128
+ )
129
+ return False
130
+ chat_id = int(chat_id)
111
131
  chat_ids = load_chat_ids()
112
132
  if chat_id not in chat_ids:
113
133
  chat_ids.add(chat_id)
114
134
  save_chat_ids(chat_ids)
115
135
  logger.info("텔레그램 웹 미러링 대상 등록: %s", chat_id)
136
+ return True
116
137
 
117
138
  # ── Telegram API helpers ──────────────────────────────────────────────────────
118
139
 
@@ -200,7 +221,8 @@ def get_lan_ip():
200
221
  def get_web_url():
201
222
  if PUBLIC_WEB_URL:
202
223
  return PUBLIC_WEB_URL.rstrip("/")
203
- return f"http://{get_lan_ip()}:{SERVER_PORT}/?code={INVITE_CODE}"
224
+ base = f"http://{get_lan_ip()}:{SERVER_PORT}/"
225
+ return f"{base}?code={INVITE_CODE}" if INVITE_CODE else base
204
226
 
205
227
  def get_graph_url():
206
228
  if PUBLIC_WEB_URL:
@@ -212,7 +234,10 @@ def get_graph_url():
212
234
  async def broadcast_web_chat(role, text):
213
235
  if not TOKEN:
214
236
  return
215
- chat_ids = load_chat_ids()
237
+ # Old releases registered every sender. Intersect persisted recipients
238
+ # with the current allowlist so stale files cannot keep receiving mirrors.
239
+ allowed = allowed_chat_ids()
240
+ chat_ids = load_chat_ids() & set(allowed)
216
241
  if not chat_ids:
217
242
  return
218
243
  label = "사용자" if role == "user" else "Lattice AI"
@@ -725,6 +750,14 @@ async def handle_plan_callback(client, chat_id, data: str) -> None:
725
750
  if len(parts) != 3:
726
751
  return
727
752
  _, action, context_id = parts
753
+ pending = _bot_pending_plans.get(context_id)
754
+
755
+ # A callback is bound to the chat that received the plan. Even two allowed
756
+ # chats cannot approve or cancel each other's plan by replaying callback
757
+ # data.
758
+ if pending and int(pending.get("chat_id")) != int(chat_id):
759
+ await send_message(client, chat_id, "❌ 다른 채팅의 작업은 처리할 수 없습니다.")
760
+ return
728
761
  pending = _bot_pending_plans.pop(context_id, None)
729
762
 
730
763
  if action == "cancel" or not pending:
@@ -865,10 +898,17 @@ async def handle_command(client, chat_id, command: str, args: str):
865
898
  # ── Callback query handler ────────────────────────────────────────────────────
866
899
 
867
900
  async def handle_callback_query(client, callback_query):
868
- cq_id = callback_query["id"]
869
- chat_id = callback_query["message"]["chat"]["id"]
901
+ cq_id = callback_query.get("id", "")
902
+ chat_id = (
903
+ (callback_query.get("message") or {}).get("chat") or {}
904
+ ).get("id")
870
905
  data = callback_query.get("data", "")
871
906
 
907
+ if not is_chat_allowed(chat_id):
908
+ logger.warning("허용되지 않은 텔레그램 callback 차단: %s", safe_log_text(chat_id))
909
+ await answer_callback(client, cq_id, "허용되지 않은 채팅입니다.")
910
+ return
911
+
872
912
  await answer_callback(client, cq_id)
873
913
 
874
914
  if data == "cmd:status":
@@ -902,6 +942,16 @@ async def run_bot():
902
942
  if not TOKEN:
903
943
  logger.warning("LATTICEAI_TELEGRAM_BOT_TOKEN이 설정되지 않아 텔레그램 봇을 시작하지 않습니다.")
904
944
  return
945
+ if not allowed_chat_ids():
946
+ logger.error(
947
+ "LATTICEAI_TELEGRAM_ALLOWED_CHAT_IDS가 없어 텔레그램 봇을 시작하지 않습니다."
948
+ )
949
+ return
950
+ if not _get_server_session():
951
+ logger.error(
952
+ "LATTICEAI_SERVER_SESSION_TOKEN이 없어 텔레그램 봇을 시작하지 않습니다."
953
+ )
954
+ return
905
955
 
906
956
  logger.info("🚀 비동기 텔레그램 봇 모드 시작!")
907
957
  last_update_id = None
@@ -937,6 +987,12 @@ async def run_bot():
937
987
 
938
988
  msg = update["message"]
939
989
  chat_id = msg["chat"]["id"]
990
+ if not is_chat_allowed(chat_id):
991
+ logger.warning(
992
+ "허용되지 않은 텔레그램 메시지 차단: %s",
993
+ safe_log_text(chat_id),
994
+ )
995
+ continue
940
996
  register_chat_id(chat_id)
941
997
  text = msg.get("text", "")
942
998
  caption = msg.get("caption", "")
@@ -0,0 +1,111 @@
1
+ """Cloud LLM provider catalog — static data split out of the router.
2
+
3
+ Pure config dicts (OpenAI-compatible providers, per-provider model
4
+ catalog, model→source family map). router.py re-exports these, so
5
+ ``from latticeai.models.router import OPENAI_COMPATIBLE_PROVIDERS`` and the
6
+ model_runtime re-export chain are unaffected.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ OPENAI_COMPATIBLE_PROVIDERS = {
12
+ "openai": {
13
+ "env_key": "OPENAI_API_KEY",
14
+ "base_url_env": "OPENAI_BASE_URL",
15
+ "default_model": "gpt-4o-mini",
16
+ },
17
+ "openrouter": {
18
+ "env_key": "OPENROUTER_API_KEY",
19
+ "base_url": "https://openrouter.ai/api/v1",
20
+ "default_model": "openai/gpt-4o-mini",
21
+ },
22
+ "groq": {
23
+ "env_key": "GROQ_API_KEY",
24
+ "base_url": "https://api.groq.com/openai/v1",
25
+ "default_model": "meta-llama/llama-4-scout-17b-16e-instruct",
26
+ },
27
+ "together": {
28
+ "env_key": "TOGETHER_API_KEY",
29
+ "base_url": "https://api.together.xyz/v1",
30
+ "default_model": "Qwen/Qwen3-VL-32B-Instruct",
31
+ },
32
+ "xai": {
33
+ "env_key": "XAI_API_KEY",
34
+ "base_url": "https://api.x.ai/v1",
35
+ "default_model": "grok-beta",
36
+ },
37
+ "ollama": {
38
+ "env_key": "OLLAMA_API_KEY",
39
+ "base_url_env": "OLLAMA_BASE_URL",
40
+ "base_url": "http://localhost:11434/v1",
41
+ "default_model": "hf.co/ggml-org/gemma-4-12B-it-GGUF:Q4_K_M",
42
+ "api_key_fallback": "ollama",
43
+ },
44
+ "vllm": {
45
+ "env_key": "VLLM_API_KEY",
46
+ "base_url_env": "VLLM_BASE_URL",
47
+ "base_url": "http://localhost:8000/v1",
48
+ "default_model": "Qwen/Qwen3-VL-8B-Instruct",
49
+ "api_key_fallback": "vllm",
50
+ },
51
+ "lmstudio": {
52
+ "env_key": "LMSTUDIO_API_KEY",
53
+ "base_url_env": "LMSTUDIO_BASE_URL",
54
+ "base_url": "http://localhost:1234/v1",
55
+ "default_model": "local-model",
56
+ "api_key_fallback": "lmstudio",
57
+ },
58
+ "llamacpp": {
59
+ "env_key": "LLAMACPP_API_KEY",
60
+ "base_url_env": "LLAMACPP_BASE_URL",
61
+ "base_url": "http://localhost:8080/v1",
62
+ "default_model": "llama.cpp-model",
63
+ "api_key_fallback": "llamacpp",
64
+ },
65
+ }
66
+
67
+ PROVIDER_MODEL_CATALOG = {
68
+ "openai": [
69
+ {"id": "gpt-5.5", "name": "GPT-5.5", "family": "GPT"},
70
+ {"id": "gpt-5.4", "name": "GPT-5.4", "family": "GPT"},
71
+ {"id": "gpt-5.4-mini", "name": "GPT-5.4 Mini", "family": "GPT"},
72
+ {"id": "gpt-5.4-nano", "name": "GPT-5.4 Nano", "family": "GPT"},
73
+ {"id": "gpt-4o-mini", "name": "GPT-4o Mini", "family": "GPT"},
74
+ {"id": "gpt-4o", "name": "GPT-4o", "family": "GPT"},
75
+ {"id": "gpt-4.1-mini", "name": "GPT-4.1 Mini", "family": "GPT"},
76
+ {"id": "gpt-4.1", "name": "GPT-4.1", "family": "GPT"},
77
+ ],
78
+ "openrouter": [
79
+ {"id": "openai/gpt-5.5", "name": "GPT-5.5 via OpenRouter", "family": "GPT"},
80
+ {"id": "openai/gpt-4o-mini", "name": "GPT-4o Mini via OpenRouter", "family": "GPT"},
81
+ {"id": "anthropic/claude-opus-4.7", "name": "Claude Opus 4.7 via OpenRouter", "family": "Claude"},
82
+ {"id": "anthropic/claude-sonnet-4.6", "name": "Claude Sonnet 4.6 via OpenRouter", "family": "Claude"},
83
+ {"id": "anthropic/claude-haiku-4.5", "name": "Claude Haiku 4.5 via OpenRouter", "family": "Claude"},
84
+ {"id": "qwen/qwen3-vl-235b-a22b-instruct", "name": "Qwen3-VL 235B A22B via OpenRouter", "family": "Qwen"},
85
+ {"id": "google/gemma-4-12b-it", "name": "Gemma 4 12B via OpenRouter", "family": "Gemma"},
86
+ {"id": "x-ai/grok-2", "name": "Grok 2 via OpenRouter", "family": "Grok"},
87
+ {"id": "meta-llama/llama-4-scout-17b-16e-instruct", "name": "Llama 4 Scout via OpenRouter", "family": "Llama"},
88
+ {"id": "google/gemini-2.5-flash", "name": "Gemini 2.5 Flash via OpenRouter", "family": "Gemini"},
89
+ ],
90
+ "groq": [
91
+ {"id": "meta-llama/llama-4-scout-17b-16e-instruct", "name": "Llama 4 Scout", "family": "Llama"},
92
+ ],
93
+ "together": [
94
+ {"id": "Qwen/Qwen3-VL-32B-Instruct", "name": "Qwen3-VL 32B", "family": "Qwen"},
95
+ {"id": "google/gemma-4-12b-it", "name": "Gemma 4 12B", "family": "Gemma"},
96
+ {"id": "meta-llama/Llama-4-Scout-17B-16E-Instruct", "name": "Llama 4 Scout", "family": "Llama"},
97
+ ],
98
+ "xai": [
99
+ {"id": "grok-beta", "name": "Grok Beta", "family": "Grok"},
100
+ {"id": "grok-vision-beta", "name": "Grok Vision Beta", "family": "Grok"},
101
+ ],
102
+ }
103
+
104
+ MODEL_SOURCE_BY_FAMILY = {
105
+ "GPT": ("미국", "OpenAI"),
106
+ "Claude": ("미국", "Anthropic"),
107
+ "Qwen": ("중국", "Alibaba"),
108
+ "Llama": ("미국", "Meta"),
109
+ "Gemini": ("미국", "Google"),
110
+ "Grok": ("미국", "xAI"),
111
+ }