clouds-coder 2026.9.20__tar.gz → 2026.9.22__tar.gz

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 (41) hide show
  1. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/Clouds_Coder.py +2336 -490
  2. {clouds_coder-2026.9.20/clouds_coder.egg-info → clouds_coder-2026.9.22}/PKG-INFO +1 -1
  3. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22/clouds_coder.egg-info}/PKG-INFO +1 -1
  4. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/pyproject.toml +1 -1
  5. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_liquid_kernel.py +8 -14
  6. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/LICENSE +0 -0
  7. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/README.md +0 -0
  8. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/clouds_coder.egg-info/SOURCES.txt +0 -0
  9. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/clouds_coder.egg-info/dependency_links.txt +0 -0
  10. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/clouds_coder.egg-info/entry_points.txt +0 -0
  11. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/clouds_coder.egg-info/requires.txt +0 -0
  12. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/clouds_coder.egg-info/top_level.txt +0 -0
  13. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/setup.cfg +0 -0
  14. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_admin_i18n.py +0 -0
  15. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_authoritative_prompt_delivery.py +0 -0
  16. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_collaboration.py +0 -0
  17. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_context_auto_compact.py +0 -0
  18. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_context_memory_recall.py +0 -0
  19. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_dynamic_agent_loop.py +0 -0
  20. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_http_incomplete_read.py +0 -0
  21. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_ide_v2.py +0 -0
  22. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_kimi_tool_history_compat.py +0 -0
  23. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_mcp_workspace_trust.py +0 -0
  24. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_model_configuration.py +0 -0
  25. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_plan_structure.py +0 -0
  26. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_plan_todo_progression.py +0 -0
  27. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_process_manager.py +0 -0
  28. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_rag_admin_auth.py +0 -0
  29. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_requirements_coverage.py +0 -0
  30. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_session_performance.py +0 -0
  31. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_shell_timeout_modes.py +0 -0
  32. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_single_no_plan_todo_bootstrap.py +0 -0
  33. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_skill_selection.py +0 -0
  34. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_skills_studio_v2.py +0 -0
  35. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_smoke.py +0 -0
  36. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_step_skill_runtime.py +0 -0
  37. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_sync_routing_scope.py +0 -0
  38. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_thinking_only_recovery.py +0 -0
  39. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_todo_tree_revision_guard.py +0 -0
  40. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_web_search_local_graph.py +0 -0
  41. {clouds_coder-2026.9.20 → clouds_coder-2026.9.22}/tests/test_web_search_public_discovery.py +0 -0
@@ -93,15 +93,25 @@ def _connect_sqlite(
93
93
  ) -> sqlite3.Connection:
94
94
  # sqlite3.Connection's own context manager does not close the connection.
95
95
  # Polling/request handlers must not rely on cyclic GC to release DB/WAL FDs.
96
- conn = sqlite3.connect(
97
- database, timeout=timeout, isolation_level=isolation_level,
98
- factory=_ClosingSQLiteConnection,
99
- )
96
+ operation = "connect"
97
+ try:
98
+ conn = sqlite3.connect(
99
+ database, timeout=timeout, isolation_level=isolation_level,
100
+ factory=_ClosingSQLiteConnection,
101
+ )
102
+ except sqlite3.Error as exc:
103
+ exc.sqlite_database_path = database
104
+ exc.sqlite_operation = operation
105
+ raise
100
106
  try:
101
107
  conn.row_factory = row_factory
102
108
  for statement in pragmas:
109
+ operation = statement
103
110
  conn.execute(statement)
104
- except BaseException:
111
+ except BaseException as exc:
112
+ if isinstance(exc, sqlite3.Error):
113
+ exc.sqlite_database_path = database
114
+ exc.sqlite_operation = operation
105
115
  conn.close()
106
116
  raise
107
117
  return conn
@@ -113,256 +123,315 @@ except Exception:
113
123
  _AESGCM = None
114
124
 
115
125
  _EMBEDDED_LIQUID_KERNEL_PACKAGE_B64 = (
116
- "UEsDBBQAAAAIAJFAK13NzUY/oAAAAHYBAAALAAAAX19pbml0X18ucHl1jrsKwkAQRfv9iiWVgvgHVppCjEZ8NSLDYjZhYHZHh10Lv96QR2OSKe+5dzilsNPL"
126
+ "UEsDBBQAAAAIAAAAIVzNzUY/oAAAAHYBAAALAAAAX19pbml0X18ucHl1jrsKwkAQRfv9iiWVgvgHVppCjEZ8NSLDYjZhYHZHh10Lv96QR2OSKe+5dzilsNPL"
117
127
  "J/sgTBrdiyXomdL1pbc8u162+QH2+SY9L5oww3fEYmfFW1q3qyMZb4c0FWEZxqfoA7quX9jSRApgP0wxIHuoTUqsWupZnCH82hE+VwrAEAHolb439eRPOGm/"
118
- "JFPKY7yRHgOddo+mxHs+rV43HuoHUEsDBBQAAAAIAJFAK13P3W5mE04AAPBjAQAKAAAAY29udHJvbC5wedV9a3fbxrXod/0KFGdlmXBpWvIjcdkwPYqtJD7H"
119
- "trJspef0KlpYEAlaiEmCJUjbqsr/fmc/5j0DgpLT25uuWgQwzz179uzZz+mqnid5Pt2sN6syz5NqvqxX66RYLOp1sa7qRXNwIN81a/nzqmiuZtWlepwXY/mb"
120
- "/oiPg826msm3vzX1Qv6uG/lrVSwm9Vw+NVdmjebvs2pdPlaPm8vlqh6XjarcXKuf63K+nFazUj1frcpiUi3eqxfVXH9cFePyshh/kC82m2pyMAU4jOvZrBzj"
121
- "rCUgJuXfNyV9nRTrYjwrmqbUX+UrVaKErozP+NzHAUzK2bqgn/+oF9zoslgDJGWNn8UjfVhfL8UE5PvjxXU/eV7MZsXljGtCG9ViWssi/0c8vxTPBwcHJ389"
122
- "ffXL2cvTN/nr0xcn75JR0ktPp9O0n6Rnm4VoFn9dVYsP/Pv4/fuVAG31sUyzg+enb87enr7K3z3/6eT1cf7Xk7fvRFOikaOD/z55++bkVY4ljp+fWd++P353"
123
- "8urlm5OcC707/eXt8xPx6d69e9MOWNbWNn97c/waGkyfz+rNpEme15NylXxfNOWsWpTpAcFlcFWsFmIyqpPpulzl67qe5WKOm9m66SeX5bQWA1nVm8VEP20W"
124
- "3ILAtPlynS/rWTW+Vu1s3s/LhXiLH7kkNmuXE4goOxSzOpiUU4FEzXhVXZa9bHiQiP9WpYDDIrnBB/gvXRTzMh0mxjz7+uO4XgDSrvOP5aoRwNIFXWCZlYpl"
125
- "cVmJPVSVjahwnuKAYLFpAg9o1PCCAfbgqq4/NOkFNbI9EOt2oFf17FSgxM+nr14+/9veS4tAMOHSw3/7yf1+sqpn5SgVw4BZlp/XozcCj204zapmTTWSepWc"
126
- "X2TO0H5+e/r657M7Dc5e3B796Tq+Zr3iGjC+NHXH99OxWKx37243Mo2cvfgIbrZOaUDsLuX9zdFTm+T+zjXhouFVYRT94eUrpECE7OmHUuDabLC8FjgZphiE"
127
- "fqmxs5zSPiZyFWvbOpVCOMLVGP2dCvai9Q8ExJgwvjp54U5OrOY/ykVTrns3xgz73iz6gUFus4Mffnn1Kj/+8eTNmdjRb09Czd6PdN63xi/aevn69S9nx9+/"
128
- "OsklHX8uZn76RjSOBwHNudisrwS+V2PEtVQCEOhLsy4X41K+Kj8Ws41dSEygNl8Um0m1lg9N9Z4OGH4UJ/xl/Vk8ZgLj8KhMXlV/FyfufyOYTlaretV7u1nA"
129
- "qYgPjGaAnHleLap1nvcEgZ8CMk7KIWy2fjIX8y3ey6dG7JhNM0yqxVrM8MnhYV9UXxfVTLybVON18s8EEFh8M/AY/ms2Ysa9bKA64nYzXUL0PICORWXY5vgT"
130
- "NvkMJ5HTYucljDx1qtGwREUxrh4/iKpifE5BHqwoCaPtyUdR9mab8UbNF/WnXpY8+C6Zzupibe1FgN0A/ump0sBv9WDpBIwE74AVxfitalBmMNnMlw2V7CcC"
131
- "14AaFc24qkY/FLNGvGsEWRLTvG5GZyso05TLYlWs61Uz6qV9wOhhmgHAp4WgBSPRiRoEMGE0EuByhsjc9JOp4GKA/dIDE39pYOvVtV4cc5RiypMGWxlgo0CU"
132
- "egJNa2DyRulmPX3wLM0IrOXncSko8Qn+EWjqtSgHIIcpJjOvxv5Al8U19KvHCdhDreFIBBzEFhrMP0wqQf/xQQKp/Cy2UV5/wEcaFvCoYoWx5qdqfZXDkd+b"
133
- "poMbfAVP28EN8KID+OeJQMur8vN2sJ4vU93C4NNKcMU0f2P9eKjhFawWEzG00SN3LTMo7oDQX4a6GYyv5vWkB933k8P6a4m/cUAvgSPmyqtyORMMN1eHuSr8"
134
- "aK6KR0+/zi+v16U4dopPwwR/BpGVbxwDqgOlET6T6n0pDiJjl6zmxaz6Ryl3JlwLGrkT6svfBHuP7cNOO2fqsbqgrqppUjXVQmxVQQDlngCE9lDoxjrHsORW"
135
- "NiEO8EAz0J/ZTlE1ZYASptVCVNCE5VKc47MSdhm9SIrVupoKXjCZb5q1OO7Fmm5W4zIBjACKUQCfVSbzQmINAmDozBfOYxqwYBfEYD7lS0R5+MUNVgua10Dg"
136
- "27zpZebqrq+YHMqazPcIkreqlgJz5aKnv/4KY39okEaGkKyFyC9AtVo3sCt6UBZbGwxSGANsRdyVGew34jVEGfiEe1W1uw9UoWYIpsDuFGLlBBuWcBUCJ1bQ"
137
- "c0CYnsPLCwMQDDhmAXmuBqIQZix4RXajAg9Wcf/zStzQ6L7mDrxqEv6a6P4yc/9gp3KXGHsjh40ldkgQTaI7RW4DIpf2RiYwDJC0CAyQtBkxjbBMYxgQpHJC"
138
- "nUtEy7bOMGkP4EBbhukSDHtQRN2jE88yb7zhQSDh7T6I9NfFr4t08FtdLQSp/49kNBoR0Lbw89fFDYFim3aHjqaexbTMQQgQoG56HMUnxlAshMh53FTFw3dX"
139
- "xeL9VVHpTRv45p8GUsiABNiliuJdtxPY7ebgAOQUgkc/eXdy9s6ELPy60LcHEGIMzXtzM74qJ5sZ3J3TGiQc5q25nk3qT4v8SgAUrsCH1u14MalAPJPPqrng"
140
- "Xu2v8+IzHRyB92MYdTnJQeQQ/F40wffr+oM4nJ0PwLTVm3XelGKXT7xqmzWIe3JxpItdjnNcCPibkxR8fJ0TRw7f6cQ3WhB87Xwzz98LqgaND7j5LbPnLA2K"
141
- "gvRTWX6YXbdC9ZtHrWA9ioD16Q6wAhcfBuzjr2OQffIsPzxsB+/jrw93QBjJLcgBJy1wRi4vCuajwVMHzFLUFgV0+bFcXeeP80lx3bSC+8mzVnA/ioD76NEO"
142
- "eB89igL8m0cxgItKOyH+zaMvAvEdmP3YxWxDohkF+kRcs9qR+9GTVmg/jkD78eFu7I5BW8A0Bu5HT3aD++hJ/mQXwKeb2SwX19zFWjAXqzvQk6cG1OHU/un4"
143
- "7Yv8+19e/Hhylr96+frlmSH4iUIvCLkdULMh9rX5VkHr8aEBrQCkHh3lSAy2SkiLN9i8/FjPNnBsAec1rd73FBfEdz+BV+VaXtbNg+scz6eLLCLjXZUfKxbf"
144
- "WlRRsB2wKqf28WUgKvU4WNbLnn6dOWgAnAA0Yx+tRiFBYsSCi7P+/fsSkdshYvQdJRn5CtYJtCjNldgReHJYFITKiikKohUq+6xtU5nTcT6ak7oSV+hatM9C"
145
- "73wimIkrh8TJMsUYNEMSt9NACbxi5IB/UCpUolxMgt83TbnKm3G9LFGMfl8KyGmVgMaIwcW+i01WorQENhUgudv6b5vJ+zL28RI+ri0CBv+JG7yE4rn4fWF9"
146
- "BEZSvAT+sedtur652/qhXdY391bf2lJ9fxdpFnDbSnJ4sN6XizjpkVWs1xdxWqS6MF+b5a+K1QTWaVXmn8rq/RUyfQOLVDf1dO0X+ebQbWVV0uEiFl6xj4PD"
147
- "p/ZZUayuvZVLBRq+J33M037ySPz/6PDQGKQ1sXE9X85KcQOA4uJUSJ6K/0dLy911Ls4s4BL8ks0Gd0k+WdXLyLhpcyMJqBbjVSmwoK3k8k9P81kB8uLrUHnz"
148
- "JNtKrRIRWy2n8cgtCoLoLkM6iM0KRGtBUa5NmeESjtccJUqFW4oj1hGvWBqTlOJwk3IQvnVxPe4TrkTxYyEzKg42S9hmPT0E+jrXomMuKDZ0j2h+n2g+Xbzw"
149
- "l5KfrKv1DKS5LETAVlh64Oh29xApyU6xNSk+Ev32E7oGiL/Mp/ZhSJZO2Dr4rDMPWruQ3z9W9abJjUmbkLzZZvvPHspKEiXavBTnhWwzSwR1I9j8YWT3TsLH"
150
- "DZ7S0fWTheTa0QKp1+c0Trh9mk2e69MWPhnXcGgK56cKSBTZXIotITZyLk9vQ2pE2EIV1eHeBxGSK1Sb1Z9AW4FNMgjcBoPg7t6qLJmr3dDTUj+mr6q1C1Ok"
151
- "Zy0TLEsP0DUwcdF7CBwjb0b6aMFtqoCrGSAsYCyXHpnccc50QMTizSIMCTElt03efzckZxAngWh0hio9vke4tzh1fd7usUnNxVLgkZu1hs1K/QoaBr32E7NP"
152
- "3LfcqU08BIBJeKLl+h7IcGIanA6/eCF3n1oJp0AfGcosc1sIcpS4q4rPPXGoHPUTcYb1jsRdgnRbbg/BBvrIkiI24w+/3yB3uk+/wQaw32ey32eyX8XHqp0j"
153
- "Mc3hcC+83WKjt8sQ61l5DfFMaBqPniT34faEykZ3NDBYC0Jh3tppUnD82JoaW7iW4GOyeA/Mmcst6bXF3/vMuRN5op+tG9NrX21PugGIY65cF2Cftdf284dl"
154
- "v1FbEXqBsapeAnM37hzR+RtlQuTZb1RdU6JNqhLRBq07gnG/6bsXGkProxgr2Z9ooE/3HU+546q/BGECe5HM1taoFs/p14VFm+BiA99giiD71nJqGD28USqq"
155
- "Bvr1yl2cD58K5hqNU2CUGpT+pcyDpV+kfXXsa5zXnP052lSzWS5nlTgY6eJnnb/MpwbO28hByZfHPjACDg/slslCHDENiUqYInlBEhz9IWICXoyQ94d18SVB"
156
- "vh5RcmakmSAzCeNWa6oX/i4WeY0MINpS2GDS2Gi1yERJtUKFJV4ZdA6a1KMXsFD9mQSNQYXHIP40+ET3VsvLH731Gs3a11t5xoYvv8wRGxdc/7ixr787Dhur"
157
- "sMn42o0QtOiMDPeTySM0NDg8Kg+tw8G7ikMncBk3uDnvLk5lvjl02vEu41Tu8CkdzHgRN8iWvJq37gsuE9wXdBhTAa2a6nK7736z73arD9/obfZG3MKfysWh"
158
- "QTMb7VftI9SYtRE/MlMaFxEKdOssWLm1u1bJAnf61GPgzD5bWuijbIJ7fnSoejbWV8lqJfYfObyQKiAWGVs6yiy5ryh4cHDwn8pAvEe2fGwQRIZwxIEcsxKf"
159
- "CCPzVmjZxqZGeC0KvJeWSoYYApXauow0jIPnkPXd2/K9OJalfjdgcreq6zV1YlrNgc0afBGgQfMM+A02H009+yjv7VbBrjZSnrrZMkBSzYEV0jemFV1c4WxZ"
160
- "I6kxSbuJJudp6Ck9TFL1NXXMAKUwNVxNf3bqlZ+X5aoCo8pgPf3ZqQc2aptluDP+pmtI+wHk6QKT7Afn0A+OsO/3n7kgXV/ts6jBhbUWl2wfnHXdvbbh9Z1c"
161
- "5tJIyQTaitF9QL4lDrgFqKqPZbAifRqADYlTic1dwaguWFOtQm6UdNqY1eMPop5yWhm8fSXeuPvIqC87ycHmz2zYrUObeXLpvhc8xloAoljml+w8ARZ0igZ4"
162
- "zUIllLWiMc3QZESDUBggGljMXhQDmF5iywOwir8qP/fCrbaYfko2+k64Q6AVKLkhx6Te40dZ+3Kb5pjiHVgk9qJmlR2om9u+ZW+5J6VjwIqmjKUd14tFOV7r"
163
- "FWU3q8Fz+mA1CYUB17iIWVduMXJnEofd6LExSigptsCnHDC/RvZLNvK2/uSOEEobQ5Q4q4aozW7hP7ASZOyWA8qSosFWbGjgKMrP5XizRv+b5brnrX+apt67"
164
- "n98e//j6OPlNnKiLYoby5NH/HL/6s1fw+duT47OTBK3tk5c/JG9Oz5KT/3357uwdSlLnRQ7SgR4f3cnLN2cnP568xWJvfnn1KvNbfPnm3cnbMyh5GmoiS96d"
165
- "vDp5fpYcJv/z08nbE6PHHn85Sn54e/rarJztNXDuqfFBZXAnydnJ/54JOL18ffz2b8l/n/ytb3Mp9F3Os8+27/bLYPsGF+MUnxeLaiouZWjI53yjy82sft+l"
166
- "C333SAQUXqnSyYuTH45/eSVg29dXj2iRYNPz6nO5q21gREH0XaztIn2+5flfdGWv0/2WdrWJLav4klcTf1VRdNxhLVkGnH+oFpMuayD48c38Mo4wWm0d/K4g"
167
- "cu9euH3S8IRQxeB0/K+63ZvtPViQZd0IArCrYBik6Bq1sy7ejOKzi2JLsE/SYwVKq/vm74pcOYrNIyhGInWBZJIKGniWHP9ydvryjWj99cmbs76JjWoKXVGL"
168
- "PSFCa680hAHyQsMLfIitwB3BNSmXs/p6HoeXLhDcmeF9o2YY/hwmuaENjYKMXGyVMSgT3YOrL46VVTVuQkAO78cYGscw9o7QXdTrSuxwcX8JAxdF3h6KTQTw"
169
- "RC3vPYvtnLd7gFiMY1F/mpWT9yEYGMva44HpocjOZXdZF8j88vMLgIzBAwi+4Uwe3qOjP+9kg8zbAPCkLA6Qh/CQxWKmpbnpqzMvxoNF+cnjafvsmCbb8azu"
170
- "+46fj+Pjo8YkG+hZ7LmGvikt6RuOK2RIYr10BSf6y339c65cD516zsuws41ls0KFHcsVlE+Lq5glTlRmgGIRtef3kWODcxvncKyoy/Mv17jHYunAwgpfuEZF"
171
- "GnyiiPHkmimRlSP8CbagKrvTU6RDlCAnSMOoyLyJOm41EqTnKVm7obFG1P/jwK4COFuAZzbJ8vG2gfsAPnt+D/BSI2fgcq1vW+Zu0TcZkAD0u11q8GYNfj4j"
172
- "637TS5n/l2QJbwGSmU9O374Q9Pv7v5mUGNUyyZG4O0/L9fiqXpiiO4ap7M2/OLOmz5WcRG7+8j/L19Kt209uDKQEBYrs/vwQNAzaqA720TaLiRLcmrZ5JCy3"
173
- "NPMK+qrb5c0bySjukmRXkoswSqbsqvXg8PDw6IFEiAc3RrPnw2cXW5v8rosVqf9C0sqHsvlAlXaZHFpzZ561qOt2ZPkb+evY49E9JGdOUwTCO7hF/oHElIm3"
174
- "2lk+NXcA2Q8cWP67KPGRFHyUygWI1hZF5PYNlEHIjPDfvnNeRrFcQSuV0yQ5oj5N7cpGhec/Hb/58eTV6Y+D+SS1AJ3+hwp7Ah5mLxfVuipmYqXxKCgnCbmn"
175
- "P2A/QeD/i3WF4UCuk1lxXa4Gv8IQ2hfKIjD+EpjCCiU2kCtmnx58beybS2Td6PUd3uC4zTtxlvz1+NUvJ+96f+lb/8sCy6QGAWgipbdp30IQlxMR5W4DRdEq"
176
- "H0r8N+uMGO3kT0+hjeoxxZMEQZ1B3CxA3Th9bH4DPwJN0276gSEZHWqqhsZ1UJ10YXLUpl2DaxIiObIInYyLpT3P61YYGaOSOlxHxxsoYWl5EcoeJLmryElu"
177
- "HDwmCAn82bka8oXBw8L04JKldg6x2CbnCjZ5AqjSrgfNrWSUi0c4CrCqOdduuYZlqPT6ZAiD2aUxMjkdQ16OrYcNwKwBWKZexiIKtleNBux2Lm4hsf10Bc7W"
178
- "cgZgRjorF3IOTZZ8y6P8Y3I0jJ0VzaBYLkuIfsNWwj6bUH+KcU+OGNNmokjkKm9RfxErI/voZ1Emyl8S0b3gSgArYSCIc+Z+sbd0oxFGtF43Nib25U1WBkJh"
179
- "Owh+4SOq3PMBLNUIJC0mAjulxdCLiniBDtztAW1aG8LolmR+MtKKoctvJTBSSmGj7qE0hTRNAqgk6P8zD415RnoYwBlx09+OksNbTOlyM/6AbBxZGFkBLKbp"
180
- "Da/d9p83et3EgxrBNvWc0s3rMPCN/eTo6yz5CuxMPK2KmoiYGY/kWzUjRLvw6DXpY74zQJkQs0LmC/vt95adeH/35jPoWxbZgbyoop+OgSKkaauolU8hiJYR"
181
- "bUFSBWhRfnty+CTzAmMEmXa59UOIYnDETtCcEN9onsnqosq7RpUlZ0F1jW07mnUb4ju8QPkNcjurMmd0U4X65nVYsU9ZR/ha6nBsL2fT21BYCz2yqkl0uSeH"
182
- "f/Jh513a5ZNXMpd3QAUspA4kJnDZhWCZkFmYNMy0Ohn6d894KBbzTrYqZ4XBhoHaAjhSuoeKO5rdTfyyJtuRZ4/ZrIsO5jClMQPjoKwXEwOobhiDVBODqsEh"
183
- "xgQCuxEFZtyGIxifRcc/cdHDvt7BjVwNrcWcIDTPblFO/jBi+YO5YATo3xUChBjRXWJHrpHLhfsEJ+IKfuKCDjlDpGWm9O8ihH53nhwNYNf0YCMOHR9ZFelH"
184
- "o7ERjOegdXWN2oQm9Dr7V8+esXbH7PnEt8/jXuDA6euxO9Jd+MZyoPDszA94lxfvrNtMs3avMmhsKe8ryJDV02lTqleHgUtpd95hXa/FXX3E/i4BDuL56S9v"
185
- "znr3M5uRsMSdnnRQzA8dEsh5tP5EzgbAplSLXeIQWwDbQQCiRR7aJsGwPegb5gQRoUgHKe+Lk3fPWdT7l+T0hx9AEfSXkMyEjVyBeX4qmWcyiM+yvmSt0QQW"
186
- "FzHzJB0E12I262UX0Vs72DIDkDE0plhA8Yh/tZhmGLqcGBdy7XwEURJjvKmNVWo/OcxZz9Oq7cu7krfwLRlYiRsW72ojpKEnVniJXi6WF0w3zOQRmU06g4pi"
187
- "kDXaLssOdELyKMiOGWEcxUdiTy0JIJC/my0zqfDDbswYNbZnPNsFaYc1WnHDL/y1Nopjabv6uXFYePgsimqMpCoxTAwKikLjsjt0Wm/aEN2NzrYfyocvH7L8"
188
- "oMst5PfnrGEUDXIrGBINvWtA+2DZPdvDyHx2nJptXVkOaefNn3QdLSyjNRIcrkG0JtV02o1UycjxogIE2DfEc0Du5Go6RNBbUzp8mOmnQiwCsY/94LWQY0jL"
189
- "tbSQleojaLkPd6UoorRSsxl9y60V8rwJlYsiw/hqs/gQlzeaCyHxRbRrTitL/pnAO2Ow7hWWijMIzbo4SOKVTDe+5axaYwgVRwCIXXAzRnd7tkJTFjR+DaJN"
190
- "xo7BRiA3+MEhenl0nwbtH/U4ioByS/AVMLTRNL3htRWjuvdwUn58uNjMZve2Dyl0YYB7WNeyJmNWS1mYnhjAfOTq8DJf2P97WCwAsCDQjgrMSLC1zArUxiWX"
191
- "AbFoSpwmPWM2Cy13NQ1LTMMQgWU2fcaAJtqYJFEsoKwLzF4TM6+RmyoW5dYMYIETQNFNN7V1d8anAe/Ixbj8Arw3CPIPWpTmN7Kv4eGTyfbBjcynMQBVm4yy"
192
- "Mdisx2j/P8UQ2Pe++ttX868mX/301euv3t3LoBaCwVGzg4UbmVEEfXseSptYX9vONfdStWs1O4ofOunae3KIAUW70UqbErejpj24m0J7hyBpv0PFum/Ww+r0"
193
- "aarirgxvCKQuTQgq1fUcbJ2pAkkHbbpZOq5KR12DMiAHOvxcSc75Ok5WioO03bNjP5sNIzI2D7PPTWR3dHX412nq97m2xvT3u3X4jIiGg13al5joa/H95UyV"
194
- "czJRVw6WIMaOiqDgV5hO/CvOUH7eofaPaegvxcGyyEk7FeAM+1JTI4UlT+90raXb9A7Fn7SZypUGkP3e2tTpoSKePv0gYFImewEtr2mxle9S/IEITnHnQbOC"
195
- "qNyNms4F4JerGr3G0T5kUa+vylXiqHmqJilmwPBjnIzn0mvckrzZml0TFt7Id6jfq6k3KSMqUlj52LKWtvn4KOH7c56KI9fNMaAqLVCkQQjtqYxNwwup9jbj"
196
- "3Bmf3Yn0E6V6HVKUB3oCoxtroOKz9dxHl/qVsv8UI9tuDzpa0+DA70xMUzamVnIcEKARjRzdo9nfC8l2PBhkt7atMn0FbPAoEu7Y/SsibprxW1b7pkBRG+Hv"
197
- "ZWTlrJS/6B6W9N3FvxG8ACwq/rOLhOJ6GtrpS0hmYRFQyNZUFo1pPdNi99S2fZSD0p7GTm22E7ewQwoYJxSuA6mSsNYfZExXQdIu8d6rIviadyc1t+3dbS9c"
198
- "MuNWtnd3sIkg0bG2tnP7cPa59fXG30mBiQfMSS0ju8B3iE8N/vBI6eCmDFRQQg2F2VYH4GsOiMh2zPSAM5enHi8Q7AJF2ux+tyFuuDsVEyijBuirprrTOIrM"
199
- "ncPkPUKXHL954RJDNItSkMmy9q6DKlE5HnBYjI6FXOdGfzHIl3gwPd3E+Gi8njthbOTB0fR2rqCkYH3dU+a35RnXW5tjeAc4mRL9KLhMMDFcrAGM/hKbP87M"
200
- "Kts6u7DsRlMn+72mVIGPO/duakwx97alU7TrjnQGyLszKB5iXdyX1kLd1Uzvix01yLh2OBBCPDl82m2Bl3wLquF9+Xe5zQ2dOQbmk+8TwdBDu18x345Cm1LK"
201
- "dI0MdjY7L/koB/SKXd/j0Gq37g5wyDEz79+Dby1W4ysxsIkk6Oo9jvceEkeu+O13+7Oz7X1TH6YaWxEki4NGsuP226XbGDmUPd6z+eIRmCYEqOPOU27vAR50"
202
- "Puj0UH/P062nDy5vGjFmnIICKYt9M0TRQ68Vl6hQ8YiPGKV/Hozr5fV6VZa2Bq7PdQP+ACbf234HdW5JFDQeX/VlWkvzpmloAZj7YzpPcro8YKr/5W4gtmQh"
203
- "bmPcldJ7zCGshz0RSeztt1lUDOGJIswbGYFhZJyz82KxKWaSICuGOguw97dpSBJ3OZlAB8FZOx4InaltxH45zKjd1b3AZ7di/gbOknZyOegAl+iRLMGcbxbF"
204
- "x6LC/OSYKUIcwCx15AP6SoBxUUu1seYirBM4yiM5SPkvO4n7kbufkm0otpJ+mPe53/kUb7uZ2c4p/98d1btuLq4A7vRtYEDSAeguZ0boftFl6ens+E8YZzWe"
205
- "l+ureqKjFyz/9JTjIrMNA6oYLtzsvqb3HJUOCX4OB9r1o15NyhWGyWW7B46UKZUYFBeXTFUo6q4KoWzKviblZ7DhqRY9cL/iRsXokiPLEND+el+M5E9PsVTm"
206
- "g5wDrFPhc+zCdDWpL5tyBacgO8JQ9Jz9ojsY4Ro4uOkQg+nq15PNCjNZy/Q1QxqWLoAiBaolAOAkv+LLByXuDmUjiUZ2+H/qdWUjOwNaSQa/gAzwtrJS+4iy"
207
- "Lo3coDVx0/sG4rwb0jct0bwTBL6AMPNWIR12e0H5Jpue6MaVx2dtYR3YJ0oyfLZVN9zl1RkYY/i6whQtBEjvYBuBqn5NrUR6oaw/7TErTzouPWjKNQf71ori"
208
- "m9SMq3zYV/GR+Ql3t3yQtACDK19sQ92dG+1dsLULfZGpGuRXQ5xhWrSYTcmhhBqS36xmsBDEAuevWXiIPKlQs/wp3Cp+dI2NJUzAno4otm9rbPaggdgXQHTF"
209
- "TlBD4GUFZyW2lmUX5w+e/OlPw4twt9Jz1zm1XLKtAoqHIaIHhXbC8skqG+QaO3MpJso6Auj49mRLBaoqhca2VDXgDJvrzSNRHyBvED/TDlfH1QtXMxQUZjXd"
210
- "27jeLKTDqjeGONYHBmC25A2rS0tTb1jghWudD8b7/Qg/v+HRQFgi+hU4DXPefmCWJmOMh8AS2MMP3RkEwOS2HgFVsHUHDoGxEwXYNXSfTnQZudN2ZOChtneP"
211
- "WzDIymQNmOXIwB3yE0JDt6XIMKMtEbcHx7PJ/TmEzvpmSdbjycd0CgGDVZaJDKg/C/m/GyVP0VbGxX71wRY9+Aj8bQDtHvDq0dA7ZwawIxivfIz7zkeUPwa6"
212
- "6pQXwO3MF7HYy/1dIJYlwMdGr++cWvcTSCQQHOU+mQQiUlMzNDK4VVcNLrKjI/PWzNRmyXeOespbU1NZFqnjLBemuLK6oiWL9mTVchc62heAUWfftd5He7Lr"
213
- "WO8jUeD0JnKZbBYTjuIyRky3Mocbu7yOGVtSpwDLbnPWFBhbG+zODZECXCI0QgBU1IMtd5I/t5YVcqlP6oCGjbbQ08yugrwdHsoeZ2cRAcpk0rcymRCDp+rC"
214
- "LUx/3V74HfEPyMBkNGOXy6W8gQoP8BFtYsuM8jrBRKuFbAzpqt7hXkoVmJ8hpMChxufpZ2TpmxlZDJc2KyPL7TqhZC59M5mL5Sr59021EpghWSdvbOcGwC4U"
215
- "bBiA32KsHK9O5gJMdSMnYo1ujy6wvNd8OSuWjdm65OV7xG2rI8dKRqON/IzYLIJVePz1IXD6bVyhOP8cyAGxt4dhlsE3njbEnKYoDfMkfEOBViBvAItEJTmR"
216
- "hgDaAGTvm7NHKGQf7VRCK7OHalT2/XtRfl7nkl7QrMxlhivrhcs7iKU5V7QE7lG6kWBRYwUvwjerPWTyph2VEsbTD+eivp+6pqsI2tANd73dafDogHCxy92t"
217
- "kKGYfAReE5FBG9Oa3cZwZHtw604htmy12JTtGBi4RinBLkViDgarUjGOpe8TRTq2rWdYwjo8iISMi1podMcMMzrzTm1eaqdZ0IGmGTV4iqyCV1OUKnmaIj8Z"
218
- "GouDoNEZN8ZeDaoxfubGMmV3FHbI9wWALfLGiE+IdvkwnTvigQi6+rpb1u/mMnAYGZZRYpCiWOAKaYEk/tDBQqUMbf6FtDSKyQks+6cuwvrbuScqKEobNBio"
219
- "filn4oZHBlinQ3NyxiLIybmekJYxQztcfIY9GkoxYHzHDKpRWCve48YJuoxniBa0szOQI78lSdlpnvelva1O3yZvT35+dfz8hMz4jaD0O8O8952Q8b5hfnZX"
220
- "omFFT+voxRRTnJptGatGPgb5aiOjFZLwg3Rn/cTMGhKLl0BOmCNIrV3fwnnF0k19ofNiD88NzLhCc8BcKtIvw5x530uG0jfSl3wBPw3un4Evs9qL9fv7ptwg"
221
- "Q4FGP8aQsr7JghHHRrWzDu4alFEO8mmornVXNzIQvDcaDwwOI8gDhIAxxlC3Ho5SnxoJCWoGElqe20aGxH5y//60KmegFz5eXAdSTxUzyEQ9QWcoz+oO87/b"
222
- "2WVSYs91FpkUjQZUYphUKqUkfyVt+1IjnEIDgexUABhW0+GZmpqsqSFHtEwLxExUGmWqq3a7HTEB0zMoowACRNgxWVB1yJTGilgGSpDbRubRZrb0dKS2Z5re"
223
- "iOa2YgpZID20LEVYyIl9aQQD8b6BfdtLCZwZXv1CCaj7RF7wkKE2DsLdEHLcmSxMPcvOm3v9exRuwABBtmVWibrFVZXmGAqBaSsFcFfRzb7MemMEDrAxN0gc"
224
- "u8/NsIsOso1G9hxkBHUyIB1RSCUAMiJStSQfsJP1qHzK+I5t2dSoPIM28n1fi72IIcIId25Sgh6I0yRdAhCKZ6TDED8CoYiBI/CXaTTKGS6s576TnwIsf5wM"
225
- "hBoyIye3SU+OsDXK6x2OHpmPiWeLkzTTI9kJkYwUSDvctltPGQKlDGCKnUFoORtqRlfBo8TG/SYXu1lxfBRDRWKTiiXnhJt7dHi36HJfIBCcHGL/LvD39hPR"
226
- "C9n2d38JbDCO9hYL8sYySxuOLHLrJ0YYuKNDKw4cFAGwdoz8ZsALgOmB91xtt1h4MBNWLdHBJGtKANLx5WZFs1bzY8dj+HT+4OjiPFUfpFksk5EQWExL8I0T"
227
- "ec6gyHdFuB1GQHiUeMeF3Hhd7H32iFAJrVoxkJUGEzqPh0BWNO/u4erU/vE3zj5bRcLK3ygm9DogdL1Zy4B/QBBc5gkTL6cG634bhtDhtCAVuWj6fPjg6YWz"
228
- "SyAROewSZNsiWwNGhpOFsRFQhv5KRTcift1nK2LmdN6G0Bb9dHcqZka34obiNenfMGYojOt3jxd6i+uhe/UgV9jwVdHO/agJyb9zrFCEezhO6Dacx36zgNhP"
229
- "8TT2Ms+9XYvfeqnt+T2EhOSft0nYLVZ048T75suYEcrOKJmjdTREwBC3ZRnWDh/CLZMJgijzzNhOYpPGEongzhEjGB60+oq4GUNU0vR47BLb6tW7LDrdVAsb"
230
- "Pq0+RVzo3G7DVle5PiN6xLucRzh+FQBzUcxB6puOZ/Vm0uSU+UfGTQPB06rMm2JatrbVLMsxkBeM4YgB+sCjDd6i8wDlnRMwQlujXihHlRxKIGqfG5BTB7EM"
231
- "JLPaXHJjTQnOnqrTZoSSAKux7KItZxEoR2FaVUO27oAX4hnPiHIlX+/BWjBM8TyZCkaKdFqbBXgOiU2OOCx9hmgSYMxgJkHXsPKBzdNGcMM4e/CPs0rXjcIr"
232
- "A+Sw3ehxd9J6AwJI4XOq2KM/+yahNwaEx62JBwjdLAxdd52A4grcLNaQCALbEICNJIM0TfL+MIqljPxyY23BBJXBcl414k48vjK8uuU3MyoTuX9DJi5EDS/O"
233
- "fRvhCK+xR4WlOKhts1OyIrQUcKtnyXc+pQ7AUhxoZe5oNK1hCAjPyum6F0xI4FQftTrwtUrl9GDUtjKBiAtt9RZbairEtBQIjURHs3WxujmWyfOwF4U3Rygc"
234
- "ng6wW7gPJ9SrOFzO8Qd8kG8MnMXbEA5vZA6WCw/QjIFkisbXPybpIM0uwiMIbQpjRCFI8QHHiKgOb3E6wN1zXdczyQ7jb5bkAhtBAUQDjserelaaz7DM5ed1"
235
- "yE1JJw7DFocHHknlo1ywEh7ijwtlRudRGnP8qTtvuuWgYSv7UsMQelgYDcQgxQDMYgT/qAmMZIB1eIByN9ssIzOgGfqa9mR7LN31WvX0AzQQWzVJL/tYO9qQ"
236
- "1kZu3qNVByhTl1I0Sw9Sk/BFFslShd11dexBd1gfkrhCYRVG8Qusj9dqKIkeDwaCplpF1Qpc1bV0xkfa0AZ1tsFGpY701Evu3//wSVBLQ9VjccZ3BDbMAcaV"
237
- "+VDm3FwMYzmMVpDxBMTNhyISEx+KeoJhUl/+VrqxgrmXVMU3xtYhF08xW2zmPW43FXwtkMkxRcoG3h/VPwTsTN203q1XmzFkUpr8DKfzXyEFSbGuubMfTt9+"
238
- "//LFi5M3+Zvj1yfvLKvmtBQtkhykHEslV0UO4uKKhpwbHQK1OG7h76W4Un1YilEDhqbvZ/VlgcQkBQYWfum2BY8rsIPdItKmBu8f7GN9vUTT1XQuoFZxGUoN"
239
- "lG5WM8EqkthF3KyaNVXG0BMonRH8MBY4MCwQwm68rPji2MumapsceVf1P8qFuP06+Rl5cX745dWr/PjHkzdngul6e0Kph+nEW3FjbDwKClI8JjESQ3r8nkyS"
240
- "P5YpLSPybKevXp28sNIYt3ogk9XJJKdQ4xQ5gTcRCkC1PLNadAhOL+Uqh5boaV28FxS1nKKF2mrNv0vQD6yq91fqNT2U6Nsg2x6848jPrwHlypV9WSpGNGQr"
241
- "Xno/uRzh4MNR1DMAZ14vQdUSUmmK0aLno+gXxA2BEOcwxz+SRaucSfLAmqCeyQNzit7+x7aMdPYfaUuVedGo4wTTELgBxfs21g0dLAto/iCUCkRyhZRwxaop"
242
- "VY5maABo1AivgK6OO5f8ktjOYtdfETkT/FE5t1grazhba/kXAtBYRHT9qZh96MFQfLgbpzDU6GP5l4ho+6RUEj1eVpNJuWCykpoBgxlxm6RYQHZfUU8s5YqE"
243
- "RvKOme0xsB/EqRQYnC1KwENAVGTOsIXJteqNgDRON5gaTxBocvlZU0uz8qO4E9GlbeRZs+1k8fmK6DUl0P4IfprjsLX7eVRY8wUWpd6sm2pCMbtI7pIU70Fq"
244
- "jQZuu9elBwvzIx4VtEhiB+Bxkd0OgZqxOI3soc4LsneYb9A6UOEQojl1llC1jsP9YbNAK9sX5ZTGfNxcL8bG20waM4i1mpQCEnDi5sCU3mpOqgl7XlPusFEz"
245
- "3CDDwYWbjrviWDA/1eVmXWYqnDMOHJgildpCX60Egqe3W5pCdhRenoK82iabBYimVOGu03gusJ1m4HweAJwYt4AO6qWBD4NqooSaDjd0q0kC+yfmNzUmCK+a"
246
- "RBVR65bcmKPYmixysVzOZGDc9pQUUiMl7fI8Vma9Wc7Kc7eac3tcwkENqfa4NZlvBuQ5riVosEws24vMeoHF2hIK+QWsC52RIYZVooEUndHlKcU15DpfSvGU"
247
- "Xho5FzF6sj0C8lsA6QQ4MeOc8ByN1MATyhYg7eDoRWqNEWRLnK7mO/buh0KQWesznbhe2sHo+HEAcDfPZyAlsudA7GAD4gbIo3CdMPwC0ci65h6xGANlfRlg"
248
- "mY0MCvVGsHqgSy9VWh9I0mMMw2RbPX5TqpcDyyoz8wrCBKUU3l2xACoUuMDARqJfq2rZy1TWhvTXX6HywzSobY+0JjMTOadrYMStiwnH0CUI98maN/9Y1bOC"
249
- "A1oayyoIIKCtQL91DYd7NYVw++Lc5foIKX/8MneSXI/u45oIQlGN0akT0W3Kdz0D02hziPMG72bAFi1LGXUCFhFEIIFh8VgGxWTSsxlWVDzLNrw1Vl/oyodL"
250
- "xwJH9RRP+8RoH8zV5ELNGMNINU4Q8CHI+llvuChIobtvuKNyFusq0Es983sAB9ZWoeui/ORVEu92SmoBpaFDWMPZRGI4Xy33YxsJeViolOvEvqlLUKUUKvmE"
251
- "seWkTUrWAm++OMpdLIbahyn3kyMX0M0+Si3OzMp4b2HdZtFslkuMx+WdCHopUzffl0np/ihDGITu7X05O6cFwt1zwFugpFzIcqW0OvGPGOv7FzpqQBtWThpk"
252
- "9rn9B9A+n4gGGDYLmUTZyPsHsU84rJu4YweFKJZXkW6k4+BbSSsrpTgnsaJl5grTydnnxHfN6IZ5kaExlG2HxFCS9AQtsAkZLKmB10h/F3SUam3EmYO/Lxfj"
253
- "q3mx+jAAmxSaZC+EU7a6+PwmRfu6VDaYku86WPmnE7hmApKnaOK/JrdFcB2Bw4hve46uD4qKWxWQtORo6yin4couLowyKsAITVHAv5gsFnXUIMInol9OpRT8"
254
- "0JGWHR1atilepDUMlSTHxQQUvPYpvVfHDeGb/gQUn0TigokJ5F0NdeW4caoGkFI8EAAfuMpScVRCcAGvNY2UJGEnPwyss93h+qOiHt6EnNokjRjaNMXxF5M7"
255
- "AfhaPtA9xzVInAkeRbTfI6WcTcoeSJbU1ClBKCUOezvNH0rUQZwu0L0SDMmi6TkGZp6rXwo4kXJoQcgmOAYw0s0pLRebecmBMdRvUI0BSkpPPNoJZJFqyLM1"
256
- "ywif1APIvssFQQ1+iwtNShpBkHAXn8HttvgMvysoJf412lzBYoApFfyFA7RcY4LlNUrMOfA+/aDjNWUxY9psINal+BcssOD+BxZY8Ndo/R/VUrwV/4oyyrxB"
257
- "vFG/xfu/gkoB94H4oB/Q8Rg0F6lUYRjt5rgakxzVD3kuijhvTA3yEGCCxjpGfjApvWcNhkPf4nZibEcWMi7zrMRYfDiStdoE7gZBZcFt+Co+LprS0fc6tItx"
258
- "CKwjnw5C1pBsqolOSnLDxS5tdLnE87mhnQlKT3xgpefWdMVbYCLT1b179w5YE2Ab38i3YCUqf7NCRT421438CXcTGMDBgR6yGSDwuhF3rkm1wKS7PcPcFM3N"
259
- "ZO3BWQnNFavrF9VKoFG9ugY/kWn1eZRa1PHBpVz+Bymah0ILvMchevlIDnWA8m74SkQQLTpLYAI+llKUQpqK8Vq6ONhiCVKD+kd3T6aW5Na8PITcuFS3hpMN"
260
- "Sks1eQjYtmqU4XI/QzSyU+DBmXZlO+3IoE7GOvZ9DcXouOTdxdyXtgITyE7MshGYdFmMPxTv5Y3tFhZgpuVXwOSri1WXjCOCzniOxYOvY085cbDYPOaTNKon"
261
- "ZwOozhFvGxYAMUMAnYCqG3EQf4A3hRyBytTBamNXCw0VWAHNVrZ3sxEzwqDw+KSZVKyB0RGvlbSTpmEsBU1GCB6qDQY0ByaHBMjaVESaMqZHhmILTTNEm3YC"
262
- "qBS0v3AoSBEpwFn9hgjesPAYhwlyeeP9fOswP3s0cgleYXeoTwTAG8XFQdT8UGOUQt6BfidNMUYAtixYEZDPrwpvWyoTppdGRctaCf9l+xAKm9toT/9SyQ20"
263
- "2YjfA1t6qPYdI5v0++N3J8nPb09f/3yW3q0nmZFbduRtwt657egewJiLtg5qsAnx2X5UINHGCQaoM2SGekFZcti1AixjxypySVlIvqs4rQM50gbL4l2JSu1s"
264
- "jFKQBzr23SskjeFbpr3m4pVecVozHEb9IXMcMZnm/HGkQ+CKQmbyU9tKFzgE8W545/HIBHwl88CU63ScnQ8fPTmUgYelAQU7VsAFaRcl5LEvVzAf5Jsmm/my"
265
- "6THqGsiGwJc0d9St8b4OPHIEIbiS+7KFhzRYvHDQrXUoO8J7/yfpurEVIwRWMUrJzJBt2pgHGGt/75zD0U2+NHDWiZ4evASi+uAd/gvGRcSgXvQD4SGXm/XI"
266
- "cUn17TtgL/s5yMjSa4nqeMGGQ0vhQsynjyjg2VMphnDYdxQ6AOMeGEG5+Chu5j//7eyn0zcvT0/ePD998fLNj0B4iP0TE6WPb05/eQcH7NkJfDxKo3d3RmoD"
267
- "uGc0nJPPS2Ah2qLmSryVaICiFYXFivtMFFuNEJiANj/ViHB+YUVnVEs+oM7AGAdYi0j8Xl8cYgzN/2gO1f8qh97TgxD3C/EWI5ia78DTDzZxYI7ssJCdP3h0"
268
- "eHg4vAimiuSZO+kcfVNL47bj9i/VTVF/11YzVlOR2raaxiK1XV4F/RC8NFKIHobYI9GFuAeX5IAdNSleIQ9LDQze4p8e1DKmU5N+sGeLCsEzuvxEv8vPy5kY"
269
- "Nf6eF4sCooCYIV7W7G7ac6OIAj+/TooExdACYsu6qeCCiBKf2TWrhIpqJcrAwgKtXJcN4m+zmYsFFzfmRIxkApZoUjVUTqq1L6xLi491NYFOymKNRBc51ISV"
270
- "+RBHAOOsYcQivt0U1YLsD6fVomquknoxu2buBAWk1Zglz3ZPVwWKn4tFYuTcwd7IxJKsf8QAUMf2oFmXy2RdNB9ovuNatA12Y3Nx2szEjGDTgpHhLCH6lobk"
271
- "oYx1LrONR+AUD48HN9UEYwluDfmvWP/B+KqGeE240JkpBTY+8iJmhuQXvoqTCXDnshI82eNHmR3WEFW+oku4EoF4C70Sn/XZuXMDiV8yg6FW6DwR22wFQSwb"
272
- "geqxFCNGCN0224lQvPCO5V1BTzR5CCaeN70IKVuM7UfIkl2zGLTtlEJvieKynJlyjF5Px95J+97MxZr0UkO05k3VE4NL8ibFZFLyJfUSOHfHZQcneY5juzCj"
273
- "hnIcAi3Wp9Y5bCgSM4wXnUWSAjBcdMvI+ZiNIA2k9AVhh4Eb6gel0vgDs5Zjs5ivHH+xDI0ikLKIm+YZ9lh9DmL3evbzrFiE3FYjWAnilmHys5X22Eh7Y3ib"
274
- "i/MeyNZqmDxn8cD5OcmoFW6giYW04wllsmEhCygq2CDdbE0iNf8jdt2utn4DjUywLWNkO9rxpLAkukORHQqjBGMhTu2Pns+s4dQb8gHuqdaymJg3IBPuWU07"
275
- "NT9KO3lRN2ZC745SMxqeUq5nDsepxlEHpJWLAszDhAMSDNBxP1ipmzMzbGOvZKggewnrwG1ur8CoQADsFjMfLEhxSUnYKJ2iCS2kV7QgEmKbjR6ZrqEUfMzf"
276
- "BxCNwH/ruVUbuE4CAuOFXdhGZmDmrBd24bxZ10sLdCcY1Clzi4GyazMDcQSWHBo1zvCXvSdcdaWLDJFMm34oXqNOvyVpgWFeyK+gdmtIBgPLgjy+1kx43eUo"
277
- "b4kMEvyM9Gia4mOZG0PqqyBorFxKpO49B86SXIS6RJSIDt/Iz6nL+ZFXvG6VQbdpW5TKryhxRWErFPEq72M0yyOHP7OKlOo6hgt9lMraP4NGAhQwzOEKaJLB"
278
- "jedaO56VBcgdW1aNAK+y742kuYbfzrmetkxf1AYUN7PSDizGLuzI4dQpRRxE06nT6TT1Q4F7ZCzguWuRMrRK86tl4WoqPIEO7GbXI3sZ0XTOPkocOVGlGzQW"
279
- "EiYD1BNUJjCbbcAVWsdDCOU8iLe1I/wnQRtU/gDXrWN5TJ4vm6W9K6EL0zdPErzWdLm7thphnIzN4+GbwlX0YpCmffhTmaaTaee6Ws/KXrCmHKjMcScfnRbA"
280
- "nHPVc8xIrO4DeXjt4bHh3MlfT1/9cvby9E3++vTFXobk0hRNOo5hq5gd/bKEde0nZ5sFSuvOrqrFB/wlpmH4lYXiMOp94444ZP4agJw/A9mwLIGNA3RhxvnP"
281
- "b0/enZy9O3d6uzDLxwAd7jPYX6DWQXwyCEsZZHJnhzu2jcojymTYz54Hx5pKMWCecZLIeifEqJ2Kcpg3u4NzNQAACAzT88mFYsZpC1tbMyuaCbAVvJKWemwN"
282
- "OYkEPw2qJhcwBf49BMsAWzUA8rPayUpZ3BfxUhzdYeRWmNX1kvyKXRMCRSvBwK8o5wLeAK5dfZPvS8/kV8TAdwPQmCQxyPqiKOvmk00ZDK6M0e84thPbjGAP"
283
- "oPkY2iJZMxCvOhZJKEqfzA0jPtfeqSl9WgF7LcGcYOBnfp4jSMExgxDjkN7DEW5NxG0aPj37+on37VNZfsCP3yT3gwXEibq6zh/nk+Ia7uaP/WJb3BTaWsyi"
284
- "Jn0nAZ2aQTRyvD1jfukkOjFXgpXp35GEQ7YvZVY0oLFYoUn9acHpYi4gtS4AKrT8hKwx/KEIJepiQJj0qajWvceHzvYKRrlhLrGNxWXLoMaLvaQDu2Hwk9FR"
285
- "xuRoI5Osee1YgJJ55aD8+eEFK6l0sFXDNKGa0hAoE83gMBhHxNqYuGtoPvZO0RGEu3CBao9ywLaeRqZ9QwApn021xrJNDgTixkoHZmleLMBXOcQyta9cfNt3"
286
- "9YZSXP+kakCGg+YstjWqZiyrBvlJ5z5h3lmleEFAHI1bepfwBKZPZGnbNRk9LNQCrvfIy6CJzSKxQ1eKwRQzoMrXnJ/eHZcSXdgIbQSzl5hjxUxvF4NwVHK1"
287
- "3VqPIlT7Q1iIkRO8PbOOHT6jpno1HtxQ+fPh0aOLrXS67O1g5P2wyJwLeKgCtxvcfbWAC3Vezi/LCQS5odVWgfbgUqEQtJiQnB2dSQEwZM4v67LhdwiB0zR9"
288
- "CVqt2QwNnkmCrCzF4X4OSrqJaBGVLVbQOsrXdiX6rcW6pYYEyksHEA1up2Wqcp4sMOdbBlijvHr55sQKuWDhtrS0pKFjsFJbXDigFhudk5IiPIQqOqPI9lKX"
289
- "hvXVKS2kZjsDRUKR+P1SKoxxt+LGNdbGg9DWjKlQd+5Ri/D1GRVzmnMakQ0PjFwBai9QFZCHxGo5uQ50Bbi0twLHAAaFvYEHvlMaN3nvcHZChqmR0A9BkZSO"
290
- "phc6rWGc8SSaAcMEG/v6ocAHgleWxQKOByrLzSg9tmiCRELc5QofiLSLKylsVaLwg1CUQ1THjG7Sq2I1kZYqaNk+XRuPmC5HPm/bYh3a0LwUPxZSXKICnx25"
291
- "kQjdVHCqvswJF4wep7Rkt9+1kRI70hDdact6WBpsm9PW6Xx0sW3cugM7Y66RKC9guuPEyh1FgWLEYR6xIUWATdPBdEec560zPgXpBHJO1F0wSBx92s9MbTdR"
292
- "U3E30ShmJG3S+qEJ7jEl1eyNZ+7mCCYpfqXWJqC8s1NIdVK4+qTPCMwOSEoSWCv56XrTsICJCqrck8DrBN1bKQAh1hvpzFiteWW7CluBFVUC5ORSnHgmXUyz"
293
- "bgtqTFVJjWkFbrWQ8eZugtloO+Q0VMxlqG1DwK1NlFrT3uoVqTBSdKn2kEY+a+DpomY/1nTb8RYhBagwWsxZltI8TP5Egte9PIQUA0F07Ah9WwWw7crJ7wQ2"
294
- "VDCY+hw9BvPWbegJ95gJk6FPtGa3+2XNWil7c4BLt/kZwWwMWFwNUBwrh9tcQ3AqmQ/HDMYoHsBom5/AsXldfyhBqyxN12wK4/pVBJTDEQY8nZTjis9TA/FM"
295
- "Pi89sfQtMx1GsGpk+BDBs4KpwCAN2AwGBtOjqctZqwmbc/Wu3TGrwT33CI1EGo65mikdFIUhVCT/9e70DStlY0aNxiprnX1QyKl4lthhoTJS+uKpRQl5XmWB"
296
- "nsGUm8I4vkXKcuKIWa6v0ovMSUlsYkrAzqDVQipQXkkYjFHJkbaFMmpZyThLoZBX9qA5x4YtBlcTtmiVj8rO6RApLPq7pm9qw8YiUZYXNkK3RSU0IFFsJtU6"
297
- "BxvQnn5rhCSylxlLAy/9obwechIzP6cZXtyNEdpuek5asxs1cTFDiQWXChHSrd6b71f1Zon8v26ceItAPaTizrJ1qBaNjoRTPw9VufAuF2aaxmHg4qGhdxCK"
298
- "sxyApvTctEEZEsACaP8w0ujUciOAvtR1S/YgemQ4B3txI0Fx+i7bEcaD2qr4lPOAIONzA5fnCUsQ16RYmxeLago2wQDOYnFtX0vI58X0o5Dzc7gtivPgTYX0"
299
- "eLeZi0c7cUqmiqCGuJRwQ/43s3HV8mkm4zHLVz0+GUQq5sfs2awGxryrDc+sNU7hbVOvVuJuF/XvtDdBpULqTgh8uG2xIPoMOxAK3MbtGBLtrTmwirXmdGqK"
300
- "DujNzlHIes6baL2GSzehMtuAXMgJBkGLwFwRx9INCLHA1PjrXCU9cyOzUUwcYqeAA9iRNKP7sVx0Nng27bSfkv7rqWfyjHSpc5OmZXdrk4pN0IMYJoUZ2wec"
301
- "7yzbaOonkH/ebOIpCenMVviVbuhG8Rj/BYuheWbDAeLPyaLciNvzjMSCAzih25gNZWrLji0yjp8TfgHjIBEh6+jzItPyGO4IqhEA7zMDuJfKZhwqVAtSlvZl"
302
- "XWrq4cPkUZbZZDUUtMd009EN05DNm2e58DgDzpDZyj6hl2OMWaomOmrPduuffdK/G0fnZvhWUqBZtRSnoh3aHL0h30MwZAl0Kyy+EznejGAurbLOh9TCxYEd"
303
- "dkkGbhTD0ovvinbFa2Tt7JCVDvLsCHDplY7FpVThJgRfIiEmxzAMcVXazQBsCiJpRhDRIK56LGybSaq45yixag8wjG48ENWSV1I2J8kXev6I6X/tCtHN0WCp"
304
- "PXs2HKrQ4czs1nS2cheqpWTbIjnWgXbvgbJO9kNZ+Hz4LJJVhPBYA5KCSD56EgAbg442TTRFSXi00pnZyYlskpqgioIYgQkafwZXegLwe0bnyVRQckh5XE4e"
305
- "3BgIS65i4abZfSzcODn6q+ZNx8EYx8IeZ/QjxmcYiz+MwCtSVZrhRYesCjDuB9gYH2eqxftyhf7dOofx3hQa7lxiNbbB7WZ2gWK8cu+sQaIKSjmNpsJ4dM6n"
306
- "gzT1dpIhG/Vb8yHDluknR1+HO1EIbQdlUGk34PAx5A0AOWA/A+pSx/HKyLuti/AtKHb/ceIqt1x+dtyWFM9hXJXiMZm9yMwydKQh4XUN1N0bQp/CU+aCaSrk"
307
- "VUb5Mw04tLQsydNURip7KNfVyWjr10GfK7tVRQa246b2JnSH7vgX6tsJN0tMMDycS9c+wdxrnvQiBJfWupp5tdwHyaC6XqhMjc5Q7icQulAZCiqZo/iWu7Ut"
308
- "maPA51WFbIljcjl1R/ytNwyPdeKWLLtf+ek6l9ymsxYWsxm/LRG/A5aVgMNA8u19e7eFNkboXL/usOR3XfbYOnhrfzj409OojNafbOQQNjUOpA2LnqlKBWHB"
309
- "l65LEk1IFzmHN+urYpE8/SpZf6rGZaxRtryAPy0lckazqNGCjP04vgKrAUV6Qrf9RJl72CDWdh+21QfYfOxDCqERiZKmEG0HvfGpn15wHEpo+x8OHh+KTUHY"
310
- "BX3ZOIklvjkMUCPZooNlwRZNLHVbfA+B6kdeww/cwTve6BhccMcqxIZgLI3T7fZgz01gIT8bgaSw+XBa340cugoYP9/Mc/gKsKDQ7sFNY0ko1FTt9dNT9VbO"
311
- "mKMDSEdaZYk92LvcLoCDHeKM3CCo5nSG7XP1IkhYe5J/OYWYxAPhxpihXoGIw7sf0bV1VyMQcy3rgUdimuU7VJkYJvx9hpTByUmLP0trHchk4eZGcuzqbmEf"
312
- "qhlBr45v37mnlc64ns0828OdRjhGrb6t1eui2MRrmWu9rtRyyq785v59qaNMc8eAeBupraiqodX12jZ0ntkOd08NAN2IjLBJpgkP0UYe9TcDTl2vy+67GhD+"
313
- "SkaiN3pEezF3XmH9ZcgCp/0w4qCVIDkIRZRL/1ZvkmJFqZMCRpOG6TtfwVeD5BjnkXy6KkWtFcWTVnjfXNWb2YTtNQdJ6nf5VgnbIGklqPMHyRtwvEkKiCeX"
314
- "FBvRorgtUuwacT1gvRqGMuYbV80mGjUVQWCJXQ2hXurP6I0IgwXD7jHFyWgGaauZHR6GHGboHcKLgw1lyKkC4h/1k0f37z8+EofbUYzbM0W3R2EtgCM1Z+Yc"
315
- "0gdgaC9nicValJS5ifQG3PbTo0eddAwg+nW6CgVN7MSkB/UdVsicNgaehNBdSgZkG/7ZkQXErI7o3RekMfJQQIi2gpAGaol+b1hq11WZL8yBhkhyLeFfLSal"
316
- "EW7IXZiIl/juDW5Fv2Q5T5hDxihOgtfhLYw5FNgQzdjDbE0PnkxE5StIT2YYGSRvZewFtlAC1PXTFWmZd7pTpYewsVg6gpY4TXbWpS00dDE9dkuQpssGd0ix"
317
- "ulBLeB6VgFrWWav6k2Qy+F1mWmmpz/wmU166s/q9+V2/zLbRnlGkX3+yFA3BwheROaOT1tDx2YqV9WLjq2rOl1gLplocGbKw0b+SfBJWQcR79RAp7SpChhYB"
318
- "i0lvZbqinA+DHHBeMHIkqMWATi9fv/7l7Pj7Vyc5Z0MVf1//fPrm5M3Zu5gem4zW0CdwXqTDyAXbu2eoTfNPad0Xr6ev3HBcBhM86P1goFcKMAIf0NYKwH5P"
319
- "i3k1Y8yXnVy0VPJX4Dw+by3lTwVfANHbKW0UxMNbV9MqKnF35fgp5XPEpx01dI4QIHaoPhCXAEGUKDzFGmzMypVYjp0t2fL885QDCsEEOIT2nC5r7c0Ysn31"
320
- "GwMIJpflVSFIURsQtm1rIW9GOxZA7sMdCyWbhBZ1DPd/YlzjZT2rxtfwyPtNvxBXwQUEBoVw77vb1+mKhirx1D+tXFddGplNOm0JVQHST+1VQabR6lppexFf"
321
- "wXDNwOuAiiXoVW1yPOpKZJgzR4fCZr7R72z+G6/vmMVo5nSXaUyMnQ2X9OHAXiueHXHQe8WKnjQeYIhYzHNm2YvHNZ3kWrID6h2JPXmxdKHwyoUmXla6xNyk"
322
- "YzrP5fQsWxd4yU8x3IvBt90r6N8VCgYnyPgF8gj25ugMAs6cbhteqEkQYquguspsRRvoW9b6NMbtHeB3/75Si31pGHseBCQtLScUEVanIpPND9K9liNo2t95"
323
- "HRSDKzWx8sWuaFghkYusi5HgIF6veoFSlxuNOsxLUtfbbpG34t3cfPnrjrHWbTja6VjpgutIMnlxW9E4x0KehRNVbbdr4jIx03RbrfWbTOV308bS3hrA+ynD"
324
- "fKplz4w/CVA+V9lnNHHiKOCD1rNS7iezcYK1tDWa2KkRA9SvtX19ZISajwjP2zevI2WRW1guXhaPgBIVRqvZGa56ur0Dz4BDcURR+8wuQijX6DEizOlbRoAq"
325
- "YZvu625CHYmO7HUa36/68MmCu103tGNTxy3zjEbAJIi7G3ZFgZuOezPp4ALXtt0EALbZflOLMtm7zxc2aREXhNxN0B0WEGvLA7LoVupQVgC0GBRFvNmTmBmN"
326
- "0isDJnfks7X/wbm3XheWnletWFcEQHcf9lO/Ex2QMJpEz9kvfRSY5u67TltX72rWxVcQd6rDnd6sJ5PGxwlx1KhTLqYaNkVqkrru+CVIisDl8vUCFl4K4Wxs"
327
- "zv5t72//9kzEjquPe+3rcrYb18F/21M8YMStVDCRUDAT9qbrxsfyIRI32/fn5DFTe3VIZ+w+/an4EjomgZFiG+SY/ojIAEUfkpGQJHELbANVqf9dpl/x4wi8"
328
- "d90s3RyWR2lqMWxZc1XPJoM02pCedLQIzbuVs25r3gPkjo7EzAQzWU6rcSWWkTVeSoYd62y3M500femmtzJ2JGxr/RRbsx3mN/uZ4YQpxU4OibEqFBtnd4SR"
329
- "XWRFNn7TAaambo7PRH5xsY35qh2083Cx84919AexI7Uf8psezYr55aRAJw9piwW/zx9dWMwDfUkfVIsp0BpQb1hv2tTkPmdH0LI5O4RXsJ6JgVDHeP7S0cXM"
330
- "wAU+5DFc2C5lpg4Z5pNsrStTYXt2399ZLU5Yjnd3GUAsZHi3C/Smlz3/PgifBxw2CCiXTCePEy826zqXnJwyRywgPitcSMSpvarFCqUHt9qx8cBXt9vM+25k"
331
- "PwpZO0+99diINmANdxCacOi2p15mvG4crbWWzL/uYG95NVmOc7sldGNzmeu2hw7hi62t12FLjK/fO06ZDYv1qhiXHDNK/R5AbrNinYsKvez8wTPM17f914Q4"
332
- "m1aLYja7DkxXx4aaVM24WE3CNn87UulE5R52NN1VOROEsOztG40YUn1a+RwEJSq/UHQ2d7oomYIv5zIU2wVKqHxC+AXCd+HVhBtOVMNODC/j/HNixfnkLRg2"
333
- "Dnv1YLCTJnUwCKajpGMgUl43QtZ2ykwNQwyUMXvlb28VNLjkm4gZW4zuQyHzbCdacPq74ZPS9/lIpaF65/Bwoh3o6LLUggrRabVuVAYTWdyON60jCLo5nbyJ"
334
- "fJkwhE4Uz7dytG4UwkGa7YOdhmxAk+uO/ewgy+14jqF9jd5v2gKWJjsiGe4dItuMQRiMXah2AlkgCT6Y/fHoEIv5nPLudJxOjXR/zWYMOX0p5bV+Pdnw5ZhT"
335
- "DvN1xEgTCItDtaQHYd9yLbwOfdw/yjxP1w+MuwsOxuxtppxnPOK/9kd33iP3hV2cUBT/7bs3RaSFaCI1Mg1meOc5ktyWvHVae4I2aBwwVjNOyOLiA1mfrmFf"
336
- "01t8MFmKSuEPR5qST3T+tKTxQKKkmWnW3cIeyjmCPagJMZFJOK6h7MlAkjE5yZyDWath3upBACOh2/kwlO0s5r5VWRy+HWGWiwtzwiDNoL7E9+TRIcV3b0/M"
337
- "EBszgjlf8cm8mfdUHng1INZni3u5MSXZ/0NzNPZ2aW+VlnW/Vu+YUeOLZ9JQOUko+ulIr6nV1XcjQzcr1dxWNhOjM8pqYueksHoBA/aev2cJ2o53n4nUulSu"
338
- "BJaYJfboEaeJFT+sdmFd9EK2NK1LuU0/k00/M2K0dcixwjtQrrbKJDIHn7xxQCEpEfvcSMLBtVAYwb8PuuhvIpx/bCupg6ycl6v35WJ8ndfTaSwH5K60bdHk"
339
- "VuBbpnJjsel7SglEdIKVIeUh2t49B9Z+EZztlBYKEJjRKZAxz3Z1jAR21znBZD4uer7wnUXp7phbgX4Dl8VQcPYQc9J3QzFhguyiubqs4WoaWdnbTs5KyeBk"
340
- "T6d8D8Ow2yXyzZE6Biz8TIoOADYoe28nq09d/a7pQRmoqiLIMlX2orsQ6fBr0+u8qcCw6pCDfWE2W9cX+cs5QGwP/i9QSwMEFAAAAAgAkUArXQFTAATEBwAA"
341
- "YRAAAAkAAABSRUFETUUubWSVV01z20YSveNXTJVLly0Slu11srFPXEUVe1eyvJGTHMUhMATHBGawMwNKSPGcH7A/cX9JXncPQNqVQ3IRKWC6pz/ee918pm7s"
342
- "fwdbq1VjXFL/NsGZtig+7Yxq5YXmF3t+oWxUCa8OJkTrnalV71tbjUq7Wu00jsSoWj2aoIaIt5tRXbV+qKO68jUeRryHXSzV+0S+rEtwjie6bUe87XXQCXbb"
343
- "4Du+yHbdkPSmNaryLgXfqr7VzuCdTkoPOALzCjZR6bqzzsYEDz7EhYr4oMch2a2uEp4cdGtrPlshXvm6UGFwURm8GzRFQpa2waOcI/7vTdj60CEJ7XQYFcJA"
344
- "UmnBWdM/UW10tVfBNGFKsCiePVO3SDoWxZG/qKNaDcl3uKVSsdqZemjp4W1OMA4BcdKTj8jeUyxTdY9wsVwu1R/8xZv13Xa7xv/f20iOanz9gN7g40dKTQeD"
345
- "yD6bigrL5z8NzrqGTH4xZo/CH9Unj9JSOj3u7tN0cd8OqA0KP/YevcLB+4SCzpXQU0LcP71N6HGvUQLXnJU037qzbp/vvUZxRzQxGKNqPca/EsGPBrCkzq6o"
346
- "4Ur3MMBVamPQJDNFxleuGmnIwXB9tOVcr+C/Ncko8wSwcFkSLn8uFz+fYFyRtz9/XSHvOz0qj+yCrQ0jeOp0qYhTHQGBspwBQO3RB4TGINj4tAMr2FIcXh98"
347
- "O3AVD9Y8sq2OADfaMPTK94zZN0WxXq8TEiqWS+HtUhi75BuBkKN0/Ti14XgqDmzMdMtyDszDZueH0I7Hmip3NNS0h1cP1LDjIyOHbmWk/yxkUf/0g6tRkKK4"
348
- "1tXujL4TDZnH2nLIS7WWIMt+XL+ZCCdMp6OUq3nqfaAW7bzfg1awoWY9CDzEkB6gOtFubGuTiFE0LRB/4hBbSoe/sI1jTKZbZtDpoekANv21YcaEmEAwoBqU"
349
- "qChA72NacgynGDvt7NbEVH6O3sEGwga/C2hk3JHozDnmpBeMDPH37nZ1xRqk0xAMu7t6t/rww/XN3Q9lV8MbtDUsOWFUduiboNHj1jdQndVJE9ltT/6Roqvg"
350
- "/UTJBdPMy1c91DaJ7AEYEkTEn41/QqBw7aj1QHzrXYMB4BmeX3e2BLFYokmCKBuWWedBZZ2AhfMivlXegYtfEJSYU+20a4Q326FtzxhKbBRRPRHio+1Na50p"
351
- "iheluueGsyks4HEaWZSNhWSlR5/bMEs7zafabPXQprJ4WYLrumYPaM/WNqh+zbVeTINLakODAwT2vXnLp7MLeURjDWrIdmg0fZ2mnljTEx49ZfGKwpaZR0A3"
352
- "wVKfoNbItuboeutoyuZUJobw3DE14YfOom2/GoJ8FUyC27+XahX3XyeCMW54NlLrtxadg4ApGhT46EjLrKtNb/DHJTQnn6fan01LXVFspO8TDAggm6FuTHqr"
353
- "tNpCKk6XUbCoxzx+Hm2CpCRV21hpcQPBA7ZOV5TF63ISXWVIRAQUZ2HQ1ch0CKgElDAMVeIMBWdUHJFFVmlX7Tod9rBGG2RvME8mVDYKzlAMoBjD3SZZR2pG"
354
- "ePTtAYD7plTXDnVCV6hkC0V4W+DA3gAM5FOammxnKDEpRMwdoqxPGrjcZHGEVvlWlo2y+LZUtyYA9GCcTdxIKmm7BIFASM6bIqfSqQC/vuND8hQOUT2U3T7x"
355
- "s15XUMAFZeZIqBSLNhKrhg61SM9PRaygeDrYSHhqSJGFPFF3hp0DT1SReStDIylqXPNxRBcdVa4Csqll/yDydxvUBmTAEiCbGPYcsBvMQD2SenV5IUlod440"
356
- "tUFJa3Vzc6s+U/Ho6LeXF3m5MjQrRCwouJ1tIJ9pSfHOi8acUll8R9DpDVx8GcbZLuKJYY9UHubHXI68vBmmHdMBWHHq9cXbqZGy3mjimefYpm2vLF5clrIZ"
357
- "4fX69YX6/2//Uy/z54vLy4t13hNK9QEzfNIDmf0xtxnXRlFUGhILxEgcNU+UAbKcjYLpOHtWBlwN7ZOF0UidJExKjm4mnw1TnJRZ9tQvtzauRRwq6iVGRAge"
358
- "eofz1GuHDay1HclnThcaLMsbgYsHRWJGApnRnvhOIoGc8iZz/58bKyWm/XzMMOA8sa24PU4Cp8ACqG7dnE99Wt65VJBXy8saO6VUsAHV0IkKqoba/mI2P73n"
359
- "oN5/f60weWxlcpGrvfOPEKaGgzqXuV5+qyDn2hxslfmcl3GZOO8s/ZYY1UdeCLBq7eTBQ9bjB/hLuzXiGxxC/XrI0NZY11KcHHs1hPMz9HMozmOEsMpjZP1y"
360
- "Xar5Ms0dWk/HWKXWNCUxTSmBvPYwSmQURVknJ790/m/weDeJYxTE0paVp1oklSP4oEb2YOsBpzq4JAiRxMGi6+PU1rPhQi9/xSwh/snwyjBtsCj1gm6Y2PDH"
361
- "Ew0qexp/HUiPYDSVgH/EzTgizE0gmggS9GOehaaeJyf1fI+mCCOyNfQqyRjCVUvyNW1pXIN/3d99uIFltbOH3Ph7vTXYJ+/6PMui/DCWxXx93ZFuE0X4B5iW"
362
- "dRMTtsvMksEVT2OSOkBnsT5iwOFiXpEq07ZZm7Ykjnl9mVdyWTh5ddj4wMNlnBE06crqTMry2rUxeclDxsKSdnxDY64bIi0ONFtnmci/Y1gt4G3a1mVfFWhN"
363
- "G2km485Umbj0Uw79B6OnBQWdaz2WUujT71BLAQIUAxQAAAAIAJFAK13NzUY/oAAAAHYBAAALAAAAAAAAAAAAAACAAQAAAABfX2luaXRfXy5weVBLAQIUAxQA"
364
- "AAAIAJFAK13P3W5mE04AAPBjAQAKAAAAAAAAAAAAAACAAckAAABjb250cm9sLnB5UEsBAhQDFAAAAAgAkUArXQFTAATEBwAAYRAAAAkAAAAAAAAAAAAAAIAB"
365
- "BE8AAFJFQURNRS5tZFBLBQYAAAAAAwADAKgAAADvVgAAAAA="
128
+ "JFPKY7yRHgOddo+mxHs+rV43HuoHUEsDBBQAAAAIAAAAIVzfKqUoQVUAAM19AQAKAAAAY29udHJvbC5wedV9a3cbx5Hod/2KyeT4CFAgiNTDcbiGs7RMO9qV"
129
+ "SK9E595chmd2CAzIiQAMghmIYhj+99v16O7qxwwAUr6bCx+LwEw/qqurq6ur6zFdVfMky6brZr0qsiwp58tq1ST5YlE1eVNWi/rRI/2sbvTXq7y+mpUX5uc8"
130
+ "H+vv9Ee9HK6bcqaf/q2uFvr7PG+u9Peq1t9W+WJSzfWv+krWrv8+K5vihfm5vliuqnFRm8r1jfnaXK2KfFIuLs2Dcl7o7+t1OXk0hSGPq9msGOMA9Zgnxd/X"
131
+ "Bb2d5E0+nuV1Xdi3+pEpUUDL4jX+HmB//6gW3NJSDVYhQxf7GcaOL5qbpQJSPz9c3AyS1/lsll/MuCa0US6mlS7yf9TvN+r3o0ePjv588vaX0zcnx9m7kx+O"
132
+ "PiSjpJeeTKfpIElP1wvVLH67Khcf+fvh5eVKoav8VKT9R69Pjk/fn7zNPrz+09G7w+zPR+8/qKZUI/tew2+z14c/H37/5u2b07+IYs8f/efR+2N4Cw0dvj51"
133
+ "mvj+8MPR2zfHRxkX+nDyy/vXR+rV48ePp1sQW1fb/O748B00mL6eVetJnbyuJsUq+T6vi1m5KNJHhL7hVb5aqDGbTqZNscqaqpplChXrWVMPkotiWilAVtV6"
134
+ "MbG/1gtuQRHZfNlky2pWjm9MO+vLebFQT/Ell8Rm3XLTcqY7VKN6NCmmisDq8aq8KHr9g0eJ+qwKhYdFcos/4JMu8nmRHiRinAP7clwtmlU+brJPxapWyLIF"
135
+ "fWTJSvkyvyjV8imLWlU4SxEgoAkawFOCGh4wwp5eVdXHOj2nRu4eqXl7ZGf19ERRzs8nb9+8/svOU4tIkHjp4b+D5MkgWVWzYpQqMGCUxedmdKzI3cXTrKwb"
136
+ "qpFUq+TsvO+B9vP7k3c/nz4IOHdye/RnW/jqZsU1AL409eH706GarA8f7geZJc5eOwS3d15pIOxtyoeLo2cWyZONc8JF47PCJPrjm7fIqIjY04+ForXZcHmj"
137
+ "aDLOMYj8UrGyvNIhJXIVZ9l6lWI0wtWY/L0K7qQNHimMMf98e/SDPzg1m/8oFnXR9G7FCAfBKAYRIO/6j3785e3b7PCno+NTtaLfH8WafdLS+cCBX7X15t27"
138
+ "X04Pv397lGl2/1qN/ORYNY77BY05XzdXit7LMdJaqhEI/KVuisW40I+KT/ls7RZSA6jkg3w9KRv9oy4vaR/in2pzv6g+q599RXG4jSZvy7+r3fg/EU1Hq1W1"
139
+ "6r1fL2DzxB9MZkCcWVYuyibLeorBT4EYJ8UBLLZBMlfjzS/1r1qtmHV9kJSLRo3w5d7eQFVv8nKmnk3KcZP8MwECVu8EHcOnXqsR9/pD0xG327clVM9D6FhV"
140
+ "hmWOX2GRz3AQGU12VgDkqVeNwFIVFVw9/qGqKvi8ggysKgnQ9vRPVfb2rs8LNVtU171+8vS7ZDqr8sZZi4C7IfzTM6VB7OrB1CkcKREDKyr4nWpQZjhZz5c1"
141
+ "lRwkitaAG+X1uCxHP+azWj2rFVtSw7ypR6crKFMXy3yVN9WqHvXSAVD0QdoHhE9zxQtGqhMDBMhjBAkIQwcoAw2SqRJ2LvLxRwuY+kuANasbOzkSSjXkSY2t"
142
+ "DLFRYEo9RaYVyHujdN1Mn36T9gmtxedxoTjxEf5RZBq0qAHQYKrBzMtxCOgyv4F+LZxAPdQaQqLwoJbQcP5xUir+jz80korPahll1Uf8SWA1xXypZhhrXpfN"
143
+ "VQZbfm+aDm/xEfy6G96CnDqEf14qsrwqPt8Nm/kytS0Mr1dKIKbxi/ljUOMzWC4mCrTRc38u+1DcQ2E4DVU9HF/Nq0kPuh8ke9XXmn7bEb0EaZkrr4rlLB8X"
144
+ "XB3Gauijvsqfv/o6u7hpCrXt5NcHCX6NEisfPIZUB0ojfiblZaE2IrFKVvN8Vv6j0CtTSR1FrVdCdfE3Jfpj+7DSzph7rM6pq3KalHW5UEtVMUC9JoCgAxK6"
145
+ "dfYxLHmnm1AbeKQZ6E+2k5d1EeGEablQFSxjuVD7+KyAVUYPknzVlFMlCybzdd2o7V7N6Xo1LhKgCOAYOchZhTpqaapBBBx444X9mABW4oIC5jpbIsnDN26w"
146
+ "XNC4hore5nWvL2e3uWJ2qGuy3KNY3qpcKsrVk57+9a8A+zPBGhlDuhYSv0LVqqlhVfSgLLY2HKYAAyxFXJV9WG8ka6gy8ArXqml3F6xCzRhOQdzJ1cwpMSzh"
147
+ "KoROrGDHgDg9g4fnAhGMOBYBeayCUIgyFjwjm0mBgTXS/7xUBzk61vmAl3XCbxPbX1+uH+xUrxKxNjJYWGqFRMmkdaXoZUDs0l3IhIYhshZFAZo3I6URlVkK"
148
+ "A4ZUTKhzTWj9Ow9MWgMIaAeYPsNwgSLu3jrwfj+ANw4EMt7tgUj/uvjrIh3+rSoXitX/NhmNRoS0O/j618UtoeIu3R47lnvm0yIDXUGEu1k48mumUCyExHlY"
149
+ "l/mzD1f54vIqL+2ijbwLdwOti0AG7HNF9Wy7Hdjv5tEj0DooGf3ow9HpB4lZ+HZuTw+g6ziQ5+Z6fFVM1jM4O6cVKELkqbmaTarrRXalEApH4D3ndLyYlKC6"
150
+ "yWblXEmv7tt5/pk2jsjzMUBdTDJQOUTf53X0eVN9VJuz9wKEtmrdZHWhVvkkqLZuQCuUqS1drXIc40LhXw5SyfFVRhI5vKcdX7Sg5Nr5ep5dKq4GjQ+5+TsW"
151
+ "z1lp1IrS66L4OLvpxOrvn3eidb8Fra82oBWk+DhiX3zdhtmX32R7e93offH13gYMI7sFHeGkA88o5bWieX/4ykOz1si1Irr4VKxushfZJL+pO9H98ptOdD9v"
152
+ "Qfe+/8LH9/7zVoT/3q9rEK4qbcT4759/EYxvoOwXPmULxWcr0ifqmNVN3M9fdmL7RQu2X2xiGi/3WrGtcNqG7ucvN6N7/2X2chPCp+vZLFPH3EWjhIvVA/jJ"
153
+ "K4F12LX/dPj+h+z7X3746eg0e/vm3ZtTofhpxV4Ucxuw5mLsa/nUYOvFnsBWBFPP9zNkBlrcsEcHddopQB82bTk3sD7BHBpoa1UyGGoZ1DZKD9Ren6bB3gdl"
154
+ "tjgqQDOg+MFnqmd15CDBAMQ/RaLFipVJKMuUE/iFcMOXqeI1xWq5KhdNerfDocMM3LSmoJ4WK1AI0YGDJeSkWsxuWFIZJAiOOtsZYAYJVVZTnkhYtEQ6BbJQ"
155
+ "59ADK5kML9Vg1aMBCM/ecQKFI/UOZKLetmjomwOZ6u9M1zrXQvhtejmrLnJEFy4F+FJOivQOOodCWE20fO68oE7O74vcuoBLKHVaUwidlpfqzD4ROCvVOYcB"
156
+ "NgeJnjsMIC4eAVbBt4SQc8SfV/43XnkzEF3nYWSCrdRX5RKWAZezsNsZdkjTOVcrUOS7cym2eq+cg42qyQuYgPvbenKpuB0wtsjyFcqz6Gn/Qu0AfT3R4eue"
157
+ "6n9AjfRNMbjWHJb1FHSIhV6w/G4v+XbE7EB92d/b2wHNNBLFtJZrpE78TcuQx75Yz4tVOU5wtHVyUTRKYFuoTmGGVWfeERDAZvjMxRRq7bLiUzVbg6ieET32"
158
+ "DK9jfZdqvmi0glIK62cok7tTIrbbVfGp5CsrRxJURAM70YkrsovNmXocLqtlzz7ue1sfnH6gGfc4IQopsUptcoqLXCrqqUPBjd6j9jZbwd4El8j1lZICUFp2"
159
+ "pCYqq4aoBLVY2W+6BAk5HO+lHNRVWTeVap8v+rKJOkBdeWKdLpOP4SJc7+dppASqVTLYc6FUrESxmETfr+tiBWtoWeDV4RN9KUizBHKVAq7tveKmBWqIM+ae"
160
+ "futE2C0vbW3LZg5w1wxa6ChwAQUaR/SDD+45NBdn6vu589LZZXxxZSDllEFMPhlIqWTgCCODUP6wh+e7TmGNgQ3enLcLbbqK8/i8XYozXcjHsvxVvpoQR82u"
161
+ "i/LyCo/LQ0fIratpExb5/Z7fyqogsVyRjzl4D/fkYVAhPl/dBDOXKmK+pJvsV4Pkufpf8TcBpDOwcTVfzoqmgJV5puTp5JX6v7W0XqNnStqH81VYsl7jWssm"
162
+ "q2rZAjeWI0ZSLsarQlFBV8nlH15lsxxu2m5i5eUZ4E7fx/tiasC0UYVOmx3d3q5XcCkRvQRz+TuoL3FjMJdQoN/xtkj1SAunhToWaA0y66u4HvcJ+1/75tIX"
163
+ "FYfrJSyzngWB3s7tpRsXRPkBd44B7RwkJ+I3o3luymYG92DQhAIfW2GRzzOe2VHg0bKOUbyrfgcJKVDUXz7hDwAkx+gGOjDbp7NzQmvn+v2nslrXmRi0xOTt"
164
+ "XX/30UNZzaJUmyDZ6Db7KCBgZ0oudHrHqopRwXy2zp8upOeOZHkSc5iLDvSpaKHnmRWYMCvMZVULd6YtEqlR7pNwnNmNHl4JrSf0j0gxBTRdrS/UOlKrP9OC"
165
+ "g1DSE4lRRSNXxA4dw1l1DZfD2CTjzW8wOkfbt6pLZmYJ9QxNaqZsWrN7FZO1mVuYyx7QeGTgcIqMoGMUjMjuR7i2DXKt7IUFxHRZyPQy9YYDGu1gFHFMqCH5"
166
+ "bZpzWkX2bcClZ2hBwWobX2lmtJW7nHjlZBn06BVewQqnfhXjg14HiewTFzt36nIcOJ2hrtpAEqIMB2bR6Ymq53rJmpnwCgxQlu33/RaiwiyuqvxzT+1E++pw"
167
+ "Xi56+8M9Psj4PUQbGKA0jNSMX8J+o4LxLv1GG8B+v9H9fqP7NSK0WTma0jzh+jxYLS55+7K4HVXQEI+EhvH8ZfIElFVo2+FDA8A6GIqL9V6T6rCBrRnY4rWU"
168
+ "8NNv74EPBXpJBm3x+wEfGog90dfOhRm0b5YnHT7U3lg0OZjK7rT8QrDcJ2YpQi8Aq+klMnZx3GkdvygTY89ho+aE1NqkKdHaoHOwEEergX+WEgoRI43p/nBP"
169
+ "xaNWcJfu6ykUYwLzvL57OW5aPKNv5w5vgtMQvIMhwk7tKt/gibEIqKHfoNz52cErJZGjLSBAaVEZngcDXIZFumfHPUEGzbmvO+cFzDtpYgwMRtci50MNeZre"
170
+ "Quk70zDUE5Izajt1GXs21UvFlnTnBQYUqXbOkvqjLcrFVNcaGZEqRk5aLmelkgvosOyIHyzbR8SNFjmBD9wDkIO8c4Nfph87RRBIVEJeACuO6Fmr4ELAwySe"
171
+ "lwC14b1DaLWipVnWzSOrFZoAeZn9dzVHDQrNaLnnoskuRqdF5smmFSqsl5Vg89CkhV7hwvQn+TmjCqUA/CrEZF8TwNTfqikQzboqAS1ixBUGfIoQSoFwt3VV"
172
+ "Bhv2WqewlPvdRghbJCLE++lrCSIGHEoKe87eGKgvoBNQYAhhNtBfUJnf73ntBAoMKrf3iuQSVF4Irq3VGZ3rgstE1wXJIlTAGkLAZ5NGZHttyHaakLgWxJXu"
173
+ "9oYAB00OAc2niLDqALHGkp360pd60BZFynadRSt3dtepjeFOXwXyq+yzo4UB6nO45+d7pmcxv0ZLrql/3xMFTQE1ydjSft/RuKuCjx49+nfjqtQjy3E2PyWz"
174
+ "axLADtlkjBgji5Z4D0eaCDRjzSLPtV2sUN2gCZUto82w4be29c5ezyowS/vwX28VR35dLRbke9Vjz66hfeQYfhefheF38XmcNTfLgr5pW2V4uFIMDkx5BZ93"
175
+ "zJUEiqytN7e9RaumnWm5yGczr2GyDVfjI7vr0Lj9fXGpxDANj0LBm1O0yj8+eg1qqA/Z67cnH46yk+Ps6H+/OVVTD9P1SGDBMX9fVVVDUyAt2AEGeKNqo6kk"
176
+ "fAf7y7qafdKaIKfgtvbKUVwaY2DTHFgE/15atLcbfxEdsWWwgUnbMNYZD8MO6VmSmrepW8uo5+PV7GuvXvFZUUEJ99nReva1Vw/IYb2Md8bvbA1ty4dyZWSQ"
177
+ "g+gYBlEIB2H/fR+lzdUukxqdWGdyyQ7Rm9fNcxuf38lFpg2GJdJWvDaGxAg8dCtUlZ+KaEV6NQR7Tq8Su56AgXu0ppmFTJT02phV44+qnvElHb5/q57460jU"
178
+ "151kYH8vG/br0GKeXPjPlQTWKETky+yCHRmBm8Br5AFBs1AJtfdo2OocUKJYGCIZOKJwKwUwq8SWh+ChdlV87sVb7XDD0GesB9EOoVaR5Jr8g3svnrdPAoEk"
179
+ "XCPUM/AO6LW6OERR4HI3v33H92HziJzRMGJVU2Jqx7T12RkN90TbJBQGWuMisq5eYuSBrESBESimgNCr1c2obQfuwAM0rlbNdcZtiH7fV9f++MH/tgUH2JDe"
180
+ "IR1CA8WQjx0oLdCj14tBj7W5gg94C/DK0sjoJ3mNrUSgKD4X43WDfrjLphfQXpqmwbOf3x/+9O4w+ZuSddTuj+fn0f86fPtvQcHX748OT48S9LpL3vyYHJ+c"
181
+ "JmpH/3D6AVX88zwDtVWPharkzfHp0U9H77HY8S9v3/bDFt8cfzh6fwolT2JN9JMPR2+VEJHsJf/rT0fvj0SPPX6zn/z4/uSdrBzppgNw7qkOUQUfPZTTo/99"
182
+ "qvD05t3h+78k/3n0l4ErP9J7Pc4B+8C5D6PtC/nSKz7PF+VUHZfRoN97R8fOWXW5TRf2VJgoLLw1pZMfjn48/OWtwu3AHgpbi0Sbnpefi01twxEB7mTyxi0y"
183
+ "4PN3+MZWDjrdbWpX67ZpVW+ychLOKt5pbDGXfDmRfSwXk23mQJ2U1vOLdoKxRhjR9wYjjx/H26f7yhipCCkrfGvbvb17DBOyrGrFADYVjKMUXaQ31sUza/vo"
184
+ "Wqkl2ifdykZKG03Ar0pcGd7ntJAY3fUoItNcUNBZcvjL6cmbY9X6u6Pj04GkRjOEbUmLPSJjc2/uuyPshcCLvGibgQeia1IsZ9XNvB1ftkB0ZcbXjRlh/HWc"
185
+ "5cYWNKqYMrVUxnDL7W9cA7WtrMpxHUNyfD22kXEbxT4Qu4uqKdUKV2enOHLxLiYgsYlC3rgIn7NC1Xu6A4oVHIvqelZMLmM4ENPaY8AsKLpz3V1/G8z88vMP"
186
+ "gBkhAyi54VRv3qP9sIYvBsmTCMjDrIrQm/ABKyylxxl8tM/uPB8PF8V1IE8P2EFdtxN43w08f1/P19fApBuw04sAml9Sj2WfkpDiPfRVWvbNE/t1bkIQePW8"
187
+ "h3GnW8cCiwp7dljwgWOgo+jFieFJtBFg9j2LsvsEicGKtjx/803VHJEO7AXxgW8iZ9GniohfvtEdWf7Cn2gLprI/PMM6VAkKhiBM5OQp2HOv1SgFG3Kw3UQr"
188
+ "olY/0EduFaDZHCK00C0LnjZwHcDrwP8RHlrijBzs7UlPrhZ7kgHtw2C7Qw2e6sHfd+Scb3opy/+aLeEpQAvzycn7HxT//v4vkhPjhVmyr87t06IZX1UL/7Cm"
189
+ "cKp7Cw/tfAXta21atA7648Rc8OsOkltBlHC1pbs/24O7H2siCuvoLmSFIiyPrOmUw+nWRovRmDVueXkiGbW7JruV9CSMkim7bD/d29vbf6oJ4umtaPbs4Jvz"
190
+ "O5f9NvmKLmZjmtJnuvlIlW59IHp1eeiIuB87fsfhPPYYumcU1EGqX7RjULvuBT6aeZuVFXJzD5HhtipNxyNTFb7EU3yqJ6C1tiqil2+kDGJmhP+6L90BOlRu"
191
+ "sJXqYZIO0+6mbmVR4fWfDo9/Onp78tNwPkkdRKe/NeHPwNP8zaJsynymZhq3gmKSUJiapxwvAOT/vCkxLNhNMstvitXwrwBC90Q5DCacAqmsMGoDPWPu7sHH"
192
+ "xoGcIudEb8/wQuKWZ+J+8ufDt78cfej9ceD8149MkwECyERrjtOBQyC+JKLK3QeLqlXelPhvf2vC6GZ/dghdXI85nmYIZg/iZgHrYvdx5Q18CTzNhuuJgCQ6"
193
+ "tFwNrT6hOt1SaqilwY3cmWGj0BJZC59sV4l7Q92AIwGVvl33bt8jJZz7d8RygEnuqmUnFxuPRCGhv39mQD4XMiwMDw5ZZuWQiC0lVzAWVUjVBmdoB6ijXT1H"
194
+ "KMDc68yG58BFy6bJ7EbHGEavUguZHo7Q1WPrcctEBwDHBlFMohJ7DTRgUGbNzLbX2F5fQdAVPQKwq5oVCz2Gup98y1D+LtkP9yVdbJgvlwVEwWOb91BMqK7b"
195
+ "pCdPjekKUaRy1aeoP6qZ0X0M+q1CVDglqnsllaCDqgIEaU6uF3dJ15ZgVOtV7VLiQJ9kdUA0tlDhByGh6jUfoVJLQNqWJbJS/FUt1hUVCQIe+csD2nQWhOiW"
196
+ "dH464pqwsuhkMFpL4ZLunrbRlcYaVBIsM/oBGfOILBggGXHT346SvXsM6WI9/ohiHNl+OYGspuktz93dP2/tvKkfBoK7NAhOI4/DIDcOkv2v+8lXYAEU3KqY"
197
+ "gaiRMSTfmhEh2cWht6yP5c4IZ0LKihmW4Creer13rMQnmxef4G/9lhXIk6r62TJglLa5VrWyKQTTFFGXNFeAFvW7l3svBSWKy+dAaNdLP0YoQiL2gufF5Ea5"
198
+ "J5uDKq8aU5YcaM0xtmtrtm2w8zLqb1DaWRUZk5spNJDHYSM++Su+Db/OVTy2l2mv8Uh4KwuZ8C4HpP8hxF1waNe/gpKZPgMaZLGHOqgJfHEhWiZmsMcIdTtx"
199
+ "sbIhJJspBubJxSwXYhhcW4BESudQdUZzu2k/rOl29N4jm/XJQYKpDSmYBnW9NjWA6YYpyDQxLGsEsU0hsJlQYMRdNIJx2mwcNJ88zELRJqwWtA5Thtg4t4t2"
200
+ "9psR6x/khBGif1UMEGG0rhI5t/WZni5cJzgQX/HTrujQI0ReJrV/5zHye/DgCIBNw4OF6PYuIv5ZMhZB+YIBO7MrahOZ0OP+/+vRM9VuGD3v+O5+3ItsOAML"
201
+ "u6fdhXesB4qPTr7As7x65pxm6sY/yqAZrD6voEBWTad1YR7tRQ6l28sOTdWos/qIHbEiEsTrk1+OT3tP+q4g4ag7A+2gGh96ypArdHXdZ1+Ra+C5G9QhrgJ2"
202
+ "CwWIVXlYmwRhezAQ5gQtSpEttLw/HH14zarePyYnP/4IF0F/jOlM2PwYhOdXWngmV4V+f6BFazROxknsB5oOwms+m/X65z5pmlM7WJkDkjFEtppA9RP/WjXN"
203
+ "QexwIg7k1isOoiW3yaaeqkNKZEZmvLfASg7v95RaNUE4AqtLheJy2BAj+lw5PlnbkSNDJJv0gGolGwfabeYamIMWTFAGEzGc1UuSSR21H/C82zuWTOGL25iA"
204
+ "GtsTv92CtKxqe1vDD8K5FsWxtFv9TOwQARGropYMqUob+UW1QzG43A691mM6odbQrPAx+8ZoE8nHTxy6/HCbo8evL04DFDWKKBgPFZ2d4MrBMbR2weiHMjg1"
205
+ "2zmzHM82GD9dcHTIiQ4kCK7gVJNyOt2OP+mUMqoCJNkROjngcXo2Pc4XzCntOCzpUyHWe7h7ffQsyAkk9Fw6xEr1EbXchz9TlE7C3K2JvvXSijlCxcq1EsP4"
206
+ "ar342K5klBOh6UW1K4fVT/6JofUEsP65lYozCmVdBJIEJOlUupyVDUYB8rR+2AU3I7rbsRUasuLxDegzmTqGa0Xc4JaI5BXwfQI63N8RisiNlhImALTRNL3l"
207
+ "uVVQPX42KT49W6xns8d3zyhucURkaCpdkymroywMTwEwH/kXd/1AjPUMIL6MmQIgCyJOmajMhFvHlsAsXPJRUJNmdGjaFWe9sMpWaU0irUEUlbn8GWPyWAuS"
208
+ "xMh9ui5GcWuzqdGLqi3EPfUs9Pior9nurnp7wacGZ9XFuPgCAjdo7yN3Suam/Fb3dbD3cnL39FYn2hrC/ZqO+TJcN2N0OJhi/ovHX/3lq/lXk6/+9NW7rz48"
209
+ "7kMtRIN3tw5mbWQ7EXUmeqYNYcMrdq650/26vVtHncNWF+w9DWLkdl200nVzu+X1enQ1xdYOYdJ9hrfpoS0P36FPUxM66OCWUOrzhOhNuh2De1FqULLFFbos"
210
+ "3X5/jhcMxmoc+PBroy7nMziZJg7TbleS3Qw1RFoMBnPATdxnTf7PXM/vclZtu7TffHHPhCg8+tKBpsTw6j6cztT4ihN35dAdCna8/Ym+heG0v8UR6tcb7vrb"
211
+ "ruUv1MayyOhKKiIZDvT1jNaQvIrIitvL+HSE3nDbpw2lMnPtx452XXfosSLBJbrUivu9wNWuNNPKNt32gd7NSOdRW4JWZRs1nSnEL1cVOvGjUciiaq6KVeLd"
212
+ "7ZR1ks9A4MeoLa+1E7+jbnOvcyUuAsg33LmX02BQIkZX/MaxYy5dm/FRwufnLFVbrp9gyFRaoEqDCFpud3gRK60t9F23DNUoXvsDGSTmvvWAgm7QL7C0cQBV"
213
+ "r53fA4xwsDJGnwqyu7uWnSGwV0HAH8xMU7agNnoc0JoRjxw9ptE/jul2Ahx4ip0dOLZ0EHDRY1i4Z+xvmLi03XdM9aUW0Vre72RZ5c1UOOkBlQz8yb9VsgBM"
214
+ "Kv6ziYXifIor6QvIZOUwUEjVWOS1NJnpMHbqWj7GK2lHC6cug4l7GB/517NUMMoMbtPqow5urFjaBZ57Tfh+eXYyY7uTYN/P4MJnM35ld3VHm4gyHWdpe6cP"
215
+ "b507b2/DlRQZeMSG1LGsi7yH5BTggI+cDk7KwAU11lCD7XQAzu1AiGy8TD9w5HrX4wmCVWBYm9vvXUwa3p6LKZIxAIb3UdvzOErLkcHgA0aXHB7/4DNDtIUy"
216
+ "mPG4XtB19B5UwwNeiq2wkL/c6I+Cfakf0r1NwUfwBj6EbZBHoeltnEHNwQa2p37YVmBR7yyODVPUiSep0W9Fl0QT48UBYBS7CsLx48icsp2ji+tuLHdyn1tO"
217
+ "FXm5ce2mYohZsCy9otuuSA9AXp1R9RBfwG2n2t1eXH+obd4X22pQcN1iQ4jJ5PBqs9ld8q2bEgEpaKP8rpe5uCjn3CT0PFECPbT7FcvtqLQptE5XpK91xXkt"
218
+ "R3moN+L6DptWt0l3REJus+3+NeTWfDW+UoBNNEM3zxHex8gcueK33+0uznb3TX3Iu2vDkBwJGtmO3+823baxQ93jY1cuHoE9QoQ7btzldgYwFCvaNjoL6q+5"
219
+ "u/XsxhUMo00YpyhExkxfxkR6FrTiMxUq3uIYVl+tm3I2HFfLm2ZVFO4N3IDrRpwApNzbfQb1TkmU9wAfDXROa3nSFLcALP0xnyc9nRMhje3zv9wJxNUstBsW"
220
+ "b8vpA+EQ5sMdiGb27tN+qxpCzAKNRJ7ICA0jsc/O88U6n2mGbARqz8fu3g1p5q4HE+kgOmrP7WBrboujD42W44LaQ30KQnGrzcnAm9Kt/Ay2wEvrlqzRnK0X"
221
+ "+ae8nEHcUUx2ojZg1jryBn2l0Lio9LWxlSKcHbhVRvKI8v/ZTjxoOfsZ3YYRK+mLPM/9yrt418nM9Uj5/26r3nRy8RVwJ+8jAGmvn4fsGbHzxTZTT3vHvwOc"
222
+ "5XheNFfVxOwm2fIPrzhKN9sw4BXDuZ+dTCzNWNBoHsze0Pp7VKtJscKoxWz3wIFLB07iLzJVMbk5ZMYV7HIxKT6DDU+56IHPFTcKCQj3Hes/9+0TBckfXmGp"
223
+ "fohyDvdPhc+wC+lfUl3UxQp2QfZ+oZA5u4V0EDEaONbsAcY2to8n61WOSUw4A9MBgWULoEqBaikEeJkv+fCxrGbl+CaWUKc1nMP/qKuVS+yMaKMZ/AI6wPvq"
224
+ "St0tyjk0coPOwKXLDWQdENo3q9F8EAa+gDLzXnEcNrs+hSabgerG18e3uyFaRygt8Lmm3JgnUu+BbQLftjiFD987uEagpl95K0EJNtH604XZuM9x6WFdNBx7"
225
+ "3V4U36YyzPXewISr5l+4uvUPzQsw1vX5Xay7M9HeOVu70BudOES/FeoMadEim9KgxBrS75xmsBCEZue3UYyc6UHFmuVX8VbxpW9srHEC9nTEsUNbY9mDReJA"
226
+ "IdFXO0ENL1/m+dnTl3/4w8F5vFvtruvtWj7bNvHd4xixQKGdsP7llI1KjfDZSkqRJOspoNuXJ1sqUFWtNHa1qhEP2MwuHk36gHnB/KQdrg2mF68mLihkNdvb"
227
+ "uFovtJdqAEM71UcAkC0FYG3T0jQAC1xvnf1BPN+N8fMThgZiEdG3yG6Y8fIDszQd8j2GlsgafuaPIIImv/UWVEVb9/AQgZ04wCbQQz6xDeRe2y2Ax9reDLcS"
228
+ "kI3JGgjLLYB77CdGhn5LLWC2tkTSHmzPUvrzGJ3zztGst+fPsxkdhKis80pQfw7xfzdKXlFiaI/6zQtX9RAS8LcRsnvKs0egb52owelJPQwo7ruQUH4X6Wqr"
229
+ "NA1+Z6GKxZ3u7yIBLAE/Lnl959V6kkBehyiUuyR2aNGaCl6LvtRljZPs3ZEFcyZvs/Qz73oqmFN5WdZSx5suTLjmdEVT1tqTU8uf6Na+AI069e2BOxutPbl1"
230
+ "nOctod/sIvKFbFYTjtp1jJj9Zg4ndn0cE0vSJqSLxt3ZtNfkGHEb7M6FSgEOEZYgACvmh6t30l/tUOlCa9Rxw0ZLSKwfShYDsh1uyoFk5zABSiwzcBLLkIBn"
231
+ "6sIpzL69Ow874i+QD0w045bLtL6BCg/xJ9rEFn3KMgYDLRe6MeSrdoUHGW5gfEJJgaC2jzNMkDOQCXKES5uTIOd+nVBunYHMreP4R/59Xa4UZWjRKYDtTCDs"
232
+ "3OCGEfgtBsgJ6vR9hJlu9EAc6HboAssHzRezfFnL1rUs3yNp22w5Tm4ga+QnArIoUeHF13sg6XdJhWr/8zAHzN4FQ5bBJ8FtiBymKg3jJHpDhVYkUQGrRDU7"
233
+ "0YYA1gBk55NzwCh0H91cwl5mHxio3PP3ovjcZJpf0KjkNMOR9dyXHdTUnBlegvnlTCPRomIGz+Mnqx108tKOyijj6Yt3UN/tumZbFbS4G972dGfRY6PAtR3u"
234
+ "7kUM+eQTyJpIDNaYVnbbRiN3j+7dKQSULRfropsCI8coo9il8MvRCFUmsLH2faLwxq71DGtYRVJMqzXstNDYnjJkSOaNt3mpm1vBRpdm0uAh8hW8GaK+kqch"
235
+ "8i9xYxFezYN+jBtjrwbTGP/mxvrG7ijuhR8qADv0jS0+IdblQzp3tEcf2NbXXfJ1Zxo4dgzrKDEyUVu0Cm2BpP7QxkKlxG3+ubY0atMTOPZP2yjr7+eeaLCo"
236
+ "bdAAUPtQj8SPiQy4Tg/k4MQk6MH5npCOMUM3XkKBvTV+YsT4jgVUUdhevLcbJ9gygSFa1M5OEEd2T5ay0TzvS3tbnbxP3h/9/Pbw9RGZ8YtI9Btjuw+8OPGh"
237
+ "YX7UJH8XpuGETNvSi6nt4lS2JWaNfAyy1VqHKCTlB92dDRKZKqQtXgI5YY4g0Xt1D+cV527qC+0XO3huYJoVGgMmUNF+GXLkgyADykDkLPkCfhrcPyP/jMKv"
238
+ "n6v5+/u6WKNAgUY/AqT+QIpgJLFR7f4W7hrwIURiJg10ENb+0rLXWx0IPgAswIgnEzKsEDBGQH0XkCv1aemRECjo0XHiFrkrB8mTJ9OymMEV8eHiJpJ6Kp9B"
239
+ "ivQJ+kUFBnggLnnZZVKS1G0WmRTtB0ximFTfT2lRS5v5pSKyQg2B7EwsGL6xw+01lVKqUCk6VgZqJCa/N9U1C98NnoDpGYx9ACEi7qOsGDxkaeM7WUZKVPBG"
240
+ "OdKVu+xw9MXPNL1Vzd2pIbjkRMPQpYggOeUyQTBUz2tYwr2U0NnHU2AsM/qAOA3uN9TGo3g3RBwP5hDTwMjz9vHgMUUeECjo37HURN3irGrLDEPAtKAitGtY"
241
+ "6EBnvRExBO6bNK19ESPf0j3JZBwSQFlldzCj7FzYZ0fFV5G6BwVSm4nIRjYy2YdEOKyWzAduliCTYhufsT2dgSgwqpur9bgqMS4ZEextSrgAdZ5mhoAQ9Zvw"
242
+ "mTJOMHAFfpNGq5xWw/k98JJigOWR8Oe12Bh5yVR6GrrOsLJCad225fnbnU78xCPEgck8TG7mJZFraYOruL+z9VxS1FFSiSAHPpZEN97W5S+uOlPswkiXFK9F"
243
+ "U4wJVufFs3u+97DwdV8g0pwGcfAQvAdrhhiSbvu7P0YWEYeTa4six/pRF4+s3hskIs7c/p4TaA6KAFq3DC0n8AXIDNB7ZpZWWygyiauOSGRaDCYE2QB2s7xu"
244
+ "zPjYyRlenT3dPz9LzQttgsvsIoYWaXW+9kLbCV76UILbYHCEe1WwH+lFt41t0Q4hMKFVJ8iyuS2FzttjLMOHKfWhofHM+gkXzi5LReMqXCgSe1sQdLVudHBB"
245
+ "YAi+dIZZpVNxTLiPxOmJcpCFXjV9dvD01bm3SiAHPawSlAtblgZAhoMF2AgpB+FMtS5EfLvLUgRw9TKEtuirv1JVKS8wKR7J/gWDkgJcv3pA0nscRf2zDbnd"
246
+ "xo+lbnJJy0j+lYORIt7jgUgVK340Vpy9dpjW+/UC4kwdWKGX0hRnmklzVvMDtxY/FYuOEpPzcwg/yV/dAttlI1czuvYCivNpT4TNEyUztMSGaBvqOK5D6OGP"
247
+ "eMtk7qDKfCOWk1qkbZlKcOUoCOxoo34p0vFIYqMjToprYRucRr1uyoWLn7ZLOVnozG3DvRrz/VMsxJscVeDDyFzkc9Awp+NZtZ7UGaUW0jHaQMm1KrI6nxad"
248
+ "bdXLYgzsBeNFYjBA8J6Dp+ioQIntFI7QrilkCQKUSIRAP/inDZgZyZa1vuDG6gIcS02n9QhVDU5j/fMutRFcxMKwyprs6oEu1G/cI4qVfryDaME4xf1kqgQp"
249
+ "uj9bL8BLSS1ypGHtn0SDAMMJmeHd4ipENg8b0Q1w9uAfb5ZuakNXAuWw3Oinu2f4wT30RDMGkMNnVLFHfzxxqDMfvQcQbreSDhC7kYtrJ1M7zxNwXEWbeQOZ"
250
+ "JrANhdiWbJPS/O83o7aclF8O1g5KMCky52WtzsDjK+FBrt/JCFDkag6pvpA0gkD6XYwjPscBF9b6pq7FTtmQ0CrBr95Pvgs5dQSXakMrJPsNwFAYnhXTJpK0"
251
+ "iOwjZPVRp7Og/kTVfhYYs6wkEnGind7appoKMS8FRqPJUbauZjfDMlkW99gIxgiF48MBcQvX4YR6VZvLGX6BF/qJoFk8DSF4IwksFx6iyQQpLcXb3yXpMO2f"
252
+ "xyHAYXuLQkAUwxRvcEyIZvNWuwOcPZuqmmlxGL+zqhjECApWGnFyXlWzQv6GaS4+NzGXKJuZDFu0o3LnHkWJgPDHuTHZCziNhD/1x02nHDSiZb9tAKGHhdEY"
253
+ "DXIYwChG8I8ZwEgHc4cfUO72rt8nk6MZ+rX2dHusPg5aDS4gCBD3GpQeDrB2a0P25nN9iRYkcHG71KpV+qGvKr7IJDnXbg+dHRfoLeaHNKtQ2IRs/ALzE7Tq"
254
+ "zw9FgkFgIECrU9TMwFVVacd/5A1dWGd7b7w10l6ByZMnH68VtxR3SY5k/EBkwxgArn6IZU7+xTjWYHSijAegTj4U/ZjkULyIOEiqi78Vflxi7iU1sZSxdUj2"
255
+ "k88W63mP202VXAtsckxRuUH2x/slQnbfnLQ+NKv1GFI1TX6G3fnPkOMkbyru7MeT99+/+eGHo+Ps+PDd0QfHgjotVIukBynG+hatJGd0dURDyY02gUptt/D3"
256
+ "Qh2pPi4V1ECh6eWsusiRmaQgwMI327aScRV1sAtGWlfgaYR9NDdLNJNN5wprJZeh3EPpejVToiKpXdTJqm6oMoa5QO2MkoexAPbT6TLMN2sc51leo5PT8Kr6"
257
+ "R7FQp18vASRPzo+/vH2bHf50dHyqhK73R5TbmHa8FTfGhqpwA4vbJEZ9SA8vyfz5U5HSNKLMdvL27dEPTp7kTm9nsnCZZBTWnKI08CJCBajVZ5aLLQLha72K"
258
+ "NTkFumryS8VRiylaw60a/l7A3cCqvLwyj+lHgX4Uuu3hB44y/Q5Irli5h6V8RCA7sdkHycUIgY9HbO8DOrNqCVcrsTtTBS16Wap+Qd0Q7PU0xt+R9aweSfLU"
259
+ "GaAdyVM5xGD9Y1viiu4TLakiy2uznWDKAz94+cClugOPyiJ3dhC2Rc1MDjnn8lVdmCTQ0ADwqBEeAU153b6Wl9RyVqv+itiZko+KuSNaOeDcOdO/UIjGIqrr"
260
+ "63z2sQeghHgXuzDUGGD5N0hou+RsUj1elJNJsWC2ksrgxEy4dZIvIH2wqqemckVKI33GDE69HYD9qHalCHCuKgE3AVWRJcMOIdepNwLWOF1j7j3FoMm9qKGW"
261
+ "ZsUndSaiQ9sosJzTn1YRn4+IQVOK7Pfhq4TDNR/Q5LBjjrTtJqVaN3U5ofhgpHdJ8kvQWqMx3eZ56cHE/IRbBU2SWgG4XfjpI7aEtR6r3cgFdZ6TQcV8jZaI"
262
+ "hoaQzKmzhKptCe6P6wVa9P5QTAnmw/pmMRZP+9paQs3VpFCYgB03A6H0XmMyTbjjmnKHtRnhGgUOLlxvuSoOlfBTXqybom9CRyPgIBSZNBr2aKUIPJZub4th"
263
+ "5Lqj+PTk5EE3WS9ANWUKbzuM14raaQTe6yHgiWkL+KCdGngxLCdGqelJQ/caJIh/anxTMUB4VCemiJm35FZCcSdF5Hy5nOkgvN3pL/SNlLYBDESZZr2cFWd+"
264
+ "Ne/0uISNWrE93ZrObQP6HN/qNFqmLbOMzrCBxbqSF4UFnAOdyEbDV6KRHKCt01OoY8hNttTqKTs1eiwKejJuAvabA+sEPLHgnPAYTV8X6wllJtCGdvQgdWAE"
265
+ "3RKnxvmOIwlAIcji9Zl23CCvYSv8CACczbMZaIncMZA4WIO6AXI23CSMP3vWMZHPts1z4ggGxtIzIjLbKk21VqIe3KUXJoUQJAQSYEixNZA39fVyZFp16l/F"
266
+ "mKCUobsrVkDFgiQIaiT+tSqXvb7JEJH+9a9Q+VnIWzwLN9mazoLk7a4RiDsnE7ahC1Duk+Vw9qmsZjkHzxTTqhggkK0iv6aCzb2cQmh/te9yfcRUCL/O06Tn"
267
+ "Y3u4JopRlGN0IEVym/JZT1AaLQ613+DZDMSiZaEjXMAkggokAhbDMswnk54rsMLHtBHMsXlDRz6cOlY4ml9OU06KKSb7aF4oH2sChpFpnDAQYpDvZwNwUZFC"
268
+ "Z994R8WsratIL9Us7AGcZTuVroviOqiknm3U1AJJQ4cwh7OJpnA+Wu4mNhLxsFIps5mDU5+hai1Uco1x7LRNSgifxTcfHPUqVqAOYMiDZN9HtJ+ythNkTv3K"
269
+ "dO9Q3XpRr5dLjP0V7Ah2Kj2gXU73Ox0uIXZuH+jReS0Q7Z4B3QIn5UJyf3E7CbcY5/0X2mrgNqyY1Cjsc/tPoX3eEQUa1gudpdnGToPtQIeQU2fsqBLF8WCy"
270
+ "jWwJfCdr5UspTnpseJmcYdo5B5xkrx7dsixyIEAR9qCtSag064maeBMxOFqDoJHBJuyYq7URpyb+vliMr+b56uMQbFJokL0YTbnXxWe3KdrXpbrBlPzkwY0g"
271
+ "ncAxE4g8RR+ChlwkwU0FNiM+7Xl3fVBUnaqApSX7d97lNBzZ1YFRRyAYoSkK+DKTxaKNUET0RPzLq5SCzzvysv09xzYliOqGYZk0XMxAIUIApRLbckEEXCR2"
272
+ "8UksLiwqzmp4V44Lp6yBKNUPQuBT/7JUbZUQyCBozRIladjJ0QPr3G1wMzIRFv34GA6POHB5iuebplcCyLW8oQdOcpCkE7yXaL23lPIWKXs7OVpTrwSRlNrs"
273
+ "3ZSCqFEHdboi91IJJIu65xmYBW6FKdBEymEMIXPhGNBIJ6e0WKznBQfhMN/hagxIUnv90Uogi1Shz7YiI7wyP0D3XSwIa/BdHWhSuhEEDXf+GVx888/wvYRS"
274
+ "6l/R5gomA0yp4C9soEWDGZwb1JhzkH/6QttrymrGtF5DXE31L1hgwfkPLLDgr2j9H+VSPVX/qjLGvEE9Md/V8z/DlQKuA/XC/kAnZ7i5SPUVhmg3w9mYZHj9"
275
+ "kGWqiPdE3iAfAE7QWEfkItPae77B8Phbu50Y25HFjMsCKzFWH450rS6Fu2CorLiNH8XHeV14970e72IaAuvIV8OYNSSbaqIXlF5wbYc2Olzi/lzTyoRLT/zB"
276
+ "l57CUX29WGDS1NXjx48f8U2Aa3yjn4KVqP7OFyr6Z31T669wNgEAHj2yIMtghDe1OnNNygUm+O0Jc1M0N9O1h6cFNJevbn4oV4qMqtUN+INMy8+j1OGOTy/0"
277
+ "9D9N0TwUWuA1DpHSRxrUIeq74S0xQbToLEAI+FRoVQrdVIwb7d7gqiXoGjTcuns6jSW3FuQ85Mb1dWs8saG2VNObgGurRtk0dzNEIzsFBk7alW20I4M6fb5j"
278
+ "39VQjLZLXl0sfVkrMEXsJCyLIKjLfPwxv9QntntYgEnLr4jJ1zZWXTpmCXr7eRYP4R17ykmK1eKRv7RRPTkbQHWOrluzAogFAugErrqRBvELeFNoCExWEL42"
279
+ "9m+hoQJfQLOV7cNsxETIFYZPm0m1NTDa57nSdtIExlLxZMTgnllgwHNgcMiAnEVFrKnP/EhcbKFphmrTEQ1uU7j9hU1Bq0gBz+Y7RAuHiceYT5A3HM/nd57w"
280
+ "s0MjF+AB9oD6xAACKKwaMDA/tBRliHdon2lTjBGgLabZIOILq8LTjspE6YWo6Fgr4b9sH0IhemsbVaAwegNrNhL2wJYepn3PyCb9/vDDUfLz+5N3P5+mD+tJ"
281
+ "Z//WHQWLsHfmOtVHKOa8q4MKbEJCsR8vkGjhBO+8Swc7oaw53LYCTOOWVfSUspJ8U3GaB/LUjZbFsxKV2tgYpTuPdBy6V2gew6dMd87VIzvjNGcIRvWx70Ul"
282
+ "Yp7zu5ENt6sKCcdMz0oXJAT1LPQK3BUeneyvYBmY8qqO+2cHz1/u6SDH2oCCHSvggLSJEzLsyxWMB+WmyXq+rHtMuoLYEPma5462a3xgg5zsQ7iv5Ilu4RkB"
283
+ "iwcOOrUe6I7w3H+tXTfuFIQgKmrkBZxMhoezxjwgWIdr5wy2bvKlgb1O9fT0DTDVpx/wXzAuIgH1PDz1lovluhl57qhBKVzLYb4zBDRf4nW8EsOhpXghltNH"
284
+ "FFztlVZDeOI7Kh1AcI9AUCw+qZP5z385/dPJ8ZuTo+PXJz+8Of4JGA+Jf2qg9PL45JcPsMGeHsHL/bT17M5ELZB7SuAcfV6CCNEVoVfTrSYDVK0YKjbSZ2LE"
285
+ "asTABG7zU0sIZ+dOJEgz5UPqDIxxQLRoiRUcqkMEaOFLCWr4VoPes0Co84V6itFS5TPw9INFHBkjOyz0z54+39vbO4hQmx25lzpSaFTY1FKcdvz+9XVTq79r"
286
+ "pxmrvEjtmk0xSV2HV8U/lCyNHKKH4fxIdaHOwQU5YLeaFK9QhqUGhu/xTw9qieFUdD/Yc1WF4BldXNP34vNypqDG7/N8kUOYERlOpmF3U88LXmEF1K1JnqAa"
287
+ "WmFsWdUlHBBR4zO74SuhvFypMjCxwCubokb6rddzNeHqxJwoSCZgiaavhopJ2YTKujT/VJUT6KTIG2S6KKEmfJkPMQMwphtGR+LTTV4uyP5wWi7K+iqpFrMb"
288
+ "lk5QQVqOWfPs9nSVo/o5XyQivw/2RiaWZP2jAMA7tqd1UyyTJq8/0njHlWob7MbmareZqRHBogUjw1lC/C2N6UOZ6nxhG7fAKW4eT2/LCcYtvBP6XzX/w/FV"
289
+ "BbGhcKL7UgssXvIk9oXmF96qnQlo56JUMtmL5303hCJe+aou4UgE6i30SvxmwM6da0gyY+E/t4YRE7XMVhAws1ak3pbORITr7bKdsMp0GxJ4y/K+ogdfxRKV"
290
+ "YJJ76UVImWlcP0LW7Mpi0LZXCr0l8otiJvUYvZ4N7pMOgpGrOemlQrUWDDVQg2v2ptVkWvOl7yVw7J7LDg7yDGE7lxFKOQ6BVetT6xyiFJkZxqbWoan9BASM"
291
+ "F9sySj6yEeSBlCoh7jBwS/2gVhq/gNaXmgXFL31jHRpFO2UVN40z7rH6GtTu1eznWb6Iua22UCWoWw6Sn50UyyLFjvA2V/s9sK3VQfKa1QNnZ6SjNrSBJhba"
292
+ "jieWNQc+yBgybZAuW9NEzf+oVbeprb/BjUy0LQHZFu0QTGoiqtmncISbarOH/idjxW7rD4fDjsqBApi0hqgtRD3YkEHy3XWFP3HM/bhnWvPrGQ1zRB3dc5r2"
293
+ "aprBqbpt1vs+lFbGCe4DexIcrxqjUxvYGMQ8SzgWwhBjBkQrbedHDRwkKBkryA7KNj6d3yvISBDnu8PCCAtS+FXSc2p/bCIL7ZCt+JNa4aPn0isVq0aWIARC"
294
+ "CJ+61dxlRroJ8cAt7K4jkCOdB7GW9WIxLesH0XmR5OM/citgjBUdrgAFbF4ybrGsbqqlM39HGJSq7xeDy771DNQxWPJA1DjFb+7ClEcLDEjqUWRLVtMw7LGo"
295
+ "YxyGIgkiLLy/TV4riR2sgXUceiXFw9aIYKxXnyDJYp7wsQv2RDB/HSaHSyAAddZXm6hsTIe2VxvMBIrPwYLwEkS1BZw/buDaV8mpOB6QLm0cY3XuBbdDD5Ms"
296
+ "52UgWK1WazACozAbMjJYayFoQ+DM+vcb1/p7ZUwMUurSacqmjXdz6bYnGUzeHCe9xxS18PHg8Rii948BL+oHaClQG6G+XxYLNK/BHybwCszVjXrANK3ektmO"
297
+ "emSYIFWBhQXfYjEczaeXHpnYOGNBFAKr+ujA44O3PY80QPi4rlYfQTZXWFH8WnVqwlZzKFAxeUySOFNdIVEEq42ese3NYEDuiKi2RQJ+fhaaOv9UZAKkgYly"
298
+ "yJe7ibZ9yeBkRy56JnLXE6tMNb6PO49HJOe15cJQSAEcxsNCGvul+i1egeDtBxQJKu9ixc6Qw59ZSVYuhSAceKmtJ/4NrgjhRpTpRqGXLOACX/fxrMjhIqBj"
299
+ "GmkmTOrNkbaf8jET3QEicSO5x2jxHr4VunH6u6FzrHRmca4Tp3XNiJ/TbQNPxy6CIXPHFOQUjSlPptOIIaXGjiNhxI0aXUkD7VXDqqF+3FQ1HNaGbXTrkjXd"
300
+ "GPdw9GDkwK0m8amgKhgYCDhwoQojuws79iKmxDKwtLcXOSvJexOaATAKAjzfeb4J5Bu3Xrp8A7qQ3rtaJOhM3r1p7dMS0NG7Aho0iwf9nLTxL341zitk/N2U"
301
+ "zUzK96KmBpRrm59eC2DwvXKjrHvdR7KCu+Cxae3Rn0/e/nL65uQ4e3fyw06uJtpYVbuWYqvzdd0ofgPzOkhO1wvU559elYuP+E0NQ3ieunRE6LXryIc4HFAT"
302
+ "w1w4At2wLoGNA3ZhxNnP74+UMPHhzOvtXJZvQ3S8z2h/kVq+3OnPkI5zu7HDDcvGZDXmfSENcnnCxmsSnshdWHP9YMsadXNWDgTpdnBmAACEAJj+wsdiQh6A"
303
+ "pW3FeSumuCYgmq8Ggj+5kUVfDcs6UziFY3YMl5GDxxDYT3DeDfscBacNjv8y8ivMqmpJkQd8IyPDK8EEOC/mCt+Ark19k3ecFM4B8M0IFIN0z7Fb7Fhb7lT2"
304
+ "uKChySbrwsYTFsHj8TTI8eTYTg1hhttWZ9Ld6OJm46WLGHoll6B6XQX7svajh/XgXAasPuFVqmenCimGZpBCAdIXeQr1iRK44dU3X78M3l0XxUd8+fvkSbSA"
305
+ "2qdXN9mLbJLfgD7wRVjsDpeZtVB1+NPAS7BpRtCaGcMdMT/0EjnJmWADnu9Iq6rb13pyAmisZmhSXS84HdY5pA4HRMWmn8i/jSIpKpI5jBNtXudl03ux5y3Y"
306
+ "aGQtFoS7pHgc9nphvMqM9GKDSWLApdF+nxncWieRDNpxEKXzZkL5s71zvhi3wZyFOVQ5JRAo09YwzG4YsDZcNTQed6XYsOg7yZisdelZYoqGHQu2/6hhg2k1"
307
+ "qtCBJM6YFUDVG47RXTYwZxjIFAAWJjBya8gCVdHnXQzCrsBom0FKLVnaYHLmltqezE0xRWFr9k+ig3eesHVeK6BMl+RUbxaEngCO1OQnzgBZdZ4v1hgApf30"
308
+ "e8/D7uZV0nrWDZ/+xrqh2jPYr3yyxQ1eCQ3miAtz0XLIbd8xdgDSnosnZQ3qfqQU14HCQl3WeMCJA2PWqlZNq0WLNpm9C/gFFrvkILJjwOVyAephFLKRHheJ"
309
+ "G3VZAZXPUHuVEJeIwRdTf8Mnym19E9KIMrqvsz25hVydc2SknaOlLsT1NUb7IW0dsChFHNgBn0oqtSAuCiYCuMqIDZzHE9F6t4bJEGvIrRH4JgfYdXcekVVH"
310
+ "s3gneUucs/kXFpwbxS/bLZWijSDEkBp5WWX6jgTK4urUroOnt1T+7GD/+fmdjtAQ2Wu31PTDh1n693lddFvSteLAo1pTziw1xT8g2W88JGLXrhldAG4fejpF"
311
+ "UhqjdDG7CymrxdbCaltvX8H4bkKdzSrcKASMtt7JB3aOqf8+K5vixRB/tiwu+IBNHtwfvCdNPgh7C/QQgySESQlmIpO8vrqo8hXYpuSTZ/om4lEMfcKUSIE0"
312
+ "CFdu1zrfjdfRCdFu2ZvQCRsH3KGSeQw6uME9iBIvV5BEEzgGV2SeUioM3mDk2Bd9MndRQ9p8+A7zgdCVA0gTJk2S3SYPIjtn/K6AeB60c0txAIW72jTFZ3eZ"
313
+ "5kLTVEcSn5GNhmWO0D3yxrR/J1Rq5QL07FkxvygmEHuSdjQT/xo0eUYsySdk/kLMVhUhL1tdl/0xY4q2NE3fAIXMZkhlZNhhHDhBcAHbOTWGAm+pnFjSlLL5"
314
+ "qoQpuxmmYqEHGcFaY06bKmacbMfCqj0wEn/75vjIiYTmnK+0AxSBjjkE3Kv0IbVY27T0FHgtVtGDIqqQaLVijJuRpjSRVtcTKRLLwBWWMtlFtisu9McuHcTE"
315
+ "Dqe64yO2YXd05NUBk2JGY049LUknO6YqIDC21SIFeqQCaMs7kSOQQdEo4QcrcoUKPdhQvEi+BhL6ovZNYzoVGjgznCFXbp8yj/rCAsBNRmY6I3eYJtHlKD10"
316
+ "eIImQlzlhh6IlSeHGJyLOPowFnwcraRGt+mV2nW0ATk6nE4b8RMzZurfrSbM4bxeqC8LfUdh4hHv+wHC/WzQpr5OCx0N6myM1+6/altKbMhEimXuu2QDKo22"
317
+ "zZmrbUrqtmUc4lyswK0pV+TKDgnPT2ExakWKSI8yYvvmyOE8ELS2p6con8BtnrqLxm6mV3Y72sZ7ZGcZ0+pWNkqSnUMyzd4GahvvRpAEOSNUkHZ4x0xHrUoL"
318
+ "MpUMuaNIqQR0THpnb3hk9jHi/KlQ2GSpB+GsKzCPrjuyeXRDIquMMdVOF6JwKjcqdDDTcUSrSCCaVhoQQzc3vDRpW50ionfHrU3eyjTB+mvkcrhbQo61Ly6l"
319
+ "rdNBrLOWWSox/0thlqClXWcA6aLi6DScUokZDfWlUIXd3d3jTpTPJhRh0FO66JloUQsJQstB4w2GRTnbe0Vo7l+EFH6laY/sJt5dU+ta3xJ+1/7BZ2QPGwJU"
320
+ "EIerDM+bWSc7DG42WRjWkSGt9WnIcdsOyQ7Ju1wHIl7J10iQAmB1RKPTpA4BeQOxe3WiTxmrXv0An1b+BXGfmupjAZav2rPH5fTb6Ay3DbYUUwMeOeYmMxtn"
321
+ "vax1fEXWAg4dAxTpdeCC06PB63GbIcvRBqF/2tyqthyatLGYZdqzxjfOsVEjmWDz5D8+nByz1Vyb15dM7mosi6M3skZ6bNu2WeyK3aUtivzSyGfiOOzeHPJ5"
322
+ "XpdTO/myuUrP+w4+XVqJWEN3upBEyhstq4BKQ9oV67VjJkMsB7xE92Bl+JpdqlYTdvnTP40jyB5uVhgQKD2uhCV4YuzDJUF3OuKJ2c7y9aRsMnCS69mnImar"
323
+ "O81YGk41H4ubA04jHWaVRhWKgNCNY+Illr41A1cj1FRwYQghvbM7wOWqWi/xJGYbJ/EtUg95eaCM3FitNXwsDv0sVuU8OObVNv9Z/yByBLTYC161YFOHtnFR"
324
+ "GdO9Amp/M7Lk1HE2g77MwVf3APbihOdoL36oXM5l7EYKCLC2yq8zBihbQo+wP/I9VUO3iPN8UU7BaRLQmS9u3AMiBQWQjuZ6fN6uT4HwgqGQGdN9xhLwThyS"
325
+ "tGeoIHA/6Cr+xZwA8ZVk4/g0thMb+HSU3bZAT4FTXwTmTW0Efn/tHL7zctBl7m7RULsQLkL4pP6A4ObeVdBiUCUPQxG9CLbmD62lNQ9Xba15nUolDj3ZCIWu"
326
+ "5z1prVdz6TpW5i6mL7GR8mgC5MUDaOu9cHpUiMUmzkYS0TeCs+bXmUkb7ce2pqiiJG+h6ZMtRN1k1A+WoKRpEKoRWwxziN5/m88V8ZF0Rv0RE5BOodYhNnLA"
327
+ "uthY3brLRqobccJ2cpDkMkgqRDFxnEypbXcT2Ch5EnBW8vwPvFD35E12bRXMsq1Zas6T1anNdgldB6RvkWeMuw0HF9AuOl4IPIxFS7xyy7gDOjWqcAk3jQB9"
328
+ "fyPm5cL47UKFckHGYwNdl5p69ix53u+7nDsWOFWGSrANE8jyoqJYBMIHuS50S2gYaaZNHisnNnLq3V24veoYWwid2PPNV1T5zcql2njd9FIYkeYSEtJopNfN"
329
+ "KnrZVXtZpLTd+9kBtXBue5tSei8Knq/AspPv6/HVY5Qe3bQBHvFsSDIQlG7LDaBBA9FHY0zDEDWgsa7eYGPZkuoRCQ1yW8Va8I+b3HMr/4JPe5IXDKUAnps8"
330
+ "k7o5zdkw+oIa/tf+jYmEBkvt2LMIaoFBP2S3MuCFP1EdJbsmCT7CwN7tPVLWy0CvC58dfNOS2ZHo2CKSAvk/fxlBG6OOFk30bTu0OqAUVg6bRhqM3UfBhwJw"
331
+ "tMz0BPD3DZmsTtVuUpeXi2Ly9FYQLIXriDfNITzijVOwNdO8DN7SJhRx1A/60ibKiMk/aMFXS1Vt1tgKsinAtB+RlEKaKReXxQpjbIFkfE8ODcc6NRsRTWs5"
332
+ "dbpAXWGxc+ZWVQVVqqKpOB2d8e6gHezAiAHiZtZX+fNXX8v6Q4w9WvR05NH+8Kr4PCkv1UEPmPk35yCXxTsxBO0GxjOpD2HzESoNwBxIuJG7cS/4hVW42of6"
333
+ "oNV2xPJy23Scrwz48QOZkTnEaaw9L06QHUeH7xdqZN8V0D+EDChFQKaEplyfloyj5ZDT++iSPExj++d23HnpKH2ehSAIl/e6W1Nk6AbPsRFdfNC9GC/2AMTN"
334
+ "kqE9/DjT4VXU8cBKxOcxvHTWtdKzE8KFXNYqPk4g3TugPEkgfLxxnDBqTfUu82s7ak0wRytRLPFcUKY+xN8GYASiE7fkeFbpVzeZlja9uXCEzfbzFsk7cNwC"
335
+ "GgaW767bh020gNA7kT1gyh867W3zEMz93vAPr1rVwOFgWzbhSTHW1oB8d9m6pxojKwe/dAzTZEI3yXN40lzli+TVV0lzXY59syvTKJvZwJ+OEhmTWauFCpak"
336
+ "PFUHgvXEFAqJse1xUWyNfFwTHzDw2YUVbmZaHJlhW+Mvex9Ehqd8C66+Y3h8NqjUBdODbg1Jlw0YjF+vJqli3MAqQ8a9k//OhlM7DtzcpFuzEvDnoVtrwMY4"
337
+ "X9fSy+cuYje78+S1T4S5zH7IVOiQhpQzYtrcxfYcJMMY698bvthTDJE4C1R3+RGW+P1eZCfSLXocJtqi5FB+i5cQ5mUUNPzUB94iFweLDlAbVmAbCGJZet3K"
338
+ "CJVbMUCH8bERRgqMF4f13cjbU4HbzdfzDN4CLii1WpRhOroxM1R3/uxQg5kTY/QQ6SlDHZ0bR3dzCyCwBzgiPwmJHM5B91iDCI4OP+ZvXiHe3mHTBuCKoEBL"
339
+ "wLkwo0onR0ckZlbXCD9pselneCMn3FkHjCkZSYhNex2ziEgmST83scc872EIDh9jA77JkNuttXGTsXGF0q6qHosTtQbupfE29+Z4JPc9Oc2tr/FZun3yRF+B"
340
+ "p5nns+B7WZr7XL0tCaOBoG1xpe5CEQZYsQiwjegMF2T78gxdIPF6kMLADQQ4u86GCezktEKGof644tfjMUOj7r0Mi6HxSDSie/qXap3kK0pdHLGOFv57rH5Z"
341
+ "DZNDHEdyfVWoWivK52Tovr5CtxeybRsmoTNU+t4oWtWiQmuRYXIMTuhJDvHck3ytWlw0HDtWHQ352hZTCfFpu2IboIqKILLUqoZQq9VnjPUBwIIHx5jiVNZD"
342
+ "FxIfkbgZcpjfD4gvDvbbx1MKEP7+IHn+5MmLfbW57bdJ+lJtvx+/Q/IuWOwtEeXz8qbYeM3wrRO3/Wr/+VY3VKD297oKjcyXzVYHtOhtmROytuvwRhcQ25SM"
343
+ "6LXCvSMYRXjtEipRmXgoKmJXQUjDvMQYEFhqk5qElSWRhujWQuO/XEwKEe7Xn5gWC8vNC9xOpNXxxU9HGEVZyTq8hDGHIUuxYg2z2wy4pBGXLyE9uLBhSd7r"
344
+ "2GtsxAqkG6YLtvcdbSc/DzeOSEfYUrvJxrq0hA58Sm87IWofBSEdUqxsFMLPorXgcyvFxVV1rYUMftaXbjfmNT/pmxg4s+pSvrcPIz77+kOedNfOJVO0cJfr"
345
+ "nnD1Iyf0trJBbjpTzXvT1oLOCmxz6IqMeL4aMJZRuc0wQZpzYItxtyEsa8kVT4T6R0tp/3btwOGMbVcCOg9xxrsMHgmVhEjaf4zU/Obdu19OD79/e0S5hU4g"
346
+ "x9C7n0+Oj45PP7QNk668MfDGPE9jdlymqDzAmNX4T21Z3l7P6nFgH45mbjRlJd2mgCMItNJZAeT6aT4vZ7ykdCctBIOVwhk4ax831qCknUrgWKGLLcSQh0D3"
347
+ "TTktW69xLAI4v2eO0g7+2lDDJv8ELop3UhTnkqLKNWAbWazUdGxsyb0kOks5XC8MgHNjzekU2N2MuDAy3zEzQHJRXOWKx3UhwU9L6rSsj1wbJkCvww0TpZuE"
348
+ "FtN/psO/VUpw2IUbtKwTp32bh/jAZJT+p5PEeptGZpOtloSpAHmld6owrgwVbVXpLrwGNq/iNSOPN0ZNjJ1WbOjZjV5AO+oqHU/w7SSAuDLNCsdx3ab+GCMN"
349
+ "k4XaOhdEceiZ20fLbAQkZoZmC24yRYOPL/yHpVpmZjuRsZx6BilWlLE2OdZJyXos3emIKjA8twneV2hU2nZmx9gqbZ4Hvl8FhlbRmj4jhlqZFD368kRLYPcm"
350
+ "4hY9r0PGW5PnwNmzNfLO7MPzDgI2N9P6wX0WthtGOrV3r6SJuLVLshYQ1huZiIfAoJuWI8lDjgARTHqUPIg7E9/13fAhzvm33apvm+OrbyoHn8gxcOCYjplU"
351
+ "y7avhx0HNQLY47QdO3ZVRyw9y6loaGTCkrXxwxZ7LtEIGJJwd/FWxBHd5qnbdo+ICsOpHw2hg2FFaLxzaK0ReTavQj92e4Q/YdeR+2oyDTYXKaw67DBD8T8a"
352
+ "iDbjC3OlB5QcbSUOLIF2FszXuXNDZGZsWwJAPxR2Zd8+8LQMNs09axxNWrkRfB7CkbC+1BIIm+pNPMnU5/sYWRcfQfTGLYR2WY+eefcf8tNC8eD9yZNpNyZk"
353
+ "AfqWrN1IUCvP9PQFfheS4FxqjkzvzlEgGXx9B8yeXr4vauxaWe759mGbNNJmpR41M6fKLPNlOu+k7ZLjD3KMYvNcnbOLlRPuj57rBiL+4foTj2iFb9g5vdUm"
354
+ "Ez5fchF0m5RgaYdB49V+B2ReKM8uGPSdIBTWia7i62BbZqSx9yBGJOIDxNtjK2ajhw46Y1Jhj7XuLT6l+E964223Ww/HpA+B1YJXxE4dkriwS38mTIYOm4CX"
355
+ "8QEQ8uKdC2mE0Cu7+wdddATIgY8kXLYsiZczJNu+z4K/rE3+DpysmHBIInN5hcER66tqNhmGsOqPHV9rERr3a9ObRRqn/OxuPsDxho7UyJSUXEzLcakmlQ9c"
356
+ "RvvW1tlm9zVtDbCdKl+sT1jk9lfbnG2wSBAAbWGZAJ+Qb2wRR0Z/dpDnTfEuoVJbQkUiDm0fyrGLhUlTq00zJi9DWJTgB7ETrhf7Hj7biw18KRo0qtlnOG0f"
357
+ "i5vRLJ9fTHL0qNDGL/D97Pm5I3PRm/RpuZgCXwO1r/Ok614yFIgJW65AjPiK1pP0DXXEb6f8ljS3A709JBIcjoApRoQ3COcTw7ttcydlw7yFG4+9nTAxlNpO"
358
+ "eJY78g0nraEhzKIO+hazoQonNj650i+fv3fscBzLCbgvOplrROTrpsq0mG2szMKAPEHTlNIN73LpYhfvpJ4uIUWnzLfGAauX64tZWV9hzuCrUFT8bUL2vYq9"
359
+ "Q7gWDCk0TE7wqkKNDdIAYzf6pRqUEslR3TYpF0UdXlD+Fo0y8tm/ya7/+78JD//938m0XNUQlmWhOG4Dbc8VCPkqgeumJI8COF9WK8o7xxkqKXp/c1XWHHUY"
360
+ "XDrrppzNkkrJb9Djh/96q9b5MGhvwzxsc9CPR/Z7dQ8tnea07XHm7s/ld+XwYeC/7jOqYO87nd6cpcFHNecoBzKle5KD1d4WpogXDguSj9ox0IF+P4ydxHks"
361
+ "qUD0xHH3wPB3Yk42ZjEIcb8hpF8Lzu+BVB1TuyWbIIvuvVQdHJubJQZo7g+zDKJ6Z5l6mPYfNEkFBZ6OzhBSDcP85WZD4x7/CsRPgWnOfLN6N4CYOtOP89Uk"
362
+ "bq+3RVjxqOZxmzjjm3NWQEhuJ9OZ2m6KLxVC0U3T1+v/KyRE2BzWMTZLqNaGt2c6huM5qre3iZx3ryB+qBDgxhPTeGQ8Qnbzgk2GPL017iT2HuBkqw1uC7tk"
363
+ "En12sElmMqQruO6tiRqHaD9juml/tVNoPSdsesFaABlLj9QOMWtxL0p5LP74F4kyui05mkvckCbtJHyRGJOcN/iisNpF1XHZ1CazoS4epvuwkUuDbAyxQX25"
364
+ "MKhe0OH3GnI/CurQvxrbTOBCwWdFiC37emiwXIpILiC47YqzHA9ueu8FQ3kGZPzTaOxUs5jI9EkdCdm7lLbvNg9qXuSeC/UT4eS8HkNO4gOU5O3jyZp1W3Wh"
365
+ "lt2k5vO+LYCTRLW0P6zw2wYP0djLWCLP7rxGPNwwpvcmPIjRu2dTHvGI/7ov/XGP/AducSlI+aoYZKnVrBzfjKSNA69E74bJ86WT6ejtrS4av+n80EaYwiMY"
366
+ "/iB72gbWOT3FH1LCLQ39cGg2/Yu2tI4kfcis7GGP4yvAOtIpYsBrCBMfxnMi6J4EkYzJ7ecMDHWFwW6AAUzi4Ga7M9bATS1ztH07whx253LAoIykvtT75Pke"
367
+ "paboTrvWBjOiOVvxRr+eow82oVoDRNPS7/flkHT/zyQ07nLpbpWmdbdWH5gv74vnyTMZBym08sjOqdPVd6MgSJiXq1B0RjkL5XR7vYBJfi9cs4Rtz19RErUt"
368
+ "lZn7Buh4uP+c+oYvTrswL3YiO5q2pfymv9FNfyOCGm6RQZFXoJ5tkydwDl6G44ihhCbsM3Ek0Ln8QBfH32PnoP+Z7II8kkFLyj5/vZrdspgXq8tiMb7JqunU"
369
+ "JrDcLZd0a8ZdcMkzCXvZYyClM5PNh3hAqUzvHp6Yd7eQ9W7KH4MITDMbSePtTF9b4gubqFgnCabf534xfWy3joowr5Fzeix5RUwCEs6odnZNoqu2md3ufB0m"
370
+ "94N/iknMDpNuBFaUfCvD1Kqr9bKhkcqopffFq5Mtxy3EqXgO4o6yeF5oqSOmIZgCr0K4YHWt8I0/bWu8VezecV75JjnSXTZS1USj5g0rCONEXDWsTY+zugSn"
371
+ "1z2O6jd6Htb/gk4pd4/+L1BLAwQUAAAACAAAACFcXz7FrDQIAAB1HQAACQAAAG1vZGVscy5webVZS4/jNhK++1cwykVqeJUgyS6wXvgQJLNAkNchuyfDENgS"
372
+ "7WYik1qS6hnH8O/KPb9sq/gS9XC3G0l0mKZMVrGeX1Vpsiz7SfaqZn97lL1oyEk2rCWKHZhioma6JF8p1jBhOG010YaeCRfEPDHCPnBtuDiSTskDbxlsSsXK"
373
+ "LMtWByVPpKoOvekVqyrCT51UhlAhpKGGS6FXK//bE9VPLX+Mrydah/XPWoqV41XWUhgl28DpO/6/njffMiVY+04pqdZESHWiLf+VVVaFClRYrVZ1S7Um755l"
374
+ "2+O93+OW3qwIPA07gIxccFNVuWbtYR000WswQC1VAwvNDCqJK34UsKp+YecNeTwbpgvHCB+kLwM52UZO4wOeK+z71Xg73AX7YTk5MIiAZ4a3VdTIKc8bPVYp"
375
+ "ERVMeeBHcEwzyFkemckzS6uzNdnti+XjyQs/EK65gIiAMMmHjTVpIS4KApwYMIp8FINgEESD+1iTX7RROTfsVJSw4l1ekINUBH/B+NrN5QKxsqxYk4fkqsh8"
376
+ "8jzkSr53tLxxhJY9/IrcU1/kwUDFHlWaiXUtBtuyw4HVhj+zsW3XzuiJifGeLbk8PMQTDw8jD+cTUhDRKblxP1wjq4/J1xxEhdQ6f/Lfb8iJGdpQQyGXmhgk"
377
+ "VjcJOakctSaNhHyAhLMCQ7ZyTWratmVkixQYRWCNYKOWPjorG25ahouG666l50rQk33XFijsirVoCSmyILmOqyqIlSUW8VYpO9nlcO+a/CAFK1KD7TKwyTNv"
378
+ "mMr2GNzgiOjEuFMQkDuTbUtPNJuQP1LNql61c/K448izolTOu9knUyZMNJ3kwsyZxJ3AZExIO46JOKcLG8tkT4yCXtqSRZLwYzFJsvmBNWl4HZLtcp3Yk55b"
379
+ "SZsKwhncaNiCXacn5kK6rAWKIQ0O4FymOgXGuAkyimIGIIKXTX/qkoBHAECD6O1/VA/vTGgsElTXnG//DUUGfoNbaN+aLQhblFCGIKzymUxYKkrB3udTaFzj"
380
+ "7etQWUr9RD/7+z+K8ol9aPiRaZMnKa2Ylm1M6Fj2thiegGTsSOvzNksjGc6AZgvVJo/Ug6j8YM9D+iHDcToMxYZaULGon1SRPDu28pF64BuT8gYrU+fhw7vV"
381
+ "SRtCvKMKEhlSNM82mzk9Yj/8C4fd7UuiJXXMxgsQjPmAeggz/tBmBseKcojLWbH2iF55uurENQT5ERV1Knu1AtLblgFuIq3EyHOdBwLOF5/+cyzQbedcAnpt"
382
+ "SGJX+V4AqsBP+BLEATjcWBPfKi/TZ4zdtoywzuXkrTp2HeQe4qCahQBIvguC72187rzM+xmDqbfs4USn/Sgs0W8JtMT0tHgywe03e9GVB9Ys+vE9hfoEB3Af"
383
+ "S74mvaDPlEP9sWVn5FSQdGRCKEl4rHFINdpiIuwASwskb1Ijcn5NfuCenJ1Ka63uHL13Rvb9xtCYzdHyDvFuWxfkGVIDbuNG+50mdA5TOWMjEwJu6GxiIKSK"
384
+ "zCInjwRLFXayOVTOt6lMmwbgWb+gOkA86q6ZeuY1I55gqm1Sr4K+aQmL0k796KRPjoJ22HahAWzxqeUJUJZVvqxYi6XHIWOT17cp72Kuqp8oMHg9KP25f6Hc"
385
+ "YIOn0AjaNpGCo+mRcjG1jK+k0KgCGVyRCr9JZb+uB6cOxfMZQBaaUeaF9UXUvUDLa5eGfTC+mta9gvLo3hJjyB7dgrjjp4jEc7ZnB2izbeqRAfBRiGe0xs99"
386
+ "c2TTDtPNRYfsgkTXAf2zO4sEiGKdDnyKWaXzyrh8M7agW/MiO/93wGry0TbWmXld7Jg6cWPCSIVsB8ge4B7Hlu12hPozVr3wrrdD5QHP597QmIqXaxE1mtGG"
387
+ "8h2lCdEdmc5FvyN0ragV+O6RNw2zIwJ2Z+hEyFf8dGBPEIOZ28YBBQLtGYKJhCr5xaefj0X+mPx4gAG31wY0PVFkF8oGYc9MoD5UDN8lBoxAvO46BrnalAtu"
388
+ "hZhzUIcdMvgNrsmia18wqD0TYiZGnUfQLPpviXz59NzcGB1c9GwSwLZpbZKOIfSxNo9fFmkW2L5DDUE9FwL47UBfO6P4q6cQAkcGXHjkoqlUL0aA8NdkPIq2"
389
+ "kO/7BdO4q8aGGSiKIOddllvSuoaxvJUJCmJaL2nf2S9gOAlhouPXnt1+nX4qQSO4TW+G2K9SsIjBBUzDM1PYpILwzWwH6ui3Awy5hjSFG3dmPl4M5AmKRXCw"
390
+ "98yDxGtTYqKJJu234zVDl7chftLLFKMwJGIL/pPHCICFb75+h396kNdCg+NQThVDIzitsi/RMITbD5XmnHaUZXadI99iWhl1XoDqF7rzoJi1yPgS9qFm3cJ3"
391
+ "SkRA2PvzzYcDIDAu7lX2jfe5WT3x1tSqJ6ZcEUq+QuCDsWzH1TClgIfjmIJf2nS+AHtvG1BeVNQK55qh7a1O/Nb9p+Rz8fTxaTwYEFFkQ1yrk0FnAS2xhh8w"
392
+ "t71x7atb7pMvfjB83jVp+i900Jhlu4tjc92TC1j3mr2aXT/ISd8IpXApM9Bffq4QL+rvuqiluAlDtf27NFff/Ng55o+iWzxb3F/M1kS49XzKSYrkXPHwYKtC"
393
+ "8f8t7pwV0ufNWT9Wdj2+3Gf0GhtadPLm4sy1bA+ExkiNvrMJeftKt7+LNPtdDNp9COoXDXUz2/Bx2WEDxOWFGzCStIAfXs0M+/e15LiRFs5W5PffljME0ztY"
394
+ "fUgTt1gOyJnBQDun5j344F7TDsI1UmPEfGv7s5gE1tR/afszVWKHp/dzb79cQP5Ywbx596uK3llIZ1NyJn8ZFPHuRZgL/VwSy35l+ToxrVHc8rr6P1BLAwQU"
395
+ "AAAACAAAACFcCOZlENoMAACYHAAACQAAAFJFQURNRS5tZJVZy3LjxhXd8yu6yjVlJ0VS8vg9WskalUeJNFIseZysiCbQJNvCy2hAGrq0zgfkE/MlOefeboBS"
396
+ "ZpFsJBLox32ee+7lZ+bS/z74wpxuXd2bv7quduVsdrdzptQXVl7cywvjg+nx6sF1wTe1K0zblD7fG1sXZmexJART2r3rzBDwdr03Z2UzFMGcNQUeBrzHvrA0"
397
+ "Fz3P8nWPw/HEluUeb1vb2R77Nl1TyUW+qoberktn8qbuu6Y0bWlrh3e2N3bAEmzPsScYW1S+9qHHCU0X5ibgHx93vd/YvMeTB1v6QtbmkFc/zk031ME4vBss"
398
+ "JeFOv8WjqCO+t67bNF0FJWxtu72BGFCqn4vW/BLM2ub3pnPbLik4m332mbmC0mE2e5IP5smcDn1T4ZbchHzniqHkw6uoYBg6yMknN9C+oSzJuk84YrFYmE/8"
399
+ "xZvserPJ8P2tDzyowMf38A3+/UzVbOcg2W8up2Fl/d1Q+3rLLb86dw/DP5m7BqalOi3ubvt0cVsOsA0Mv28b+AoLb3sYdLSETQqJ/+ymh49bCxPU2wOTxlt3"
400
+ "vr6P957DuHs4sXPOFHYf/h8JfnYIS3r2lA43tsUGXGXWDk5ySTK58nSrDnlwYh/rRdcznF+63hn3EcEiZulx+ZFefJTCOOdp//t1M31f2b1poF3nCycRnDy9"
401
+ "NMypioFALccAoHvsA0STIFg3/Q5ZITv1wPOHphzEig/ePcpeGxDccMPQmqaVmH0zm2VZ1kOh2WKhebvQjF3IjYiQJ/X6U3LD02Qc7HHplsUoWIM9u2boyv1T"
402
+ "Qcs9OTpt9dWKDnt6lMjhrQoWsEvJRKSgrlo7pLyFCauWd8DCedPCMBsohrRvkS2AFSgaaKZMMWIlGLFs99nSXNejir4nlqTjxHFlg4AbT3kECBjeQ5wS+yAo"
403
+ "Y0jiqcIADSpyFUkY2cbHozTBVF5jt+kgW7pOPafeSEIF1/dcmKmxV2rsVXy90uDNjM1z1yJdMl/vXOf7zHzBGwu3sUPZ/4n34BVzM3tzsOjeuVZFbglCgSEK"
404
+ "cCG4KdRGHF20viYIT1D1uPNwXToTmFi8UD3iuKU9a8RTK1gznWEefQ+v95ARulNF2f4Rd/PLKAUkX3fO3ou1ymJCdgG+D/GwH5uhLpAfs9m5zXcHaJ5QWWDd"
405
+ "eonghclUOobAm1EgAX4upeLuY9t0FHfXNPe4DHuYu9HgupEPkCzBr33p+2SwEgaZIFV2asI/2xv2MHa1iBhkh22FoLIvN0aI0C2oHygiVFQLQtuEfiEyTDJW"
406
+ "tvYbhOLyt9DU2IM6h3PnKJlhxxo06hiVngtQ6Hnvrk7PpCTZfuicHHf27vT9T+eX1z8tqwKnIT26hSgMyw7ttrMFc2QLX5xOJVKOTfFU5zh9Quh5jAP5aIfC"
407
+ "91oF4V0VIuDPuvkIQXF0TSQAAJZNvUV4NRIiLz27BM5KxWZFojZSdesGyG57xMKhEU9MUyNZn+E1gTTf2XqrMLoZyvIAsAnOGmoTPt741pW+drPZl0tzKw6X"
408
+ "rdiBE8fIhzYeKdk/NtENY/qQrsTUXM5eLwH9tpAT4J6N38L6hdh6nqJdbUMeATxvWndiDrJbHxFVgESyD47mx5QquptPhIksZ19RbKVADHRgAf2EnIO2hUgX"
409
+ "8z2qkjJEaIgrGD9cC7f94Rjyeed6HPv10pyG+5eKgNU5oUp0/YaogXpmyBvwr2Jp83XhWoc/dQ/nxPW0/QF5AsBBNoGJGAYMkPVQbF1/AojZoHJE/5cQ7vcB"
410
+ "SSAPFZaQOyJ9jIw1pK8AX3WzUtdny9k3y1SCjSOG6IsDKXgzFB06HAV87oa8FwU1zHi6Fkmp2XW+q2x3j91BigJYpPvoutwHDTPYAkEMqoeqI+S0kAAPTfmA"
411
+ "ePt2ac5rmAlOocXmhuE2x4J7h1jgmerT3leOGKp2CNFBhOQJAhfriI2AqqZU6rmcfbc0V65DzCPhfC9+FNstkD/IR9GbkhOlTYdzUen+GJ/iQCQmrO4/yjNU"
412
+ "NgDgnJrVYmsp4SxtA+pB3R9NRmSxs50PDKctAVlzJ1j4g4ez4pGYJI6OuKbUuOZmj4LBAtzkLPVQ4nvmfrWGbZALoITKy8F6kdxIDNijN18dv1IlbH0YaGYN"
413
+ "kxbm8vLK/Ebjcel3x68i1XYsFYoVFG7nt0DPfkF5R9o5qrSc/cDQaZ3tX4hxwEwbJtjIBCZzRCrvJOskGxArtfnm1UlypJJdyzRrRLbE/ZezL4+XypPxOvvm"
414
+ "lfn3P/9lXsf/Xx4fv8oia1ya96jACQ6UCYboZlwbFFBZI1JhH+vwuKlzlWgvwICrAX3aPji1k4pJ5Xgzz9xKhhOYtWt5zuHFFmHI6UtUiK5rAHdYT1/X4OOl"
415
+ "r4ieUV1AsFL5lMg4nxmJyAxeDOxDxC/oFHnt7d8uvZpYqYSGgegJ7lrfYyXiFLGAVPf1qE8xtXJiKqCrF+ouh1IV8LICOJED1GDbX936lwsR6uLtuQG8+NxF"
416
+ "I+f3dfMIXNqKUIco12rnCp0L9+DzmM+xNdOC886TUu7NjfABEO+dPlhFOF7hvH6XQb6h7sN/1Rj2EEWhxomy50N3uIbNcRirCGNVqkj2Grx4vMyKh7K0TFAq"
417
+ "Y5FEMaUChzRRK1HQ5iKdy/V/JtNO4Bg0YkmyYlELRDmGD2zkH3wxYFWFIxlChDjsqNqQ3HpQW/jyD5QS5p/WrhimW/CkVqMbW3z36YIGlJ2qX4WkhzCWJhAu"
418
+ "P8YRYy4FUUqQzj7GUiiUWS+nz+/hFM2IuBt41WtPjKsWPCuRNLHBX26v319iZ77zD9Hxt3bjQCevW9ZB+lI7H20MsvOKuM0UkXbcKts8aEW0cIWpStIDXDtP"
419
+ "hVEYUu7KMmLThuAY2cvYoCnfFOawbjopLvsxghKunB5A2VhbJ66vWVLu37DMVUMgb2BtHWEidrWCFjgtkXWlqxpaiZDGZNy5PCYuG3v4Hxmd+Ak8VzbgpMAn"
420
+ "NSS6yjwWQC1vwXzBahUJe9r3+k/Irp/O78yRbf2RzHeORjsc6cZMjHfYoD9jvJpLsUtmkZOqRD23ZbNmPAszGWFj/iwhdSbGdPqcQ4i827cSMEqYIoETinkw"
421
+ "iVK8ITFC/n9y31JWNI8gVGHnW4rFXEszrxHkYQe/8W7K4xwNhRf4zv6+GInvAqct7shBkPuImIsbIgzhOTL1MKxxYD9IRmOzyPfCTG+TBRgKyJkhEqJRGI49"
422
+ "POlFjkhxY5soqKGdoRQdEihygFk2EsyV2HjVOaYFDJZJZT98GptAkwWJDGREJtbhh2i0lS/4TTZlMf5r7Y02uN51bcfxkEwYIPVm4zRvfP3QxLFEAqio8Rlw"
423
+ "ghaw5QhPCg2ApabzW09QfLZH8QfohIADLEuJOnyvqSA4UIiBvFBS0ekEMQahJKyid6Txm4SY69RhFDeOGIJMAybeFWNZhxJCzzSEkeYjRxfzKH/qd4Dc7e4Z"
424
+ "x0pwszSXbms5ZIsdwMXb0RRhaGOnLRUB3TAjQxRgGUV7gurne8FTmVocJtXYRM1u7YNLxIDVpXOHmiQgXZofWZo0gUcwjIlNE+IUxE3n2OQ2DHPYPSXp0Gqt"
425
+ "Qvw6evzd3d2N+fr4B7WQQ6mFXXWiQuaCwAJZCOQfepqIN9R6h1TbEIsJycwQ2PiXwiwRpb4emL+nsfhIX6CASM4dSyyF6zinitgWR4W9RReGFynjOGB5hiZF"
426
+ "GuAeAAhjJHY8n4iNeRSefKaOgUAnssM68BHr8JiosEZs7Y3MeRqIzQIBewBDpOSUxIB9NDsS6wEGXqQeLsbASRSX50S78mMUW46F/2UCrXXYJESQGfAIBPKt"
427
+ "3VlJhxddz0HEzVPzoCoLMYUjrg4HdjLfj3Efchn7iSHYzbCVBzhokIigAMcBRLXUpcBktHtdN7Q6XqjaIc59DlkEe0f+PKFdrXLrTk2bGMg4IKaYsY5G0jbB"
428
+ "TC8DkregNRKKjIyjyHljfMjcQMjDSbruWfzMx6c7OisNJufPKUQcYY6jiscG8neIDI4lY/9QOhuxnjFdNvk987bHcltyHJBGnlLiQUPPP1xf/nJ3cf1+dXX9"
429
+ "9vxydXZ6c/rjxeXF3T9WH85/vsULsMrTmlNB+TVgHK6SonOC0sY+NVvGlwsl7+EoS6nSubZE7ypJV2izHcgK0xQSEUaK8Qx951NHEV02zrCQ/nsGzvRTEI2R"
430
+ "aEbyDhQVdsirhbquB18WmtLTLxDh82mYGuWP7XjWaifM2V84Cvs6Xz2fDq+hd+lkwD2bfZDiPpamCuyzSD/WTP1SVjR5mEjP6uFgF4d/gHBins6Z6XkaGiVi"
431
+ "IaVMoT9TgWTrfjWdhbgpdfo2jdh7IH3TWe3oaOO5jIRGNjlnQ141YIxA2G9TwdFeBgREofD18dffp/IjTyeiBC3RRSzGJBEiPI2c27LZc+K6nP0HUEsBAhQD"
432
+ "FAAAAAgAAAAhXM3NRj+gAAAAdgEAAAsAAAAAAAAAAAAAAIABAAAAAF9faW5pdF9fLnB5UEsBAhQDFAAAAAgAAAAhXN8qpShBVQAAzX0BAAoAAAAAAAAAAAAA"
433
+ "AIAByQAAAGNvbnRyb2wucHlQSwECFAMUAAAACAAAACFcXz7FrDQIAAB1HQAACQAAAAAAAAAAAAAAgAEyVgAAbW9kZWxzLnB5UEsBAhQDFAAAAAgAAAAhXAjm"
434
+ "ZRDaDAAAmBwAAAkAAAAAAAAAAAAAAIABjV4AAFJFQURNRS5tZFBLBQYAAAAABAAEAN8AAACOawAAAAA="
366
435
  )
367
436
 
368
437
 
@@ -372,45 +441,45 @@ def _ensure_embedded_liquid_kernel_package(package_root: Path | None = None) ->
372
441
  quarantine = raw_root.with_name(f"{raw_root.name}.invalid-{uuid.uuid4().hex[:12]}")
373
442
  raw_root.rename(quarantine)
374
443
  root = raw_root.resolve(strict=False)
375
- required = ("__init__.py", "control.py")
376
- ready = root.is_dir() and all((root / name).is_file() for name in required)
444
+ required = ("__init__.py", "control.py", "models.py")
445
+ capability = 0
446
+ ready = root.is_dir() and all((root / name).is_file() and not (root / name).is_symlink() for name in required)
377
447
  if ready:
378
448
  try:
379
- ready = "def inject_embedded_kernel" in (root / "control.py").read_text(encoding="utf-8")
449
+ match = re.search(r"^EVOLUTION_MODEL_CAPABILITY_VERSION = (\d+)$", (root / "control.py").read_text(encoding="utf-8"), re.M)
450
+ capability = int(match.group(1)) if match else 0
451
+ ready = capability >= 2
380
452
  except Exception:
381
453
  ready = False
382
454
  if ready:
383
- return {
384
- "root": str(root),
385
- "ready": True,
386
- "restored": False,
387
- "source": "local",
388
- "required_files": list(required),
389
- }
390
- if root.exists():
391
- quarantine = root.with_name(f"{root.name}.incomplete-{uuid.uuid4().hex[:12]}")
392
- root.rename(quarantine)
455
+ return {"root": str(root), "ready": True, "restored": False, "source": "local",
456
+ "required_files": list(required), "model_capability_version": capability}
393
457
  root.mkdir(parents=True, exist_ok=True)
458
+ backup = None
394
459
  try:
395
460
  payload = base64.b64decode(_EMBEDDED_LIQUID_KERNEL_PACKAGE_B64.encode("ascii"), validate=True)
396
461
  with zipfile.ZipFile(io.BytesIO(payload)) as archive:
397
- allowed = {"__init__.py", "control.py", "README.md"}
398
- for name in allowed:
399
- member = archive.getinfo(name)
462
+ names = (*required, "README.md")
463
+ contents = {name: archive.read(name) for name in names}
464
+ # Back up package code only. Runtime records, config, signing keys
465
+ # and artifact/version history stay at their original paths.
466
+ if any((root / name).exists() for name in names):
467
+ backup = root / ".package-backups" / uuid.uuid4().hex
468
+ backup.mkdir(parents=True)
469
+ for name in names:
470
+ old = root / name
471
+ if old.is_file():
472
+ shutil.copy2(old, backup / name)
473
+ for name, content in contents.items():
400
474
  target = root / name
401
475
  temporary = target.with_name(f".{target.name}.{uuid.uuid4().hex}.tmp")
402
- temporary.write_bytes(archive.read(member))
476
+ temporary.write_bytes(content)
403
477
  os.replace(temporary, target)
404
478
  except Exception as exc:
405
- raise RuntimeError(f"unable to restore embedded liquid_kernel package: {exc}") from exc
406
- return {
407
- "root": str(root),
408
- "ready": True,
409
- "restored": True,
410
- "source": "embedded",
411
- "required_files": list(required),
412
- }
413
-
479
+ raise RuntimeError(f"unable to restore embedded liquid_kernel package: {type(exc).__name__}") from exc
480
+ return {"root": str(root), "ready": True, "restored": True, "source": "embedded",
481
+ "required_files": list(required), "model_capability_version": 2,
482
+ "backup_root": str(backup) if backup else ""}
414
483
 
415
484
  LIQUID_KERNEL_PACKAGE_STATUS = _ensure_embedded_liquid_kernel_package()
416
485
 
@@ -420,6 +489,32 @@ from liquid_kernel import ( # noqa: E402
420
489
  LiquidKernelError,
421
490
  )
422
491
 
492
+
493
+ def _ensure_liquid_kernel_sqlite_lifecycle(registry_class) -> None:
494
+ """Close legacy local-package connections without replacing customized files."""
495
+ if getattr(registry_class, "SQLITE_CONNECTIONS_CLOSE_ON_EXIT", False):
496
+ return
497
+ original_connect = registry_class._connect
498
+
499
+ @contextlib.contextmanager
500
+ def managed_connect(self):
501
+ with contextlib.closing(original_connect(self)) as conn:
502
+ with conn:
503
+ yield conn
504
+
505
+ registry_class._connect = managed_connect
506
+ registry_class.SQLITE_CONNECTIONS_CLOSE_ON_EXIT = True
507
+
508
+
509
+ # A standalone main-file update can leave an older local liquid_kernel package.
510
+ # Adapt that package in memory as well as shipping the fixed embedded archive.
511
+ from liquid_kernel.control import ( # noqa: E402
512
+ LiquidKernelRegistry as _LiquidKernelRegistry,
513
+ )
514
+
515
+ _ensure_liquid_kernel_sqlite_lifecycle(_LiquidKernelRegistry)
516
+
517
+
423
518
  # BEGIN EMBEDDED COLLABORATION BACKEND
424
519
  COLLAB_DB_FILENAME = "collaboration.sqlite"
425
520
  COLLAB_SESSION_TTL_SECONDS = 24 * 60 * 60
@@ -12152,6 +12247,63 @@ def probe_ollama_environment(base_url: str, timeout: int = 4) -> tuple[bool, lis
12152
12247
  except Exception as exc:
12153
12248
  return False, [], str(exc)
12154
12249
 
12250
+ def extract_ollama_model_capabilities(payload: object) -> dict:
12251
+ """Translate Ollama ``/api/show`` metadata to provider-neutral hints."""
12252
+ if not isinstance(payload, dict):
12253
+ return {}
12254
+ values: set[str] = set()
12255
+ raw = payload.get("capabilities")
12256
+ if isinstance(raw, dict):
12257
+ values.update(str(key).strip().lower() for key, value in raw.items() if value)
12258
+ elif isinstance(raw, (list, tuple, set)):
12259
+ values.update(str(value).strip().lower() for value in raw)
12260
+ elif isinstance(raw, str):
12261
+ values.update(part.strip().lower() for part in re.split(r"[,\s]+", raw) if part.strip())
12262
+ caps: dict[str, object] = {}
12263
+ if any("think" in value or "reason" in value for value in values):
12264
+ caps["reasoning_supported"] = True
12265
+ caps["reasoning_style"] = "ollama"
12266
+ if any("vision" in value or "image" in value for value in values):
12267
+ caps["vision"] = True
12268
+ if any("tool" in value or "function" in value for value in values):
12269
+ caps["tools"] = True
12270
+ return caps
12271
+
12272
+
12273
+ def probe_ollama_model_records(
12274
+ base_url: str,
12275
+ *,
12276
+ timeout: float = 1.5,
12277
+ max_models: int | None = None,
12278
+ ) -> list[dict]:
12279
+ """List Ollama models and inspect each advertised capability by default.
12280
+
12281
+ ``max_models`` remains available for callers that explicitly need a bound;
12282
+ normal discovery must import the complete provider directory.
12283
+ """
12284
+ ok, tags, _ = probe_ollama_environment(base_url, timeout=max(0.5, float(timeout)))
12285
+ if not ok:
12286
+ return []
12287
+ records = [{"id": name, "capabilities": {}} for name in tags]
12288
+ names = tags if max_models is None else tags[: max(0, int(max_models))]
12289
+ for index, name in enumerate(names):
12290
+ try:
12291
+ body = json.dumps({"name": name}).encode("utf-8")
12292
+ req = Request(
12293
+ f"{str(base_url or '').rstrip('/')}/api/show",
12294
+ data=body,
12295
+ method="POST",
12296
+ headers={"Content-Type": "application/json", "Accept": "application/json"},
12297
+ )
12298
+ with urlopen(req, timeout=max(0.5, float(timeout))) as resp:
12299
+ payload = json.loads(resp.read().decode("utf-8", errors="replace"))
12300
+ records[index]["capabilities"] = extract_ollama_model_capabilities(payload)
12301
+ records[index]["raw"] = payload
12302
+ except Exception:
12303
+ continue
12304
+ return records
12305
+
12306
+
12155
12307
  def list_ollama_models(base_url: str, timeout: int = 4) -> list[str]:
12156
12308
  ok, tags, _ = probe_ollama_environment(base_url, timeout=timeout)
12157
12309
  return tags if ok else []
@@ -12167,7 +12319,46 @@ class EmptyActionError(RuntimeError):
12167
12319
  class CircuitBreakerTriggered(RuntimeError):
12168
12320
  """Raised when fused fault counter reaches hard break threshold."""
12169
12321
 
12170
- def list_ollama_models_cached(base_url: str, ttl_seconds: int = 30, force_refresh: bool = False) -> list[str]:
12322
+ def _fetch_ollama_models_cached(key: str, base_url: str, cached_tags: list[str]) -> None:
12323
+ """Fetch Ollama tags and publish one cache snapshot.
12324
+
12325
+ The caller marks the cache row as fetching before starting this worker, so
12326
+ concurrent catalog requests can keep returning the last snapshot while the
12327
+ network request is in flight.
12328
+ """
12329
+ try:
12330
+ ok, tags, _ = probe_ollama_environment(base_url)
12331
+ except Exception:
12332
+ ok, tags = False, []
12333
+ finally:
12334
+ with _OLLAMA_TAG_CACHE_LOCK:
12335
+ if ok:
12336
+ _OLLAMA_TAG_CACHE[key] = {"ts": time.time(), "tags": list(tags)}
12337
+ else:
12338
+ row_now = _OLLAMA_TAG_CACHE.get(key, {})
12339
+ row_now.pop("_fetching", None)
12340
+ if cached_tags:
12341
+ _OLLAMA_TAG_CACHE[key] = {
12342
+ "ts": time.time(),
12343
+ "tags": list(cached_tags),
12344
+ }
12345
+ else:
12346
+ _OLLAMA_TAG_CACHE[key] = {"ts": time.time(), "tags": []}
12347
+
12348
+
12349
+ def list_ollama_models_cached(
12350
+ base_url: str,
12351
+ ttl_seconds: int = 30,
12352
+ force_refresh: bool = False,
12353
+ *,
12354
+ background: bool = False,
12355
+ ) -> list[str]:
12356
+ """Return cached Ollama tags, optionally refreshing without blocking.
12357
+
12358
+ UI/catalog reads use ``background=True`` so an unavailable local daemon
12359
+ cannot delay the first render. Explicit scan endpoints keep the historical
12360
+ synchronous behaviour by leaving it false.
12361
+ """
12171
12362
  key = str(base_url or "").rstrip("/")
12172
12363
  now = time.time()
12173
12364
  cached_tags: list[str] = []
@@ -12186,25 +12377,19 @@ def list_ollama_models_cached(base_url: str, ttl_seconds: int = 30, force_refres
12186
12377
  row = {"ts": 0.0, "tags": []}
12187
12378
  row["_fetching"] = True
12188
12379
  _OLLAMA_TAG_CACHE[key] = row
12189
- try:
12190
- ok, tags, _ = probe_ollama_environment(base_url)
12191
- except Exception:
12192
- ok, tags = False, []
12193
- finally:
12194
- with _OLLAMA_TAG_CACHE_LOCK:
12195
- if ok:
12196
- _OLLAMA_TAG_CACHE[key] = {"ts": time.time(), "tags": list(tags)}
12197
- else:
12198
- # Clear fetching flag, keep old cache if available
12199
- row_now = _OLLAMA_TAG_CACHE.get(key, {})
12200
- row_now.pop("_fetching", None)
12201
- if not cached_tags:
12202
- _OLLAMA_TAG_CACHE[key] = {"ts": time.time(), "tags": []}
12203
- if ok:
12204
- return list(tags)
12205
- if cached_tags:
12380
+ if background:
12381
+ threading.Thread(
12382
+ target=_fetch_ollama_models_cached,
12383
+ args=(key, base_url, cached_tags),
12384
+ name="ollama-model-probe",
12385
+ daemon=True,
12386
+ ).start()
12206
12387
  return list(cached_tags)
12207
- return []
12388
+ _fetch_ollama_models_cached(key, base_url, cached_tags)
12389
+ with _OLLAMA_TAG_CACHE_LOCK:
12390
+ row = _OLLAMA_TAG_CACHE.get(key, {})
12391
+ tags = row.get("tags", []) if isinstance(row, dict) else []
12392
+ return list(tags) if isinstance(tags, list) else list(cached_tags)
12208
12393
 
12209
12394
  def resolve_ollama_model(base_url: str, preferred: str) -> str:
12210
12395
  models = list_ollama_models(base_url)
@@ -12619,39 +12804,45 @@ def clamp_effort(effort: str, *, ceiling: str = EFFORT_MAX, floor: str = EFFORT_
12619
12804
  return EFFORT_LEVELS[min(max(EFFORT_ORDER[e], lo), hi)]
12620
12805
 
12621
12806
 
12622
- def model_reasoning_style(provider: str, model: str) -> str:
12807
+ def model_reasoning_style(
12808
+ provider: str,
12809
+ model: str,
12810
+ capabilities: dict | None = None,
12811
+ *,
12812
+ capabilities_probed: bool = False,
12813
+ ) -> str:
12623
12814
  """Return which reasoning dialect a (provider, model) pair speaks.
12624
12815
 
12625
12816
  One of: "anthropic" | "openai" | "deepseek" | "glm" | "ollama" | "none".
12626
12817
  Conservative: only models known to support reasoning return a non-"none"
12627
12818
  style, so unknown models are never sent reasoning fields.
12628
12819
  """
12629
- prov = normalize_openai_compat_provider_name(provider) if provider else ""
12630
- raw_prov = str(provider or "").strip().lower()
12631
- m = str(model or "").strip().lower()
12632
- if raw_prov == "anthropic":
12633
- # Extended thinking on Claude 3.7+ / 4.x families.
12634
- if any(tok in m for tok in ("claude-3-7", "claude-3.7", "claude-sonnet-4",
12635
- "claude-opus-4", "claude-haiku-4", "claude-4",
12636
- "-thinking", "claude-sonnet-5", "claude-opus-5")):
12637
- return "anthropic"
12638
- return "none"
12639
- if raw_prov == "ollama":
12640
- # Local reasoning models (qwen3, deepseek-r1, etc.) accept native think flag.
12641
- if any(tok in m for tok in ("r1", "qwen3", "deepseek", "thinking", "reasoner", "magistral")):
12642
- return "ollama"
12643
- return "none"
12644
- if prov in OPENAI_COMPAT_PROVIDER_NAMES or raw_prov == "custom_http":
12645
- if prov == "glm" or m.startswith("glm-") or "glm" in m:
12646
- # GLM 4.5/4.6/5.x + coding endpoints use {"thinking": {...}}.
12647
- return "glm"
12648
- if "deepseek" in m or m in ("deepseek-reasoner",):
12649
- return "deepseek"
12650
- # OpenAI o-series / gpt-5 reasoning, and openrouter slugs that wrap them.
12651
- if (m.startswith("o1") or m.startswith("o3") or m.startswith("o4")
12652
- or "gpt-5" in m or "/o1" in m or "/o3" in m or "/o4" in m
12653
- or "reasoning" in m):
12820
+ # Probe metadata is authoritative. A model name is never evidence of a
12821
+ # reasoning API: providers may alias, fine-tune, or proxy models freely.
12822
+ if isinstance(capabilities, dict):
12823
+ explicit_style = str(
12824
+ capabilities.get("reasoning_style")
12825
+ or capabilities.get("reasoning_dialect")
12826
+ or ""
12827
+ ).strip().lower()
12828
+ supported = capabilities.get("reasoning_supported")
12829
+ if explicit_style in {"anthropic", "openai", "deepseek", "glm", "ollama", "none"}:
12830
+ return explicit_style
12831
+ if supported is False:
12832
+ return "none"
12833
+ if supported is True:
12834
+ # A provider can advertise reasoning without a dialect; use its
12835
+ # transport dialect rather than guessing from the model name.
12836
+ raw_provider = str(provider or "").strip().lower()
12837
+ if raw_provider == "anthropic":
12838
+ return "anthropic"
12839
+ if raw_provider == "ollama":
12840
+ return "ollama"
12841
+ if normalize_openai_compat_provider_name(provider) == "glm":
12842
+ return "glm"
12654
12843
  return "openai"
12844
+ if supported is None and not explicit_style:
12845
+ return "none"
12655
12846
  return "none"
12656
12847
  return "none"
12657
12848
 
@@ -12662,6 +12853,7 @@ def resolve_reasoning_payload(
12662
12853
  effort: str,
12663
12854
  *,
12664
12855
  max_tokens: int = 2000,
12856
+ capabilities: dict | None = None,
12665
12857
  ) -> dict:
12666
12858
  """Map (provider, model, effort) -> concrete request mutations.
12667
12859
 
@@ -12676,7 +12868,7 @@ def resolve_reasoning_payload(
12676
12868
  eff = str(effort or EFFORT_OFF).strip().lower()
12677
12869
  if eff not in EFFORT_ORDER or eff == EFFORT_OFF:
12678
12870
  return {}
12679
- style = model_reasoning_style(provider, model)
12871
+ style = model_reasoning_style(provider, model, capabilities)
12680
12872
  if style == "none":
12681
12873
  return {}
12682
12874
  if style == "anthropic":
@@ -12707,13 +12899,19 @@ def resolve_reasoning_payload(
12707
12899
  return {}
12708
12900
 
12709
12901
  def openai_compat_probe_headers(provider: str, api_key: str = "") -> dict[str, str]:
12902
+ raw_provider = str(provider or "").strip().lower().replace("-", "_")
12710
12903
  normalized = normalize_openai_compat_provider_name(provider)
12711
12904
  headers = {
12712
12905
  "Accept": "application/json",
12713
12906
  "User-Agent": "Clouds-Coder/1.0",
12714
12907
  }
12715
12908
  if str(api_key or "").strip():
12716
- headers["Authorization"] = f"Bearer {str(api_key).strip()}"
12909
+ if raw_provider == "anthropic":
12910
+ headers["x-api-key"] = str(api_key).strip()
12911
+ else:
12912
+ headers["Authorization"] = f"Bearer {str(api_key).strip()}"
12913
+ if raw_provider == "anthropic":
12914
+ headers["anthropic-version"] = "2023-06-01"
12717
12915
  if normalized == "openrouter":
12718
12916
  headers.setdefault("HTTP-Referer", "https://clouds-coder.local")
12719
12917
  headers.setdefault("X-Title", "Clouds Coder")
@@ -12753,6 +12951,21 @@ def openai_compat_model_list_urls(endpoint_or_base: str, provider: str = "") ->
12753
12951
  out.append(url)
12754
12952
  return out
12755
12953
 
12954
+
12955
+ def anthropic_model_list_url(endpoint_or_base: str) -> str:
12956
+ """Return the Anthropic Models API URL for an API base or Messages URL."""
12957
+ base = str(endpoint_or_base or "").strip().rstrip("/")
12958
+ if not base:
12959
+ return ""
12960
+ low = base.lower()
12961
+ for suffix in ("/v1/messages", "/messages"):
12962
+ if low.endswith(suffix):
12963
+ base = base[: -len(suffix)].rstrip("/")
12964
+ break
12965
+ if base.lower().endswith("/v1"):
12966
+ return base + "/models"
12967
+ return base + "/v1/models"
12968
+
12756
12969
  def extract_openai_compat_model_ids(payload: object) -> list[str]:
12757
12970
  out: list[str] = []
12758
12971
  seen: set[str] = set()
@@ -12788,6 +13001,406 @@ def extract_openai_compat_model_ids(payload: object) -> list[str]:
12788
13001
  _walk(payload, 0)
12789
13002
  return out
12790
13003
 
13004
+
13005
+ def extract_openai_compat_model_records(payload: object) -> list[dict]:
13006
+ """Extract model ids plus conservative capability hints from /models."""
13007
+ records: list[dict] = []
13008
+ seen: set[str] = set()
13009
+
13010
+ def capability_hints(node: dict) -> dict:
13011
+ caps: dict = {}
13012
+ for key in ("reasoning_supported", "supports_reasoning", "reasoning", "thinking"):
13013
+ if key in node:
13014
+ raw = node.get(key)
13015
+ if isinstance(raw, bool):
13016
+ caps["reasoning_supported"] = raw
13017
+ break
13018
+ if isinstance(raw, (list, tuple, set)):
13019
+ caps["reasoning_supported"] = bool(raw)
13020
+ break
13021
+ if isinstance(raw, str) and raw.strip().lower() in {
13022
+ "true", "yes", "on", "enabled", "supported"
13023
+ }:
13024
+ caps["reasoning_supported"] = True
13025
+ break
13026
+ style = node.get("reasoning_style") or node.get("reasoning_dialect")
13027
+ if isinstance(style, str) and style.strip():
13028
+ normalized_style = style.strip().lower()
13029
+ if normalized_style in {"reasoning_effort", "reasoning-effort", "reasoning"}:
13030
+ normalized_style = "openai"
13031
+ caps["reasoning_style"] = normalized_style
13032
+ caps.setdefault("reasoning_supported", True)
13033
+ for key in ("supported_parameters", "supported_features", "features"):
13034
+ values = node.get(key)
13035
+ if isinstance(values, dict):
13036
+ values = values.keys()
13037
+ if isinstance(values, (list, tuple, set)):
13038
+ names = {str(value or "").strip().lower() for value in values}
13039
+ if any(
13040
+ any(token in value for token in ("reasoning", "thinking"))
13041
+ for value in names
13042
+ ):
13043
+ caps.setdefault("reasoning_supported", True)
13044
+ if any("reasoning_effort" in value for value in names):
13045
+ caps.setdefault("reasoning_style", "openai")
13046
+ break
13047
+ raw_caps = node.get("capabilities")
13048
+ if isinstance(raw_caps, dict):
13049
+ nested = capability_hints(raw_caps)
13050
+ caps.update(nested)
13051
+ elif isinstance(raw_caps, (list, tuple, set)):
13052
+ nested = capability_hints({"supported_features": raw_caps})
13053
+ caps.update(nested)
13054
+ return caps
13055
+
13056
+ def walk(node: object, depth: int = 0):
13057
+ if depth > 5 or node is None:
13058
+ return
13059
+ if isinstance(node, list):
13060
+ for item in node:
13061
+ walk(item, depth + 1)
13062
+ return
13063
+ if not isinstance(node, dict):
13064
+ return
13065
+ model_id = ""
13066
+ for key in ("id", "model", "name"):
13067
+ value = node.get(key)
13068
+ if isinstance(value, (str, int, float)) and str(value).strip():
13069
+ model_id = trim(str(value).strip(), 240)
13070
+ break
13071
+ if model_id and model_id.lower() not in {"list", "model", "models", "object", "data"}:
13072
+ low = model_id.lower()
13073
+ if low not in seen:
13074
+ seen.add(low)
13075
+ caps = capability_hints(node)
13076
+ records.append({"id": model_id, "capabilities": caps, "raw": node})
13077
+ for key in ("data", "models", "items", "result", "results", "value"):
13078
+ if key in node:
13079
+ walk(node.get(key), depth + 1)
13080
+
13081
+ walk(payload)
13082
+ return records
13083
+
13084
+
13085
+ _PROVIDER_MODEL_CACHE_LOCK = threading.Lock()
13086
+ _PROVIDER_MODEL_CACHE: dict[str, dict] = {}
13087
+
13088
+
13089
+ def _fetch_provider_models_cached(
13090
+ key: str,
13091
+ profile: dict,
13092
+ cached_models: list[dict],
13093
+ ) -> None:
13094
+ """Fetch provider models and publish one cache snapshot."""
13095
+ if not isinstance(profile, dict):
13096
+ return
13097
+ provider = str(profile.get("provider", "") or "").strip().lower()
13098
+ base_url = str(profile.get("base_url", "") or "").strip().rstrip("/")
13099
+ endpoint = str(profile.get("endpoint", "") or "").strip()
13100
+ api_key = str(profile.get("api_key", "") or "").strip()
13101
+ custom_headers = profile.get("headers", {})
13102
+ custom_headers = (
13103
+ {str(k): str(v) for k, v in custom_headers.items() if str(k).strip()}
13104
+ if isinstance(custom_headers, dict)
13105
+ else {}
13106
+ )
13107
+ records: list[dict] = []
13108
+ try:
13109
+ if provider == "ollama" and base_url:
13110
+ records = probe_ollama_model_records(base_url, timeout=1.5)
13111
+ elif provider == "anthropic":
13112
+ models_url = anthropic_model_list_url(base_url or endpoint)
13113
+ headers = openai_compat_probe_headers(provider, api_key)
13114
+ headers.update(custom_headers)
13115
+ seen: set[str] = set()
13116
+ after_id = ""
13117
+ for _page in range(20):
13118
+ if not models_url:
13119
+ break
13120
+ separator = "&" if "?" in models_url else "?"
13121
+ page_url = f"{models_url}{separator}limit=1000"
13122
+ if after_id:
13123
+ page_url += f"&after_id={quote(after_id, safe='')}"
13124
+ req = Request(page_url, method="GET", headers=headers)
13125
+ with urlopen(req, timeout=3) as resp:
13126
+ payload = json.loads(resp.read().decode("utf-8", errors="replace"))
13127
+ page_records = extract_openai_compat_model_records(payload)
13128
+ for record in page_records:
13129
+ model_id = str(record.get("id", "") or "").strip()
13130
+ if model_id and model_id not in seen:
13131
+ seen.add(model_id)
13132
+ records.append(record)
13133
+ if not isinstance(payload, dict) or not bool(payload.get("has_more")):
13134
+ break
13135
+ next_after = str(payload.get("last_id", "") or "").strip()
13136
+ if not next_after or next_after == after_id:
13137
+ break
13138
+ after_id = next_after
13139
+ elif is_openai_compat_provider(provider) or provider == "custom_http":
13140
+ headers = openai_compat_probe_headers(provider, api_key)
13141
+ headers.update(custom_headers)
13142
+ for url in openai_compat_model_list_urls(base_url or endpoint, provider):
13143
+ try:
13144
+ req = Request(url, method="GET", headers=headers)
13145
+ with urlopen(req, timeout=3) as resp:
13146
+ payload = json.loads(resp.read().decode("utf-8", errors="replace"))
13147
+ records = extract_openai_compat_model_records(payload)
13148
+ if records:
13149
+ break
13150
+ except Exception:
13151
+ continue
13152
+ except Exception:
13153
+ records = []
13154
+ finally:
13155
+ with _PROVIDER_MODEL_CACHE_LOCK:
13156
+ previous = _PROVIDER_MODEL_CACHE.get(key, {})
13157
+ _PROVIDER_MODEL_CACHE[key] = {
13158
+ "ts": time.time(),
13159
+ "models": records or previous.get("models", cached_models),
13160
+ }
13161
+
13162
+
13163
+ def probe_provider_models(
13164
+ profile: dict,
13165
+ *,
13166
+ force_refresh: bool = False,
13167
+ ttl_seconds: int = 30,
13168
+ background: bool = False,
13169
+ cached_only: bool = False,
13170
+ ) -> list[dict]:
13171
+ """Return cached provider models and optionally refresh them in the background.
13172
+
13173
+ Direct callers retain synchronous probing by default. Model catalog reads
13174
+ pass ``background=True`` so a cold network cache never blocks a UI request.
13175
+ """
13176
+ if not isinstance(profile, dict):
13177
+ return []
13178
+ provider = str(profile.get("provider", "") or "").strip().lower()
13179
+ base_url = str(profile.get("base_url", "") or "").strip().rstrip("/")
13180
+ endpoint = str(profile.get("endpoint", "") or "").strip()
13181
+ api_key = str(profile.get("api_key", "") or "").strip()
13182
+ raw_headers = profile.get("headers", {})
13183
+ header_secret = (
13184
+ json.dumps(
13185
+ {str(k): str(v) for k, v in raw_headers.items()},
13186
+ sort_keys=True,
13187
+ ensure_ascii=False,
13188
+ )
13189
+ if isinstance(raw_headers, dict)
13190
+ else ""
13191
+ )
13192
+ auth_material = f"{api_key}\0{header_secret}"
13193
+ key_fingerprint = (
13194
+ hashlib.sha256(auth_material.encode("utf-8")).hexdigest()[:12]
13195
+ if api_key or header_secret
13196
+ else "-"
13197
+ )
13198
+ key = hashlib.sha256(
13199
+ f"{provider}|{base_url}|{endpoint}|{key_fingerprint}".encode()
13200
+ ).hexdigest()[:24]
13201
+ now = time.time()
13202
+ with _PROVIDER_MODEL_CACHE_LOCK:
13203
+ cached = _PROVIDER_MODEL_CACHE.get(key, {})
13204
+ cached_models = [
13205
+ dict(x) for x in cached.get("models", []) if isinstance(x, dict)
13206
+ ]
13207
+ if cached_only:
13208
+ return cached_models
13209
+ if cached and not force_refresh and now - float(cached.get("ts", 0.0) or 0.0) <= ttl_seconds:
13210
+ return cached_models
13211
+ if cached.get("fetching"):
13212
+ return cached_models
13213
+ cached["fetching"] = True
13214
+ _PROVIDER_MODEL_CACHE[key] = cached
13215
+ profile_snapshot = dict(profile)
13216
+ if background:
13217
+ threading.Thread(
13218
+ target=_fetch_provider_models_cached,
13219
+ args=(key, profile_snapshot, cached_models),
13220
+ name=f"model-probe-{provider or 'provider'}",
13221
+ daemon=True,
13222
+ ).start()
13223
+ return cached_models
13224
+ _fetch_provider_models_cached(key, profile_snapshot, cached_models)
13225
+ with _PROVIDER_MODEL_CACHE_LOCK:
13226
+ row = _PROVIDER_MODEL_CACHE.get(key, {})
13227
+ models = row.get("models", []) if isinstance(row, dict) else []
13228
+ return [dict(x) for x in models if isinstance(x, dict)]
13229
+
13230
+
13231
+ MODEL_RUNTIME_SETTING_KEYS = {
13232
+ "effort", "max_effort", "thinking_stream", "response_stream",
13233
+ "reasoning_supported", "reasoning_style", "temperature", "request_timeout",
13234
+ }
13235
+
13236
+
13237
+ def merge_probed_models_into_profile(
13238
+ profile: dict,
13239
+ records: object = None,
13240
+ model_ids: object = None,
13241
+ ) -> bool:
13242
+ """Persist the provider's discovered model directory and per-model hints."""
13243
+ if not isinstance(profile, dict):
13244
+ return False
13245
+ existing = [str(value).strip() for value in profile.get("models", []) if str(value).strip()]
13246
+ ids = list(existing)
13247
+ for raw in model_ids or []:
13248
+ value = str(raw or "").strip()
13249
+ if value and value not in ids:
13250
+ ids.append(value)
13251
+ settings = dict(profile.get("model_settings", {}) or {}) if isinstance(profile.get("model_settings"), dict) else {}
13252
+ for record in records or []:
13253
+ if not isinstance(record, dict):
13254
+ continue
13255
+ model_id = str(record.get("id") or record.get("model") or record.get("name") or "").strip()
13256
+ if not model_id:
13257
+ continue
13258
+ if model_id not in ids:
13259
+ ids.append(model_id)
13260
+ caps = record.get("capabilities") if isinstance(record.get("capabilities"), dict) else {}
13261
+ if not isinstance(caps, dict):
13262
+ caps = {}
13263
+ if caps.get("reasoning_style") is None and caps.get("reasoning_dialect"):
13264
+ caps = dict(caps)
13265
+ caps["reasoning_style"] = caps.get("reasoning_dialect")
13266
+ normalized = normalize_model_runtime_settings(caps)
13267
+ if normalized:
13268
+ prior = normalize_model_runtime_settings(settings.get(model_id, {}))
13269
+ settings[model_id] = {**prior, **normalized}
13270
+ changed = ids != existing or settings != (profile.get("model_settings") if isinstance(profile.get("model_settings"), dict) else {})
13271
+ if ids:
13272
+ profile["models"] = ids
13273
+ if not str(profile.get("model", "") or "").strip() or str(profile.get("model", "")).strip().lower() in {"auto", "custom-model"}:
13274
+ profile["model"] = ids[0]
13275
+ profile["selection"] = f"{profile.get('id', '')}::{profile.get('model', '')}"
13276
+ if settings:
13277
+ profile["model_settings"] = settings
13278
+ return changed
13279
+
13280
+
13281
+ def probe_and_merge_model_profiles(
13282
+ profiles: object,
13283
+ *,
13284
+ force_refresh: bool = True,
13285
+ ) -> bool:
13286
+ """Probe every configured provider and merge its complete model directory.
13287
+
13288
+ Import/reset paths use synchronous probing so a newly imported profile is
13289
+ immediately usable with every model the provider exposes. Network errors
13290
+ leave the explicitly configured models intact; the catalog can retry later.
13291
+ """
13292
+ if isinstance(profiles, dict):
13293
+ rows = list(profiles.values())
13294
+ elif isinstance(profiles, list):
13295
+ rows = profiles
13296
+ else:
13297
+ return False
13298
+ valid_rows = [profile for profile in rows if isinstance(profile, dict)]
13299
+ if not valid_rows:
13300
+ return False
13301
+ records_by_index: dict[int, list[dict]] = {}
13302
+
13303
+ def _probe(profile: dict) -> list[dict]:
13304
+ try:
13305
+ return probe_provider_models(
13306
+ profile,
13307
+ force_refresh=bool(force_refresh),
13308
+ background=False,
13309
+ )
13310
+ except Exception:
13311
+ return []
13312
+
13313
+ # Providers are independent network services. Probe them concurrently so
13314
+ # an unavailable vendor contributes one timeout instead of serially
13315
+ # delaying every other imported profile.
13316
+ with concurrent.futures.ThreadPoolExecutor(
13317
+ max_workers=min(8, len(valid_rows)),
13318
+ thread_name_prefix="provider-model-import",
13319
+ ) as executor:
13320
+ future_map = {
13321
+ executor.submit(_probe, profile): index
13322
+ for index, profile in enumerate(valid_rows)
13323
+ }
13324
+ for future in concurrent.futures.as_completed(future_map):
13325
+ index = future_map[future]
13326
+ try:
13327
+ records_by_index[index] = future.result()
13328
+ except Exception:
13329
+ records_by_index[index] = []
13330
+
13331
+ changed = False
13332
+ for index, profile in enumerate(valid_rows):
13333
+ changed = merge_probed_models_into_profile(
13334
+ profile, records_by_index.get(index, [])
13335
+ ) or changed
13336
+ return changed
13337
+
13338
+
13339
+ def normalize_model_runtime_settings(raw: object) -> dict:
13340
+ if not isinstance(raw, dict):
13341
+ return {}
13342
+ out: dict = {}
13343
+ for key in MODEL_RUNTIME_SETTING_KEYS:
13344
+ if key not in raw:
13345
+ continue
13346
+ value = raw.get(key)
13347
+ if key in {"thinking_stream", "response_stream", "reasoning_supported"}:
13348
+ if key == "reasoning_supported" and value is None:
13349
+ continue
13350
+ out[key] = _to_bool_like(value, default=False)
13351
+ elif key in {"effort", "max_effort", "reasoning_style"}:
13352
+ text = str(value or "").strip().lower()
13353
+ if key == "reasoning_style" or text in EFFORT_ORDER or not text:
13354
+ out[key] = text
13355
+ elif key == "temperature":
13356
+ try:
13357
+ out[key] = max(0.0, min(2.0, float(value)))
13358
+ except Exception:
13359
+ continue
13360
+ elif key == "request_timeout":
13361
+ out[key] = normalize_timeout_seconds(value, minimum=MIN_TIMEOUT_SECONDS, maximum=MAX_TIMEOUT_SECONDS, fallback=DEFAULT_REQUEST_TIMEOUT)
13362
+ return out
13363
+
13364
+
13365
+ def apply_model_runtime_settings(profile: dict, model: str, settings: object = None) -> dict:
13366
+ row = dict(profile or {})
13367
+ target = str(model or row.get("model", "") or "").strip()
13368
+ per_model = row.get("model_settings")
13369
+ if not isinstance(per_model, dict):
13370
+ per_model = {}
13371
+ merged = normalize_model_runtime_settings(per_model.get(target, {}))
13372
+ merged.update(normalize_model_runtime_settings(settings))
13373
+ if merged:
13374
+ per_model[target] = dict(merged)
13375
+ row["model_settings"] = per_model
13376
+ row["model"] = target or str(row.get("model", "") or "")
13377
+ row["selection"] = f"{row.get('id', '')}::{row.get('model', '')}"
13378
+ return row
13379
+
13380
+
13381
+ def model_runtime_settings_for(profile: dict, model: str = "") -> dict:
13382
+ """Return the effective settings for one model without exposing secrets."""
13383
+ row = profile if isinstance(profile, dict) else {}
13384
+ target = str(model or row.get("model", "") or "").strip()
13385
+ out = normalize_model_runtime_settings(row)
13386
+ per_model = row.get("model_settings")
13387
+ if isinstance(per_model, dict):
13388
+ out.update(normalize_model_runtime_settings(per_model.get(target, {})))
13389
+ return out
13390
+
13391
+
13392
+ def apply_model_option_runtime_fields(option: dict, profile: dict, model: str = "") -> dict:
13393
+ """Overlay per-model runtime settings on a public catalog option."""
13394
+ target = str(model or option.get("model", "") or "").strip()
13395
+ settings = model_runtime_settings_for(profile, target)
13396
+ for key in MODEL_RUNTIME_SETTING_KEYS:
13397
+ if key in settings:
13398
+ option[key] = settings[key]
13399
+ if isinstance(profile, dict):
13400
+ option.setdefault("display_name", str(profile.get("display_name", profile.get("label", "")) or ""))
13401
+ option.setdefault("title", str(profile.get("title", profile.get("label", "")) or ""))
13402
+ return option
13403
+
12791
13404
  # ============================================================================
12792
13405
  # Architecture / 架构 / アーキテクチャ
12793
13406
  # Layer 2: Model/provider profile loading and normalization.
@@ -12863,6 +13476,19 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
12863
13476
  if not isinstance(model_caps_map, dict):
12864
13477
  model_caps_map = {}
12865
13478
 
13479
+ # Reasoning metadata is kept separate from multimodal capabilities. The
13480
+ # importer writes this map from provider model probes, and it must survive
13481
+ # a config round-trip so model-specific effort controls remain available
13482
+ # after restart.
13483
+ raw_reasoning_map = config.get(
13484
+ "model_reasoning_capabilities",
13485
+ config.get("reasoning_capabilities", {}),
13486
+ )
13487
+ if isinstance(raw_reasoning_map, str):
13488
+ raw_reasoning_map = parse_json_object(raw_reasoning_map, {})
13489
+ if not isinstance(raw_reasoning_map, dict):
13490
+ raw_reasoning_map = {}
13491
+
12866
13492
  raw_global_caps = config.get("multimodal_capabilities", config.get("capabilities", {}))
12867
13493
  if isinstance(raw_global_caps, str):
12868
13494
  raw_global_caps = parse_json_object(raw_global_caps, {})
@@ -12963,6 +13589,30 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
12963
13589
  return {str(k): str(v) for k, v in parsed.items()}
12964
13590
  return {}
12965
13591
 
13592
+ def parse_profile_model_settings(raw: object, model_ids: list[str] | None = None) -> dict:
13593
+ """Normalize per-model runtime settings from config without secrets."""
13594
+ if isinstance(raw, str):
13595
+ raw = parse_json_object(raw, {})
13596
+ if not isinstance(raw, dict):
13597
+ return {}
13598
+ settings: dict[str, dict] = {}
13599
+ # A flat settings object applies to the profile's default model.
13600
+ if any(key in MODEL_RUNTIME_SETTING_KEYS for key in raw):
13601
+ target = str((model_ids or [""])[0] or "").strip()
13602
+ if target:
13603
+ normalized = normalize_model_runtime_settings(raw)
13604
+ if normalized:
13605
+ settings[target] = normalized
13606
+ return settings
13607
+ for target, value in raw.items():
13608
+ model_id = str(target or "").strip()
13609
+ if not model_id or not isinstance(value, dict):
13610
+ continue
13611
+ normalized = normalize_model_runtime_settings(value)
13612
+ if normalized:
13613
+ settings[model_id] = normalized
13614
+ return settings
13615
+
12966
13616
  def add_profile(
12967
13617
  out: list[dict],
12968
13618
  *,
@@ -12970,6 +13620,9 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
12970
13620
  provider: str,
12971
13621
  label: str,
12972
13622
  model: str = "",
13623
+ models: list[str] | None = None,
13624
+ display_name: str = "",
13625
+ title: str = "",
12973
13626
  base_url: str = "",
12974
13627
  endpoint: str = "",
12975
13628
  api_key: str = "",
@@ -12984,6 +13637,9 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
12984
13637
  source: str = "config",
12985
13638
  effort: str = "",
12986
13639
  max_effort: str = "",
13640
+ reasoning_supported: bool | None = None,
13641
+ reasoning_style: str = "",
13642
+ model_settings: dict | None = None,
12987
13643
  ):
12988
13644
  out.append(
12989
13645
  {
@@ -12991,6 +13647,9 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
12991
13647
  "provider": provider,
12992
13648
  "label": label,
12993
13649
  "model": (model or "").strip(),
13650
+ "models": [str(x).strip() for x in (models or []) if str(x).strip()],
13651
+ "display_name": str(display_name or label or "").strip(),
13652
+ "title": str(title or label or "").strip(),
12994
13653
  "base_url": (base_url or "").strip(),
12995
13654
  "endpoint": (endpoint or "").strip(),
12996
13655
  "api_key": (api_key or "").strip(),
@@ -13009,12 +13668,20 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
13009
13668
  "media_endpoints": media_endpoints or {},
13010
13669
  "effort": str(effort or "").strip().lower(),
13011
13670
  "max_effort": str(max_effort or "").strip().lower(),
13671
+ "reasoning_supported": reasoning_supported,
13672
+ "reasoning_style": str(reasoning_style or "").strip().lower(),
13673
+ "model_settings": {
13674
+ str(model_id).strip(): normalize_model_runtime_settings(values)
13675
+ for model_id, values in (model_settings or {}).items()
13676
+ if str(model_id).strip() and normalize_model_runtime_settings(values)
13677
+ },
13012
13678
  "source": source,
13013
13679
  }
13014
13680
  )
13015
13681
 
13016
13682
  profiles: list[dict] = []
13017
- provider = str(config.get("provider", "")).strip().lower()
13683
+ config_provider = normalize_profile_provider(str(config.get("provider", "")))
13684
+ provider = config_provider
13018
13685
  temp = float(config.get("temperature", 0.2) or 0.2)
13019
13686
  timeout = normalize_timeout_seconds(
13020
13687
  config.get("request_timeout", DEFAULT_REQUEST_TIMEOUT),
@@ -13098,6 +13765,12 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
13098
13765
  ).strip()
13099
13766
  if not provider and (base_hint or endpoint):
13100
13767
  provider = "openai_compat"
13768
+ # Arbitrary provider names are transported through the custom HTTP
13769
+ # adapter while preserving the user supplied name for display.
13770
+ provider_name = str(provider_hint or "").strip()
13771
+ if provider and provider not in {"ollama", "anthropic", "custom_http"} and not is_openai_like_provider(provider) and (base_hint or endpoint):
13772
+ provider_name = provider_name or provider
13773
+ provider = "custom_http"
13101
13774
  if not provider:
13102
13775
  continue
13103
13776
  profile_id = sanitize_profile_id(
@@ -13112,9 +13785,12 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
13112
13785
  )
13113
13786
  label = str(
13114
13787
  raw_profile.get("label")
13788
+ or raw_profile.get("custom_name")
13115
13789
  or raw_profile.get("name")
13116
13790
  or raw_profile.get("title")
13791
+ or raw_profile.get("custom_title")
13117
13792
  or raw_profile.get("display_name")
13793
+ or provider_name
13118
13794
  or profile_id
13119
13795
  ).strip() or profile_id
13120
13796
  model = str(
@@ -13123,8 +13799,32 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
13123
13799
  or raw_profile.get("default_model")
13124
13800
  or ""
13125
13801
  ).strip()
13802
+ raw_models = raw_profile.get("models", raw_profile.get("model_ids", raw_profile.get("available_models", [])))
13803
+ if isinstance(raw_models, str):
13804
+ raw_models = [x.strip() for x in re.split(r"[,\n]", raw_models) if x.strip()]
13805
+ if not isinstance(raw_models, list):
13806
+ raw_models = []
13807
+ model_settings: dict[str, dict] = {}
13808
+ model_values: list[str] = []
13809
+ for value in raw_models:
13810
+ if isinstance(value, dict):
13811
+ model_id = str(value.get("id") or value.get("model") or value.get("name") or "").strip()
13812
+ if not model_id:
13813
+ continue
13814
+ model_values.append(model_id)
13815
+ inline_settings = normalize_model_runtime_settings(value)
13816
+ if inline_settings:
13817
+ model_settings[model_id] = inline_settings
13818
+ elif str(value).strip():
13819
+ model_values.append(str(value).strip())
13820
+ models = list(dict.fromkeys(model_values))
13821
+ if model and model not in models:
13822
+ models.insert(0, model)
13126
13823
  if not model:
13127
- model = default_model_for_provider(provider)
13824
+ # A supplied model directory is already authoritative. Do not
13825
+ # prepend a synthetic provider default such as ``custom-model``.
13826
+ model = models[0] if models else default_model_for_provider(provider)
13827
+ model_settings.update(parse_profile_model_settings(raw_profile.get("model_settings"), [model] + models))
13128
13828
  base_url = extract_base_url(base_hint or endpoint)
13129
13829
  if provider == "ollama" and not base_url:
13130
13830
  base_url = extract_base_url(default_ollama_url)
@@ -13171,6 +13871,20 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
13171
13871
  provider=provider,
13172
13872
  label=label,
13173
13873
  model=model,
13874
+ models=models,
13875
+ display_name=str(
13876
+ raw_profile.get("display_name")
13877
+ or raw_profile.get("provider_name")
13878
+ or raw_profile.get("custom_name")
13879
+ or (raw_profile.get("name") if provider == "custom_http" else "")
13880
+ or (provider_name if provider_name and provider_name.lower() not in {"custom_http", "openai_compat"} else "")
13881
+ or label
13882
+ ),
13883
+ title=str(
13884
+ raw_profile.get("title")
13885
+ or raw_profile.get("custom_title")
13886
+ or label
13887
+ ),
13174
13888
  base_url=base_url,
13175
13889
  endpoint=endpoint,
13176
13890
  api_key=api_key,
@@ -13197,6 +13911,9 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
13197
13911
  media_endpoints=media_endpoints,
13198
13912
  effort=str(raw_profile.get("effort", effort_default) or effort_default),
13199
13913
  max_effort=str(raw_profile.get("max_effort", max_effort_default) or max_effort_default),
13914
+ reasoning_supported=(raw_profile.get("reasoning_supported") if isinstance(raw_profile.get("reasoning_supported"), bool) else None),
13915
+ reasoning_style=str(raw_profile.get("reasoning_style") or raw_profile.get("reasoning_dialect") or ""),
13916
+ model_settings=model_settings,
13200
13917
  source=str(raw_profile.get("source", "profiles") or "profiles"),
13201
13918
  )
13202
13919
  if bool(raw_profile.get("default")) or bool(raw_profile.get("active")) or bool(raw_profile.get("selected")):
@@ -13405,17 +14122,60 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
13405
14122
  media_endpoints=build_profile_media_endpoints("openrouter"),
13406
14123
  )
13407
14124
 
13408
- custom_url = str(config.get("custom_url", "")).strip()
14125
+ custom_url = str(
14126
+ config.get("custom_url")
14127
+ or (config.get("provider_url") if config_provider == "custom_http" else "")
14128
+ or (config.get("url") if config_provider == "custom_http" else "")
14129
+ or ""
14130
+ ).strip()
13409
14131
  custom_key = str(config.get("custom_key", "")).strip()
13410
14132
  custom_headers = parse_json_object(str(config.get("custom_headers", "{}") or "{}"), {})
13411
14133
  custom_payload = str(config.get("custom_payload", "") or "").strip()
13412
14134
  if custom_url:
14135
+ raw_custom_models = config.get(
14136
+ "custom_models",
14137
+ config.get(
14138
+ "custom_model_ids",
14139
+ config.get("models", []) if config_provider == "custom_http" else [],
14140
+ ),
14141
+ )
14142
+ if isinstance(raw_custom_models, str):
14143
+ raw_custom_models = [x.strip() for x in re.split(r"[,\n]", raw_custom_models) if x.strip()]
14144
+ custom_models = [str(x).strip() for x in raw_custom_models] if isinstance(raw_custom_models, list) else []
14145
+ custom_model = str(
14146
+ config.get("custom_model", "")
14147
+ or (config.get("model", "") if config_provider == "custom_http" else "")
14148
+ or config.get("openai_model", "")
14149
+ ).strip()
14150
+ if not custom_model and custom_models:
14151
+ custom_model = custom_models[0]
14152
+ if not custom_model:
14153
+ custom_model = "custom-model"
14154
+ if custom_model and custom_model not in custom_models:
14155
+ custom_models.insert(0, custom_model)
13413
14156
  add_profile(
13414
14157
  profiles,
13415
14158
  profile_id="custom",
13416
14159
  provider=normalize_profile_provider("custom_http"),
13417
- label="Custom HTTP",
13418
- model=str(config.get("custom_model", "") or config.get("openai_model", "") or "custom-model"),
14160
+ label=str(
14161
+ config.get("custom_name")
14162
+ or (config.get("provider_name") if config_provider == "custom_http" else "")
14163
+ or config.get("custom_title")
14164
+ or "Custom HTTP"
14165
+ ),
14166
+ model=custom_model,
14167
+ models=custom_models,
14168
+ display_name=str(
14169
+ config.get("custom_name")
14170
+ or (config.get("provider_name") if config_provider == "custom_http" else "")
14171
+ or "Custom HTTP"
14172
+ ),
14173
+ title=str(
14174
+ config.get("custom_title")
14175
+ or (config.get("provider_title") if config_provider == "custom_http" else "")
14176
+ or config.get("custom_name")
14177
+ or "Custom HTTP"
14178
+ ),
13419
14179
  base_url=extract_base_url(custom_url),
13420
14180
  endpoint=custom_url,
13421
14181
  api_key=custom_key,
@@ -13446,6 +14206,44 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
13446
14206
  source="default",
13447
14207
  )
13448
14208
 
14209
+ # Legacy provider keys may carry a comma/newline separated model list.
14210
+ # Keep the configured default first, then let the catalog append probed
14211
+ # models without creating duplicate profiles.
14212
+ provider_model_keys = {
14213
+ "ollama": "ollama_models", "openai_compat": "openai_models",
14214
+ "siliconflow": "siliconflow_models", "vllm": "vllm_models",
14215
+ "lmstudio": "lmstudio_models", "anthropic": "anthropic_models",
14216
+ "glm": "glm_models", "kimi": "kimi_models", "openrouter": "openrouter_models",
14217
+ "custom_http": "custom_models",
14218
+ }
14219
+ for profile in profiles:
14220
+ prov = str(profile.get("provider", "") or "").lower()
14221
+ model_list_key = provider_model_keys.get(prov, "")
14222
+ raw_list = (
14223
+ config.get(
14224
+ model_list_key,
14225
+ config.get("models", []) if config_provider == prov else [],
14226
+ )
14227
+ if model_list_key
14228
+ else []
14229
+ )
14230
+ if isinstance(raw_list, str):
14231
+ raw_list = [x.strip() for x in re.split(r"[,\n]", raw_list) if x.strip()]
14232
+ existing = [str(x).strip() for x in profile.get("models", []) if str(x).strip()]
14233
+ if not existing and isinstance(raw_list, list):
14234
+ existing = [str(x).strip() for x in raw_list if str(x).strip()]
14235
+ default_model = str(profile.get("model", "") or "").strip()
14236
+ # ``auto`` is a transport placeholder for local OpenAI-compatible
14237
+ # servers. Once a model directory is available, make its first
14238
+ # discovered entry the active model so importing a directory is
14239
+ # immediately runnable and stable across restarts.
14240
+ if existing and default_model.lower() == "auto":
14241
+ default_model = existing[0]
14242
+ profile["model"] = default_model
14243
+ if default_model and default_model not in existing:
14244
+ existing.insert(0, default_model)
14245
+ profile["models"] = existing
14246
+
13449
14247
  active_map = {
13450
14248
  "ollama": "ollama",
13451
14249
  "openai": "openai",
@@ -13466,7 +14264,7 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
13466
14264
  default_profile_id = candidate
13467
14265
  break
13468
14266
  if not default_profile_id:
13469
- default_profile_id = active_map.get(provider, "")
14267
+ default_profile_id = active_map.get(config_provider or provider, "")
13470
14268
  if not default_profile_id or default_profile_id not in profile_ids:
13471
14269
  # Fallback: first non-ollama profile that was explicitly configured
13472
14270
  for p in profiles:
@@ -13485,6 +14283,51 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
13485
14283
  p["effort"] = effort_default
13486
14284
  if max_effort_default and not str(p.get("max_effort", "") or "").strip():
13487
14285
  p["max_effort"] = max_effort_default
14286
+ raw_runtime_settings = config.get(
14287
+ "model_settings",
14288
+ config.get("model_runtime_settings", config.get("runtime_settings", {})),
14289
+ )
14290
+ if isinstance(raw_runtime_settings, str):
14291
+ raw_runtime_settings = parse_json_object(raw_runtime_settings, {})
14292
+ if isinstance(raw_runtime_settings, dict):
14293
+ for profile in profiles:
14294
+ current = dict(profile.get("model_settings", {}) or {})
14295
+ profile_model = str(profile.get("model", "") or "").strip()
14296
+ provider_id = str(profile.get("id", "") or "").strip()
14297
+ provider_name = str(profile.get("provider", "") or "").strip()
14298
+ for model_id, value in raw_reasoning_map.items():
14299
+ normalized = normalize_model_runtime_settings(value)
14300
+ target = str(model_id or "").strip()
14301
+ if target and normalized:
14302
+ current[target] = {**current.get(target, {}), **normalized}
14303
+ for key in (profile_model, provider_id, provider_name):
14304
+ value = raw_runtime_settings.get(key) if key else None
14305
+ if isinstance(value, dict) and any(k in MODEL_RUNTIME_SETTING_KEYS for k in value):
14306
+ normalized = normalize_model_runtime_settings(value)
14307
+ if profile_model and normalized:
14308
+ current[profile_model] = {**current.get(profile_model, {}), **normalized}
14309
+ for model_id, value in raw_runtime_settings.items():
14310
+ if not isinstance(value, dict):
14311
+ continue
14312
+ normalized = normalize_model_runtime_settings(value)
14313
+ if normalized:
14314
+ current[str(model_id).strip()] = {**current.get(str(model_id).strip(), {}), **normalized}
14315
+ elif str(model_id) in {provider_id, provider_name}:
14316
+ for nested_model, nested_value in value.items():
14317
+ nested = normalize_model_runtime_settings(nested_value)
14318
+ if nested:
14319
+ target = str(nested_model or "").strip()
14320
+ current[target] = {**current.get(target, {}), **nested}
14321
+ profile["model_settings"] = current
14322
+ elif raw_reasoning_map:
14323
+ for profile in profiles:
14324
+ current = dict(profile.get("model_settings", {}) or {})
14325
+ for model_id, value in raw_reasoning_map.items():
14326
+ target = str(model_id or "").strip()
14327
+ normalized = normalize_model_runtime_settings(value)
14328
+ if target and normalized:
14329
+ current[target] = {**current.get(target, {}), **normalized}
14330
+ profile["model_settings"] = current
13488
14331
  return {"profiles": profiles, "default_profile_id": default_profile_id}
13489
14332
 
13490
14333
  def looks_like_llm_config(config: dict) -> bool:
@@ -13526,13 +14369,23 @@ def looks_like_llm_config(config: dict) -> bool:
13526
14369
  "openrouter_model",
13527
14370
  "openrouter_key",
13528
14371
  "custom_url",
14372
+ "provider_url",
14373
+ "provider_name",
14374
+ "provider_title",
14375
+ "name",
14376
+ "title",
14377
+ "custom_name",
14378
+ "custom_title",
13529
14379
  "custom_model",
14380
+ "custom_models",
13530
14381
  "custom_key",
13531
14382
  "custom_headers",
13532
14383
  "custom_payload",
13533
14384
  "capabilities",
13534
14385
  "multimodal_capabilities",
13535
14386
  "model_capabilities",
14387
+ "model_reasoning_capabilities",
14388
+ "reasoning_capabilities",
13536
14389
  "media_endpoints",
13537
14390
  "ollama_capabilities",
13538
14391
  "openai_capabilities",
@@ -13562,6 +14415,8 @@ def looks_like_llm_config(config: dict) -> bool:
13562
14415
  "custom_video_endpoint",
13563
14416
  "temperature",
13564
14417
  "request_timeout",
14418
+ "model_settings",
14419
+ "model_runtime_settings",
13565
14420
  }
13566
14421
  return bool(keys & markers)
13567
14422
 
@@ -25418,6 +26273,8 @@ class OllamaClient:
25418
26273
  self.thinking_stream = bool(thinking_stream)
25419
26274
  self.response_stream = bool(response_stream)
25420
26275
  self.capabilities = default_multimodal_capabilities()
26276
+ self.reasoning_supported: bool | None = None
26277
+ self.reasoning_style: str = ""
25421
26278
  self.media_endpoints: dict[str, str] = {}
25422
26279
  self.embed_model: str = "" # embedding model name, e.g. "nomic-embed-text"
25423
26280
  self.telemetry_callback = None
@@ -25755,6 +26612,8 @@ class OllamaClient:
25755
26612
  infer_model_multimodal_capabilities(self.provider, self.model),
25756
26613
  declared_caps,
25757
26614
  )
26615
+ self.reasoning_supported = profile.get("reasoning_supported") if isinstance(profile.get("reasoning_supported"), bool) else None
26616
+ self.reasoning_style = str(profile.get("reasoning_style", "") or "").strip().lower()
25758
26617
  self.media_endpoints = parse_media_endpoints(profile.get("media_endpoints", {}))
25759
26618
  if "embed_model" in profile:
25760
26619
  self.embed_model = str(profile.get("embed_model") or "").strip()
@@ -27669,7 +28528,14 @@ class OllamaClient:
27669
28528
  # Resolve provider-neutral effort into native reasoning mutations once.
27670
28529
  # probe_mode never reasons (it is a cheap capability ping).
27671
28530
  reasoning = {} if probe_mode else resolve_reasoning_payload(
27672
- self.provider, self.model, effort, max_tokens=max_tokens,
28531
+ self.provider,
28532
+ self.model,
28533
+ effort,
28534
+ max_tokens=max_tokens,
28535
+ capabilities={
28536
+ "reasoning_supported": self.reasoning_supported,
28537
+ "reasoning_style": self.reasoning_style,
28538
+ },
27673
28539
  )
27674
28540
  if reasoning.get("max_tokens"):
27675
28541
  max_tokens = int(reasoning["max_tokens"])
@@ -27795,7 +28661,11 @@ class OllamaClient:
27795
28661
  # thinking by default when the field is omitted. Send the explicit
27796
28662
  # boolean for models that advertise the native switch so a bounded
27797
28663
  # no-thinking compatibility turn can actually produce a tool call.
27798
- if model_reasoning_style(provider, self.model) == "ollama":
28664
+ if provider == "ollama" and self.reasoning_supported is not False:
28665
+ # Ollama accepts an explicit boolean on native chat. Unknown
28666
+ # capability metadata stays conservative because the caller's
28667
+ # ``think`` value defaults to false; no model-name inference is
28668
+ # used to turn reasoning on.
27799
28669
  native_payload["think"] = bool(think)
27800
28670
  if tools:
27801
28671
  native_payload["tools"] = tools
@@ -29472,7 +30342,7 @@ class SessionState:
29472
30342
  l2_todo_policy if l2_todo_policy is not None else (cfg_l2_todo_policy or DEFAULT_L2_TODO_POLICY)
29473
30343
  )
29474
30344
  self.runtime_requires_todos: bool | None = None
29475
- self._todowrite_step_counter: dict[str, int] = {} # Fix 5: track consecutive TodoWrite per step for loop detection
30345
+ self._todowrite_step_counter: dict[str, int] = {} # Consecutive no-progress writes by task, step, and role.
29476
30346
  self.runtime_scale_preference = "balanced"
29477
30347
  self.runtime_direct_objective = ""
29478
30348
  # Full, lossless source request for the active task. Derived runtime
@@ -30405,7 +31275,14 @@ class SessionState:
30405
31275
  profile = self.model_profiles.get(self.active_profile_id)
30406
31276
  if not profile:
30407
31277
  return
30408
- row = dict(profile)
31278
+ stored = dict(profile)
31279
+ row = dict(stored)
31280
+ # Restore the selected model's persisted runtime controls before the
31281
+ # client is configured. This keeps effort/streaming identical after a
31282
+ # restart and when a session is loaded by the IDE.
31283
+ active_model = str(row.get("model", "") or "").strip()
31284
+ for key, value in model_runtime_settings_for(row, active_model).items():
31285
+ row[key] = value
30409
31286
  timeout_floor = max(MIN_TIMEOUT_SECONDS, int(self.max_run_seconds or 0))
30410
31287
  row["request_timeout"] = normalize_timeout_seconds(
30411
31288
  row.get("request_timeout", DEFAULT_REQUEST_TIMEOUT),
@@ -30420,9 +31297,13 @@ class SessionState:
30420
31297
  if cached_caps:
30421
31298
  merged = merge_multimodal_capabilities(self._capabilities_from_profile(row), cached_caps)
30422
31299
  row["capabilities"] = merged
30423
- self.model_profiles[self.active_profile_id] = row
30424
- profile = row
30425
- self.ollama.apply_profile(profile)
31300
+ stored["capabilities"] = merged
31301
+ stored["request_timeout"] = row["request_timeout"]
31302
+ self.model_profiles[self.active_profile_id] = stored
31303
+ # Keep per-model controls in ``model_settings``. Applying them to the
31304
+ # client must not promote them to profile-level defaults, otherwise a
31305
+ # later model switch would inherit the previous model's settings.
31306
+ self.ollama.apply_profile(row)
30426
31307
  self.thinking = False
30427
31308
 
30428
31309
  def _profile_is_runnable(self, profile: dict) -> bool:
@@ -30488,6 +31369,7 @@ class SessionState:
30488
31369
  if force_probe:
30489
31370
  self._ensure_active_profile_capabilities(force_probe=True)
30490
31371
  opts = []
31372
+ profiles_changed = False
30491
31373
  ollama_profile_id = ""
30492
31374
  ollama_base = self.ollama.base_url
30493
31375
  for pid, profile in sorted(self.model_profiles.items(), key=lambda x: x[0]):
@@ -30511,6 +31393,29 @@ class SessionState:
30511
31393
  "thinking_hint": bool(profile.get("thinking_hint", False)),
30512
31394
  "thinking_stream": bool(profile.get("thinking_stream", False)),
30513
31395
  "response_stream": bool(profile.get("response_stream", False)),
31396
+ "effort": str(profile.get("effort", "") or ""),
31397
+ "max_effort": str(profile.get("max_effort", "") or ""),
31398
+ "reasoning_supported": (
31399
+ profile.get("reasoning_supported")
31400
+ if profile.get("reasoning_supported") is not None
31401
+ else model_reasoning_style(
31402
+ str(profile.get("provider", "")),
31403
+ model,
31404
+ caps,
31405
+ )
31406
+ != "none"
31407
+ ),
31408
+ "reasoning_style": str(
31409
+ profile.get("reasoning_style")
31410
+ or model_reasoning_style(
31411
+ str(profile.get("provider", "")),
31412
+ model,
31413
+ caps,
31414
+ )
31415
+ or ""
31416
+ ),
31417
+ "display_name": str(profile.get("display_name", profile.get("label", pid)) or ""),
31418
+ "title": str(profile.get("title", profile.get("label", pid)) or ""),
30514
31419
  "capabilities": caps,
30515
31420
  "capabilities_probed": bool(cache_entry),
30516
31421
  "capabilities_probed_at": float(cache_entry.get("updated_at", 0.0) or 0.0)
@@ -30527,7 +31432,24 @@ class SessionState:
30527
31432
  base = str(profile.get("base_url", self.ollama.base_url) or self.ollama.base_url).strip()
30528
31433
  if not base:
30529
31434
  continue
30530
- tags = list_ollama_models_cached(base, force_refresh=bool(force_probe))
31435
+ tags = list_ollama_models_cached(
31436
+ base,
31437
+ force_refresh=bool(force_probe),
31438
+ background=not force_probe,
31439
+ )
31440
+ ollama_records = probe_provider_models(
31441
+ profile,
31442
+ force_refresh=bool(force_probe),
31443
+ background=not force_probe,
31444
+ )
31445
+ profiles_changed = merge_probed_models_into_profile(
31446
+ profile, ollama_records, model_ids=tags
31447
+ ) or profiles_changed
31448
+ ollama_record_map = {
31449
+ str(record.get("id", "")): record
31450
+ for record in ollama_records
31451
+ if isinstance(record, dict) and str(record.get("id", "")).strip()
31452
+ }
30531
31453
  if tags:
30532
31454
  if pid == self.active_profile_id:
30533
31455
  self.ollama_env_tags = list(tags)
@@ -30543,6 +31465,13 @@ class SessionState:
30543
31465
  tag_cache_key = self._profile_cache_key(tag_profile, tag)
30544
31466
  tag_cache_entry = self.multimodal_capability_cache.get(tag_cache_key, {})
30545
31467
  tag_caps = self._capabilities_from_profile(tag_profile, model_override=tag)
31468
+ tag_record = ollama_record_map.get(tag)
31469
+ tag_record_caps = (
31470
+ tag_record.get("capabilities", {})
31471
+ if isinstance(tag_record, dict)
31472
+ else {}
31473
+ )
31474
+ tag_record_probed = isinstance(tag_record, dict)
30546
31475
  opts.append(
30547
31476
  {
30548
31477
  "selection": selection,
@@ -30554,6 +31483,35 @@ class SessionState:
30554
31483
  "thinking_hint": bool(profile.get("thinking_hint", self.thinking)),
30555
31484
  "thinking_stream": bool(profile.get("thinking_stream", self.ollama.thinking_stream)),
30556
31485
  "response_stream": bool(profile.get("response_stream", self.ollama.response_stream)),
31486
+ "effort": str(profile.get("effort", "") or ""),
31487
+ "max_effort": str(profile.get("max_effort", "") or ""),
31488
+ "reasoning_supported": (
31489
+ profile.get("reasoning_supported")
31490
+ if profile.get("reasoning_supported") is not None
31491
+ else tag_record_caps.get("reasoning_supported")
31492
+ if isinstance(tag_record_caps, dict)
31493
+ and tag_record_caps.get("reasoning_supported") is not None
31494
+ else model_reasoning_style(
31495
+ str(profile.get("provider", "")),
31496
+ tag,
31497
+ tag_caps,
31498
+ capabilities_probed=tag_record_probed,
31499
+ )
31500
+ != "none"
31501
+ ),
31502
+ "reasoning_style": str(
31503
+ profile.get("reasoning_style")
31504
+ or tag_record_caps.get("reasoning_style", "")
31505
+ or model_reasoning_style(
31506
+ str(profile.get("provider", "")),
31507
+ tag,
31508
+ tag_caps,
31509
+ capabilities_probed=tag_record_probed,
31510
+ )
31511
+ or ""
31512
+ ),
31513
+ "display_name": str(profile.get("display_name", profile.get("label", pid)) or ""),
31514
+ "title": str(profile.get("title", profile.get("label", pid)) or ""),
30557
31515
  "capabilities": tag_caps,
30558
31516
  "capabilities_probed": bool(tag_cache_entry),
30559
31517
  "capabilities_probed_at": float(tag_cache_entry.get("updated_at", 0.0) or 0.0)
@@ -30561,6 +31519,69 @@ class SessionState:
30561
31519
  else 0.0,
30562
31520
  }
30563
31521
  )
31522
+ # Expand every provider from configured model lists and bounded probes.
31523
+ # The cache prevents status polling from repeatedly hitting vendors.
31524
+ for pid, profile in sorted(self.model_profiles.items(), key=lambda x: x[0]):
31525
+ configured = [str(x).strip() for x in profile.get("models", []) if str(x).strip()]
31526
+ records = probe_provider_models(
31527
+ profile,
31528
+ force_refresh=bool(force_probe),
31529
+ background=not force_probe,
31530
+ )
31531
+ profiles_changed = merge_probed_models_into_profile(profile, records) or profiles_changed
31532
+ for rec in records:
31533
+ model_id = str(rec.get("id", "") or "").strip()
31534
+ if model_id and model_id not in configured:
31535
+ configured.append(model_id)
31536
+ for model_id in configured:
31537
+ selection = f"{pid}::{model_id}"
31538
+ if not model_id or selection in seen:
31539
+ continue
31540
+ seen.add(selection)
31541
+ rec = next((r for r in records if str(r.get("id", "")) == model_id), {})
31542
+ rcaps = rec.get("capabilities", {}) if isinstance(rec, dict) else {}
31543
+ opts.append({
31544
+ "selection": selection, "profile_id": pid,
31545
+ "provider": profile.get("provider", "unknown"), "model": model_id,
31546
+ "label": f"{profile.get('label', pid)} | {model_id}",
31547
+ "source": "provider-probe" if rec else profile.get("source", ""),
31548
+ "thinking_hint": bool(profile.get("thinking_hint", False)),
31549
+ "thinking_stream": bool(profile.get("thinking_stream", False)),
31550
+ "response_stream": bool(profile.get("response_stream", False)),
31551
+ "effort": str(profile.get("effort", "") or ""),
31552
+ "max_effort": str(profile.get("max_effort", "") or ""),
31553
+ "reasoning_supported": (
31554
+ rcaps.get("reasoning_supported")
31555
+ if isinstance(rcaps, dict) and rcaps.get("reasoning_supported") is not None
31556
+ else profile.get("reasoning_supported")
31557
+ if profile.get("reasoning_supported") is not None
31558
+ else model_reasoning_style(
31559
+ str(profile.get("provider", "")),
31560
+ model_id,
31561
+ rcaps if isinstance(rcaps, dict) else None,
31562
+ capabilities_probed=bool(rec),
31563
+ )
31564
+ != "none"
31565
+ ),
31566
+ "reasoning_style": str(
31567
+ (rcaps.get("reasoning_style") if isinstance(rcaps, dict) else "")
31568
+ or profile.get("reasoning_style", "")
31569
+ or model_reasoning_style(
31570
+ str(profile.get("provider", "")),
31571
+ model_id,
31572
+ rcaps if isinstance(rcaps, dict) else None,
31573
+ capabilities_probed=bool(rec),
31574
+ )
31575
+ or ""
31576
+ ),
31577
+ "display_name": str(profile.get("display_name", profile.get("label", pid)) or ""),
31578
+ "title": str(profile.get("title", profile.get("label", pid)) or ""),
31579
+ "capabilities": self._capabilities_from_profile(dict(profile, model=model_id), model_override=model_id),
31580
+ })
31581
+ for _option in opts:
31582
+ _pid = str(_option.get("profile_id", "") or "")
31583
+ _profile = self.model_profiles.get(_pid, {})
31584
+ apply_model_option_runtime_fields(_option, _profile, str(_option.get("model", "") or ""))
30564
31585
  runnable_opts = [x for x in opts if self._option_is_runnable(x)]
30565
31586
  if runnable_opts:
30566
31587
  opts = runnable_opts
@@ -30582,6 +31603,27 @@ class SessionState:
30582
31603
  "thinking_hint": bool(active.get("thinking_hint", False)),
30583
31604
  "thinking_stream": bool(active.get("thinking_stream", False)),
30584
31605
  "response_stream": bool(active.get("response_stream", False)),
31606
+ "effort": str(active.get("effort", "") or ""),
31607
+ "max_effort": str(active.get("max_effort", "") or ""),
31608
+ "reasoning_supported": (
31609
+ active.get("reasoning_supported")
31610
+ if active.get("reasoning_supported") is not None
31611
+ else model_reasoning_style(
31612
+ str(active.get("provider", "")),
31613
+ str(active.get("model", "") or ""),
31614
+ active.get("capabilities", {}),
31615
+ )
31616
+ != "none"
31617
+ ),
31618
+ "reasoning_style": str(
31619
+ active.get("reasoning_style")
31620
+ or model_reasoning_style(
31621
+ str(active.get("provider", "")),
31622
+ str(active.get("model", "") or ""),
31623
+ active.get("capabilities", {}),
31624
+ )
31625
+ or ""
31626
+ ),
30585
31627
  "capabilities": self._capabilities_from_profile(active if isinstance(active, dict) else {}),
30586
31628
  "capabilities_probed": bool(active_cache_entry),
30587
31629
  "capabilities_probed_at": float(active_cache_entry.get("updated_at", 0.0) or 0.0)
@@ -30589,11 +31631,15 @@ class SessionState:
30589
31631
  else 0.0,
30590
31632
  },
30591
31633
  )
31634
+ apply_model_option_runtime_fields(opts[0], active, str(active.get("model", "") or ""))
30592
31635
  option_map.add(selected)
30593
31636
  if opts and selected not in option_map:
30594
31637
  selected = str(opts[0].get("selection", ""))
30595
31638
  active_caps = self._capabilities_from_profile(active if isinstance(active, dict) else {})
30596
31639
  active_cache = self.multimodal_capability_cache.get(self._profile_cache_key(active if isinstance(active, dict) else {}), {})
31640
+ if profiles_changed:
31641
+ self.updated_at = now_ts()
31642
+ self._persist()
30597
31643
  return {
30598
31644
  "provider": active.get("provider", "ollama"),
30599
31645
  "selected": selected,
@@ -30615,6 +31661,7 @@ class SessionState:
30615
31661
  model_override: str | None = None,
30616
31662
  *,
30617
31663
  reset_failures: bool = True,
31664
+ settings: dict | None = None,
30618
31665
  ) -> dict:
30619
31666
  raw = str(selection or "").strip()
30620
31667
  explicit_profile = "::" in raw
@@ -30645,6 +31692,11 @@ class SessionState:
30645
31692
  profile["model"] = str(model_override).strip()
30646
31693
  elif str(selected_model).strip():
30647
31694
  profile["model"] = str(selected_model).strip()
31695
+ profile = apply_model_runtime_settings(profile, str(profile.get("model", "") or ""), settings)
31696
+ for record in probe_provider_models(profile, background=True):
31697
+ if str(record.get("id", "")) == str(profile.get("model", "")):
31698
+ merge_probed_models_into_profile(profile, [record])
31699
+ break
30648
31700
  if not self._profile_is_runnable(profile) and not explicit_profile and str(selected_model).strip():
30649
31701
  fallback = self._pick_runnable_selection(preferred_provider="ollama")
30650
31702
  if fallback:
@@ -30725,6 +31777,9 @@ class SessionState:
30725
31777
  if cfg_user_memory_mode is not None:
30726
31778
  self.user_memory_mode = normalize_user_memory_mode(cfg_user_memory_mode)
30727
31779
  parsed = parse_llm_config_profiles(config, self.ollama.base_url, self.ollama.model)
31780
+ # A config import is also a model-directory import. Probe each provider
31781
+ # once so all advertised models are available to both UIs immediately.
31782
+ probe_and_merge_model_profiles(parsed.get("profiles", []), force_refresh=True)
30728
31783
  self.multimodal_capability_cache = {}
30729
31784
  OllamaClient.clear_global_probe_cache()
30730
31785
  self.ollama.clear_probe_cache()
@@ -49486,6 +50541,7 @@ body{padding:18px}
49486
50541
  self.set_runtime_selection(
49487
50542
  selection,
49488
50543
  model_override if isinstance(model_override, str) else None,
50544
+ settings=normalize_model_runtime_settings(payload.get("settings", payload)),
49489
50545
  )
49490
50546
  applied_notes.append(f"deferred model switch applied: {trim(selection, 120)}")
49491
50547
  sync_needed = True
@@ -49620,8 +50676,9 @@ body{padding:18px}
49620
50676
  prof = {}
49621
50677
  if not isinstance(prof, dict):
49622
50678
  prof = {}
49623
- default = str(prof.get("effort", "") or "").strip().lower()
49624
- ceiling = str(prof.get("max_effort", "") or "").strip().lower()
50679
+ effective = model_runtime_settings_for(prof, str(prof.get("model", "") or ""))
50680
+ default = str(effective.get("effort", "") or "").strip().lower()
50681
+ ceiling = str(effective.get("max_effort", "") or "").strip().lower()
49625
50682
  if ceiling not in EFFORT_ORDER:
49626
50683
  ceiling = EFFORT_MAX
49627
50684
  if default not in EFFORT_ORDER:
@@ -49735,6 +50792,13 @@ body{padding:18px}
49735
50792
  except Exception as exc:
49736
50793
  self.kernel_runtime_degraded = True
49737
50794
  self._emit("status", {"summary": f"liquid kernel hook degraded: {trim(str(exc), 160)}"})
50795
+ if tools is not None:
50796
+ guarded_tools = self._filter_todo_progress_tools(tools, role=context_role_hint)
50797
+ if len(guarded_tools) != len(tools):
50798
+ system = f"{system}\n\n{self._todo_no_progress_instruction()}"
50799
+ if canonicalize_tool_name(tool_choice) in {"TodoWrite", "TodoWriteRescue"}:
50800
+ tool_choice = ""
50801
+ tools = guarded_tools
49738
50802
  system = self._inject_runtime_environment_context(system)
49739
50803
  estimated_prompt_tokens = self._estimate_model_call_prompt_tokens(
49740
50804
  messages,
@@ -63566,6 +64630,15 @@ body{padding:18px}
63566
64630
  return {}
63567
64631
  bb = board if isinstance(board, dict) else self._ensure_blackboard()
63568
64632
  step_id = trim(str(plan_step.get("id", "") or ""), 60)
64633
+ # An unchanged, uniquely identified child needs no model classification.
64634
+ # Providers often resend the whole list without parent/row IDs.
64635
+ existing_rows = self._active_plan_worker_todo_rows(step_id, role="")
64636
+ if sum(
64637
+ normalize_work_text(str(existing.get("content", "") or "")).strip().casefold()
64638
+ == normalize_work_text(content).strip().casefold()
64639
+ for existing in existing_rows
64640
+ ) == 1:
64641
+ return {}
63569
64642
  prompt = (
63570
64643
  "/no_think\n"
63571
64644
  "Decide the intent of one unscoped TodoWrite row inside the active execution-plan step. "
@@ -63661,6 +64734,12 @@ body{padding:18px}
63661
64734
  content = trim(str(incoming.get("content", "") or "").strip(), 900)
63662
64735
  if not content:
63663
64736
  return {}
64737
+ exact_content = normalize_work_text(str(incoming.get("content", "") or "")).strip().casefold()
64738
+ if sum(
64739
+ normalize_work_text(str(row.get("content", "") or "")).strip().casefold() == exact_content
64740
+ for row in existing_rows if isinstance(row, dict)
64741
+ ) == 1:
64742
+ return {}
63664
64743
  bb = board if isinstance(board, dict) else self._ensure_blackboard()
63665
64744
  rows_text = "\n".join(
63666
64745
  f"- id={trim(str(row.get('subtask_id', '') or row.get('key', '') or ''), 100)} "
@@ -77697,7 +78776,79 @@ body{padding:18px}
77697
78776
  resume=is_resume,
77698
78777
  )
77699
78778
 
78779
+ def _todo_progress_guard_key(self, role: str = "") -> str:
78780
+ bb = self._ensure_blackboard()
78781
+ step = self._get_active_plan_step(bb) or {}
78782
+ role_key = self._sanitize_agent_role(role) or self._current_plan_worker_owner(bb)
78783
+ return json.dumps([
78784
+ getattr(self, "run_generation", 0), bb.get("task_epoch", 0),
78785
+ step.get("id", ""), role_key,
78786
+ ], ensure_ascii=False)
78787
+
78788
+ @staticmethod
78789
+ def _todo_material_signature(rows: list[dict]) -> str:
78790
+ # Timestamp-only writes are not task progress. Evidence, identities,
78791
+ # statuses and scope changes still pass through the normal audit.
78792
+ fields = (
78793
+ "content", "status", "owner", "parent_step_id", "key", "subtask_id",
78794
+ "external_subtask_id", "root_group_id", "evidence", "evidence_binding", "evidence_ids",
78795
+ "id", "full_content", "category",
78796
+ )
78797
+ return json.dumps(
78798
+ [{field: row[field] for field in fields if row.get(field) not in (None, "", [])}
78799
+ for row in rows if isinstance(row, dict)],
78800
+ ensure_ascii=False, sort_keys=True, default=str,
78801
+ )
78802
+
78803
+ @staticmethod
78804
+ def _todo_no_progress_instruction() -> str:
78805
+ return (
78806
+ "Todo state has not changed. Do not repeat TodoWrite, switch to TodoWriteRescue, "
78807
+ "or rewrite the same list. Execute the current in_progress task using a work or "
78808
+ "evidence-gathering tool, then update only the statuses supported by its result. "
78809
+ "Repeated no-progress Todo writes are temporarily unavailable until a non-Todo "
78810
+ "work action runs. Existing tasks and completion/evidence checks remain intact."
78811
+ )
78812
+
78813
+ def _filter_todo_progress_tools(self, tools: list[dict], *, role: str = "") -> list[dict]:
78814
+ counts = getattr(self, "_todowrite_step_counter", {})
78815
+ if not counts or counts.get(self._todo_progress_guard_key(role), 0) < 2:
78816
+ return tools
78817
+ return [
78818
+ tool for tool in tools
78819
+ if canonicalize_tool_name(tool.get("function", {}).get("name", ""))
78820
+ not in {"TodoWrite", "TodoWriteRescue"}
78821
+ ]
78822
+
77700
78823
  def _dispatch_todo_update(self, args: dict, *, role: str = "", resume: bool = False) -> str:
78824
+ key = self._todo_progress_guard_key(role)
78825
+ counts = getattr(self, "_todowrite_step_counter", None)
78826
+ if not isinstance(counts, dict):
78827
+ counts = self._todowrite_step_counter = {}
78828
+ if counts.get(key, 0) >= 2:
78829
+ return self._plan_control_feedback("todo_no_progress", self._todo_no_progress_instruction())
78830
+ before = self._todo_material_signature(self.todo.snapshot())
78831
+ plan_before = self._todo_material_signature(self._ensure_blackboard().get("project_todos", []))
78832
+ result = self._dispatch_todo_update_inner(args, role=role, resume=resume)
78833
+ if key != self._todo_progress_guard_key(role):
78834
+ counts.pop(key, None)
78835
+ return result
78836
+ changed = before != self._todo_material_signature(self.todo.snapshot())
78837
+ plan_changed = plan_before != self._todo_material_signature(
78838
+ self._ensure_blackboard().get("project_todos", []),
78839
+ )
78840
+ if changed or plan_changed:
78841
+ counts.pop(key, None)
78842
+ elif self._todo_runtime_has_worker_rows(role) and not str(result).startswith("Error:"):
78843
+ counts[key] = counts.get(key, 0) + 1
78844
+ if len(counts) > 40:
78845
+ counts.pop(next(iter(counts)))
78846
+ return self._plan_control_feedback(
78847
+ "todo_no_progress", f"{self._todo_no_progress_instruction()}\n\n{result}",
78848
+ )
78849
+ return result
78850
+
78851
+ def _dispatch_todo_update_inner(self, args: dict, *, role: str = "", resume: bool = False) -> str:
77701
78852
  """Canonical dispatcher shared by TodoWrite, Rescue, and Resume aliases."""
77702
78853
  source = args if isinstance(args, dict) else {}
77703
78854
  bb = self._ensure_blackboard()
@@ -77878,13 +79029,20 @@ body{padding:18px}
77878
79029
  txt = str(output or "").strip()
77879
79030
  low = txt.lower()
77880
79031
  has_worker_rows = self._todo_runtime_has_worker_rows()
77881
- changed = self._todo_progress_changed(before_rows, after_rows) if before_rows is not None else False
79032
+ changed = (
79033
+ self._todo_material_signature(before_rows)
79034
+ != self._todo_material_signature(after_rows if after_rows is not None else self.todo.snapshot())
79035
+ ) if before_rows is not None else False
77882
79036
  if not txt:
77883
79037
  return ("failed", "empty output")
77884
79038
  if txt.startswith("Error:"):
77885
79039
  return ("failed", txt[6:].strip() or "unknown error")
79040
+ if self._tool_control_feedback_outcome(tool_name, txt) == "todo_no_progress":
79041
+ return ("no_progress", "canonical todo state unchanged; execute the current task")
77886
79042
  if changed:
77887
79043
  return ("ok", "todo updated")
79044
+ if before_rows is not None and has_worker_rows:
79045
+ return ("no_progress", "canonical todo state unchanged; execute the current task")
77888
79046
  if txt == self.todo.no_changes_text() or "no todo changes" in low:
77889
79047
  if has_worker_rows:
77890
79048
  return ("ok", "todo already up to date")
@@ -79508,6 +80666,13 @@ body{padding:18px}
79508
80666
  f"Error: tool '{name}' is unavailable to remote Program sessions because it can access "
79509
80667
  "resources outside the isolated session workspace."
79510
80668
  )
80669
+ # A real work/evidence action releases this role's Todo cooldown, also
80670
+ # for root Todos and external MCP tools. Bookkeeping alone does not.
80671
+ if getattr(self, "_todowrite_step_counter", {}) and canonicalize_tool_name(name) not in {
80672
+ "TodoWrite", "TodoWriteRescue", "compress", "tool_memory", "task_list",
80673
+ "task_get", "read_from_blackboard", "read_inbox", "route_to_next_agent",
80674
+ }:
80675
+ self._todowrite_step_counter.pop(self._todo_progress_guard_key(role_key), None)
79511
80676
  # External MCP tools (mcp__<server>__<tool>): route to the owning
79512
80677
  # subprocess. Handled before any built-in branch so an MCP name can
79513
80678
  # never be shadowed by a builtin, and so every role/mode reaches it.
@@ -79517,16 +80682,6 @@ body{padding:18px}
79517
80682
  return f"Error: MCP is not available in this session for tool '{name}'"
79518
80683
  self._emit("status", {"summary": f"calling MCP tool {name}"})
79519
80684
  return mgr.call(name, args if isinstance(args, dict) else {})
79520
- # Fix 5d: Reset TodoWrite loop counter on non-TodoWrite tool calls
79521
- if name not in ("TodoWrite", "TodoWriteRescue") and hasattr(self, '_todowrite_step_counter'):
79522
- try:
79523
- _rst_step = self._get_active_plan_step()
79524
- if isinstance(_rst_step, dict):
79525
- _rst_id = str(_rst_step.get("id", "") or "")
79526
- if _rst_id:
79527
- self._todowrite_step_counter.pop(_rst_id, None)
79528
- except Exception:
79529
- pass
79530
80685
  if name == "bash":
79531
80686
  guard_error = self._guard_shell_write_scope(str(args.get("command", "") or ""), self.files_root)
79532
80687
  if guard_error:
@@ -87497,6 +88652,8 @@ body{padding:18px}
87497
88652
  "status",
87498
88653
  {
87499
88654
  "summary": (
88655
+ "todo unchanged; continue current task execution"
88656
+ if result_item.get("control_outcome") == "todo_no_progress" else
87500
88657
  "todo control gate preserved canonical current-step subtasks; "
87501
88658
  "continue the existing in_progress item"
87502
88659
  )
@@ -91450,6 +92607,7 @@ class SessionManager:
91450
92607
  except Exception:
91451
92608
  pass
91452
92609
  opts = []
92610
+ profiles_changed = False
91453
92611
  ollama_profile_id = ""
91454
92612
  ollama_base = self.ollama_base
91455
92613
  for pid, profile in sorted(self.user_model_profiles.items(), key=lambda x: x[0]):
@@ -91472,6 +92630,23 @@ class SessionManager:
91472
92630
  "thinking_hint": bool(profile.get("thinking_hint", False)),
91473
92631
  "thinking_stream": bool(profile.get("thinking_stream", False)),
91474
92632
  "response_stream": bool(profile.get("response_stream", False)),
92633
+ "effort": str(profile.get("effort", "") or ""),
92634
+ "max_effort": str(profile.get("max_effort", "") or ""),
92635
+ "reasoning_supported": (
92636
+ profile.get("reasoning_supported")
92637
+ if profile.get("reasoning_supported") is not None
92638
+ else model_reasoning_style(
92639
+ str(profile.get("provider", "")), model, caps
92640
+ )
92641
+ != "none"
92642
+ ),
92643
+ "reasoning_style": str(
92644
+ profile.get("reasoning_style")
92645
+ or model_reasoning_style(str(profile.get("provider", "")), model, caps)
92646
+ or ""
92647
+ ),
92648
+ "display_name": str(profile.get("display_name", profile.get("label", pid)) or ""),
92649
+ "title": str(profile.get("title", profile.get("label", pid)) or ""),
91475
92650
  "capabilities": caps,
91476
92651
  }
91477
92652
  )
@@ -91484,7 +92659,24 @@ class SessionManager:
91484
92659
  base = str(profile.get("base_url", self.ollama_base) or self.ollama_base).strip()
91485
92660
  if not base:
91486
92661
  continue
91487
- tags = list_ollama_models_cached(base, force_refresh=bool(force_probe))
92662
+ tags = list_ollama_models_cached(
92663
+ base,
92664
+ force_refresh=bool(force_probe),
92665
+ background=not force_probe,
92666
+ )
92667
+ ollama_records = probe_provider_models(
92668
+ profile,
92669
+ force_refresh=bool(force_probe),
92670
+ background=not force_probe,
92671
+ )
92672
+ profiles_changed = merge_probed_models_into_profile(
92673
+ profile, ollama_records, model_ids=tags
92674
+ ) or profiles_changed
92675
+ ollama_record_map = {
92676
+ str(record.get("id", "")): record
92677
+ for record in ollama_records
92678
+ if isinstance(record, dict) and str(record.get("id", "")).strip()
92679
+ }
91488
92680
  if tags:
91489
92681
  if pid == self.user_active_profile_id:
91490
92682
  self.ollama_env_tags = list(tags)
@@ -91499,6 +92691,13 @@ class SessionManager:
91499
92691
  infer_model_multimodal_capabilities("ollama", tag),
91500
92692
  parse_capability_overrides(profile.get("capabilities", {})),
91501
92693
  )
92694
+ tag_record = ollama_record_map.get(tag)
92695
+ tag_record_caps = (
92696
+ tag_record.get("capabilities", {})
92697
+ if isinstance(tag_record, dict)
92698
+ else {}
92699
+ )
92700
+ tag_record_probed = isinstance(tag_record, dict)
91502
92701
  opts.append(
91503
92702
  {
91504
92703
  "selection": selection,
@@ -91510,9 +92709,99 @@ class SessionManager:
91510
92709
  "thinking_hint": bool(profile.get("thinking_hint", self.thinking)),
91511
92710
  "thinking_stream": bool(profile.get("thinking_stream", False)),
91512
92711
  "response_stream": bool(profile.get("response_stream", False)),
92712
+ "effort": str(profile.get("effort", "") or ""),
92713
+ "max_effort": str(profile.get("max_effort", "") or ""),
92714
+ "reasoning_supported": (
92715
+ profile.get("reasoning_supported")
92716
+ if profile.get("reasoning_supported") is not None
92717
+ else tag_record_caps.get("reasoning_supported")
92718
+ if isinstance(tag_record_caps, dict)
92719
+ and tag_record_caps.get("reasoning_supported") is not None
92720
+ else model_reasoning_style(
92721
+ "ollama",
92722
+ tag,
92723
+ tag_caps,
92724
+ capabilities_probed=tag_record_probed,
92725
+ )
92726
+ != "none"
92727
+ ),
92728
+ "reasoning_style": str(
92729
+ profile.get("reasoning_style")
92730
+ or tag_record_caps.get("reasoning_style", "")
92731
+ or model_reasoning_style(
92732
+ "ollama",
92733
+ tag,
92734
+ tag_caps,
92735
+ capabilities_probed=tag_record_probed,
92736
+ )
92737
+ or ""
92738
+ ),
92739
+ "display_name": str(profile.get("display_name", profile.get("label", pid)) or ""),
92740
+ "title": str(profile.get("title", profile.get("label", pid)) or ""),
91513
92741
  "capabilities": tag_caps,
91514
92742
  }
91515
92743
  )
92744
+ for pid, profile in sorted(self.user_model_profiles.items(), key=lambda x: x[0]):
92745
+ configured = [str(x).strip() for x in profile.get("models", []) if str(x).strip()]
92746
+ records = probe_provider_models(
92747
+ profile,
92748
+ force_refresh=bool(force_probe),
92749
+ background=not force_probe,
92750
+ )
92751
+ profiles_changed = merge_probed_models_into_profile(profile, records) or profiles_changed
92752
+ for rec in records:
92753
+ model_id = str(rec.get("id", "") or "").strip()
92754
+ if model_id and model_id not in configured:
92755
+ configured.append(model_id)
92756
+ for model_id in configured:
92757
+ selection = f"{pid}::{model_id}"
92758
+ if not model_id or selection in seen:
92759
+ continue
92760
+ seen.add(selection)
92761
+ rec = next((r for r in records if str(r.get("id", "")) == model_id), {})
92762
+ rcaps = rec.get("capabilities", {}) if isinstance(rec, dict) else {}
92763
+ opts.append({
92764
+ "selection": selection, "profile_id": pid,
92765
+ "provider": profile.get("provider", "unknown"), "model": model_id,
92766
+ "label": f"{profile.get('label', pid)} | {model_id}",
92767
+ "source": "provider-probe" if rec else profile.get("source", ""),
92768
+ "thinking_hint": bool(profile.get("thinking_hint", False)),
92769
+ "thinking_stream": bool(profile.get("thinking_stream", False)),
92770
+ "response_stream": bool(profile.get("response_stream", False)),
92771
+ "effort": str(profile.get("effort", "") or ""),
92772
+ "max_effort": str(profile.get("max_effort", "") or ""),
92773
+ "reasoning_supported": (
92774
+ rcaps.get("reasoning_supported")
92775
+ if isinstance(rcaps, dict) and rcaps.get("reasoning_supported") is not None
92776
+ else profile.get("reasoning_supported")
92777
+ if profile.get("reasoning_supported") is not None
92778
+ else model_reasoning_style(
92779
+ str(profile.get("provider", "")),
92780
+ model_id,
92781
+ rcaps if isinstance(rcaps, dict) else None,
92782
+ capabilities_probed=bool(rec),
92783
+ )
92784
+ != "none"
92785
+ ),
92786
+ "reasoning_style": str(
92787
+ (rcaps.get("reasoning_style") if isinstance(rcaps, dict) else "")
92788
+ or profile.get("reasoning_style", "")
92789
+ or model_reasoning_style(
92790
+ str(profile.get("provider", "")),
92791
+ model_id,
92792
+ rcaps if isinstance(rcaps, dict) else None,
92793
+ capabilities_probed=bool(rec),
92794
+ )
92795
+ or ""
92796
+ ),
92797
+ "display_name": str(profile.get("display_name", profile.get("label", pid)) or ""),
92798
+ "title": str(profile.get("title", profile.get("label", pid)) or ""),
92799
+ "capabilities": merge_multimodal_capabilities(infer_model_multimodal_capabilities(str(profile.get("provider", "")), model_id), parse_capability_overrides(profile.get("capabilities", {}))),
92800
+ })
92801
+ for _option in opts:
92802
+ _pid = str(_option.get("profile_id", "") or "")
92803
+ _profile = self.user_model_profiles.get(_pid, {})
92804
+ apply_model_option_runtime_fields(_option, _profile, str(_option.get("model", "") or ""))
91516
92805
  runnable_opts = [x for x in opts if self._option_is_runnable(x)]
91517
92806
  if runnable_opts:
91518
92807
  opts = runnable_opts
@@ -91536,9 +92825,31 @@ class SessionManager:
91536
92825
  "thinking_hint": bool(active.get("thinking_hint", False)),
91537
92826
  "thinking_stream": bool(active.get("thinking_stream", False)),
91538
92827
  "response_stream": bool(active.get("response_stream", False)),
92828
+ "effort": str(active.get("effort", "") or ""),
92829
+ "max_effort": str(active.get("max_effort", "") or ""),
92830
+ "reasoning_supported": (
92831
+ active.get("reasoning_supported")
92832
+ if active.get("reasoning_supported") is not None
92833
+ else model_reasoning_style(
92834
+ str(active.get("provider", "")),
92835
+ str(active.get("model", "") or ""),
92836
+ active.get("capabilities", {}),
92837
+ )
92838
+ != "none"
92839
+ ),
92840
+ "reasoning_style": str(
92841
+ active.get("reasoning_style")
92842
+ or model_reasoning_style(
92843
+ str(active.get("provider", "")),
92844
+ str(active.get("model", "") or ""),
92845
+ active.get("capabilities", {}),
92846
+ )
92847
+ or ""
92848
+ ),
91539
92849
  "capabilities": active_caps,
91540
92850
  },
91541
92851
  )
92852
+ apply_model_option_runtime_fields(opts[0], active, str(active.get("model", "") or ""))
91542
92853
  option_map.add(selected)
91543
92854
  if opts and selected not in option_map:
91544
92855
  selected = str(opts[0].get("selection", ""))
@@ -91546,6 +92857,8 @@ class SessionManager:
91546
92857
  infer_model_multimodal_capabilities(str(active.get("provider", "")), str(active.get("model", ""))),
91547
92858
  parse_capability_overrides(active.get("capabilities", {})),
91548
92859
  )
92860
+ if profiles_changed:
92861
+ self._persist_user_prefs()
91549
92862
  return {
91550
92863
  "provider": active.get("provider", "ollama"),
91551
92864
  "models": [x["selection"] for x in opts] or [self.model],
@@ -91557,7 +92870,7 @@ class SessionManager:
91557
92870
  "active_capabilities": active_caps,
91558
92871
  }
91559
92872
 
91560
- def set_runtime_model(self, model: str, thinking: bool | None = None) -> dict:
92873
+ def set_runtime_model(self, model: str, thinking: bool | None = None, settings: dict | None = None) -> dict:
91561
92874
  raw = str(model or "").strip()
91562
92875
  if not raw:
91563
92876
  raise ValueError("model required")
@@ -91585,6 +92898,11 @@ class SessionManager:
91585
92898
  raise ValueError(f"profile not found: {pid}")
91586
92899
  if selected_model.strip():
91587
92900
  profile["model"] = selected_model.strip()
92901
+ profile = apply_model_runtime_settings(profile, str(profile.get("model", "") or ""), settings)
92902
+ for record in probe_provider_models(profile, background=True):
92903
+ if str(record.get("id", "")) == str(profile.get("model", "")):
92904
+ merge_probed_models_into_profile(profile, [record])
92905
+ break
91588
92906
  if str(profile.get("provider", "")).lower() == "ollama" and selected_model.strip():
91589
92907
  profile["capabilities"] = infer_model_multimodal_capabilities("ollama", selected_model.strip())
91590
92908
  if not self._user_profile_is_runnable(profile) and not explicit_profile and selected_model.strip():
@@ -91644,6 +92962,7 @@ class SessionManager:
91644
92962
  cfg = dict(config or {})
91645
92963
  OllamaClient.clear_global_probe_cache()
91646
92964
  profiles, active = self._profiles_from_config(cfg)
92965
+ probe_and_merge_model_profiles(profiles, force_refresh=True)
91647
92966
  with self.lock:
91648
92967
  normalized: dict[str, dict] = {}
91649
92968
  for pid, row in profiles.items():
@@ -91977,9 +93296,10 @@ window.MathJax={
91977
93296
  </div>
91978
93297
  <div class="actions">
91979
93298
  <select id="langSelect"></select>
91980
- <select id="modelSelect"></select>
91981
- <button id="applyModelBtn" class="subtle">Apply Model</button>
91982
- <button id="llmConfigBtn" class="subtle">Fill LLM Config</button>
93299
+ <div id="modelPicker" class="model-picker" role="group" aria-label="Model selection">
93300
+ <select id="modelSelect"></select>
93301
+ <button id="modelManageBtn" class="subtle" type="button" title="Manage models" aria-label="Manage models">&#9881;</button>
93302
+ </div>
91983
93303
  <button id="programBtn" class="subtle" type="button">Program</button>
91984
93304
  <input id="configInput" type="file" accept=".json,application/json" tabindex="-1" aria-hidden="true" style="position:absolute;left:-10000px;top:auto;width:1px;height:1px;opacity:0;pointer-events:none">
91985
93305
  <a id="downloadBtn" href="#">Open Skills Studio</a>
@@ -92172,6 +93492,7 @@ window.MathJax={
92172
93492
  </aside>
92173
93493
  </main>
92174
93494
  </div>
93495
+ <div id="menuPopup" class="menu-popup is-hidden" role="dialog" aria-label="Model management"></div>
92175
93496
  <div id="applicationEditor" class="application-modal" hidden>
92176
93497
  <div class="application-dialog" role="dialog" aria-modal="true" aria-labelledby="applicationEditorTitle">
92177
93498
  <div class="application-dialog-head">
@@ -92809,18 +94130,63 @@ const CODE_KEYWORDS={default:new Set(['if','else','for','while','switch','case',
92809
94130
  S.staticMode=STATIC_UI;
92810
94131
  async function setTaskLevel(level){if(!S.activeId)return;const lvl=parseInt(level,10);try{await api('/api/sessions/'+S.activeId+'/config/task-level',{method:'POST',body:JSON.stringify({level:lvl})});updateLevelBtn(lvl);scheduleSnapshot({forceFull:false,delayMs:80,allowWhenFrozen:true})}catch(err){showError(err.message||String(err))}}
92811
94132
  function updateLevelBtn(level){const btn=E('levelBtn');if(!btn)return;if(!level||level===0){setTextIfChanged(btn,t('btn_level')+': '+t('level_auto'))}else{const labels={1:'L1',2:'L2',3:'L3',4:'L4',5:'L5'};setTextIfChanged(btn,t('btn_level')+': '+(labels[level]||t('level_auto')))}}
92812
- const LLM_PROVIDER_FIELDS={ollama:[{key:'ollama_url',label:'Ollama URL',type:'url',placeholder:'http://127.0.0.1:11434',hint:'Ollama API endpoint'}],vllm:[{key:'vllm_url',label:'vLLM URL',type:'url',placeholder:'http://localhost:8000/v1',hint:'vLLM OpenAI-compat endpoint'},{key:'vllm_model',label:'Model',type:'text',placeholder:'(auto-detect)',hint:'Leave empty to auto-detect'},{key:'vllm_key',label:'API Key (optional)',type:'password',placeholder:'',hint:'Usually not required for local'}],lmstudio:[{key:'lmstudio_url',label:'LM Studio URL',type:'url',placeholder:'http://localhost:1234/v1',hint:'LM Studio server endpoint'},{key:'lmstudio_model',label:'Model',type:'text',placeholder:'(auto-detect)',hint:'Leave empty to auto-detect'}],openai_compat:[{key:'openai_url',label:'API Base URL',type:'url',placeholder:'https://api.openai.com/v1',hint:'OpenAI-compatible endpoint'},{key:'openai_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'Your API key'},{key:'openai_model',label:'Model',type:'text',placeholder:'gpt-4o-mini',hint:'e.g. gpt-4o, claude-sonnet-4-20250514'}],anthropic:[{key:'anthropic_url',label:'API URL',type:'url',placeholder:'https://api.anthropic.com',hint:'Anthropic API endpoint'},{key:'anthropic_key',label:'API Key',type:'password',placeholder:'sk-ant-...',hint:'Anthropic API key'},{key:'anthropic_model',label:'Model',type:'text',placeholder:'claude-sonnet-4-20250514',hint:'e.g. claude-sonnet-4-20250514, claude-opus-4-20250514'}],glm:[{key:'glm_url',label:'API URL',type:'url',placeholder:'https://open.bigmodel.cn/api/paas/v4',hint:'GLM API endpoint'},{key:'glm_key',label:'API Key',type:'password',placeholder:'',hint:'GLM API Key'},{key:'glm_model',label:'Model',type:'text',placeholder:'glm-4-flash',hint:'e.g. glm-4-flash, glm-4-plus, glm-4v'}],kimi:[{key:'kimi_url',label:'API URL',type:'url',placeholder:'https://api.moonshot.cn/v1',hint:'KIMI/Moonshot API endpoint'},{key:'kimi_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'Moonshot API Key'},{key:'kimi_model',label:'Model',type:'text',placeholder:'moonshot-v1-8k',hint:'e.g. moonshot-v1-8k, moonshot-v1-32k, moonshot-v1-128k'}],openrouter:[{key:'openrouter_url',label:'API URL',type:'url',placeholder:'https://openrouter.ai/api/v1',hint:'OpenRouter endpoint'},{key:'openrouter_key',label:'API Key',type:'password',placeholder:'sk-or-...',hint:'OpenRouter API Key'},{key:'openrouter_model',label:'Model',type:'text',placeholder:'meta-llama/llama-3.1-8b-instruct',hint:'Full model slug from openrouter.ai/models'}],siliconflow:[{key:'siliconflow_url',label:'API URL',type:'url',placeholder:'https://api.siliconflow.cn/v1',hint:'SiliconFlow API endpoint'},{key:'siliconflow_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'SiliconFlow API key'},{key:'siliconflow_model',label:'Model',type:'text',placeholder:'Qwen/Qwen3-Next-80B-A3B-Instruct',hint:'Model identifier'}],custom_http:[{key:'custom_url',label:'API Endpoint URL',type:'url',placeholder:'https://your-api.com/v1/chat/completions',hint:'Full API endpoint URL'},{key:'custom_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'API key (optional)'},{key:'custom_model',label:'Model',type:'text',placeholder:'model-name',hint:'Model identifier'},{key:'custom_headers',label:'Custom Headers (JSON)',type:'textarea',placeholder:'{"Authorization":"Bearer token","X-Custom":"value"}',hint:'JSON object of additional HTTP headers'},{key:'custom_payload',label:'Payload Template (JSON)',type:'textarea',placeholder:'{"custom_param":"value","stream":true}',hint:'Extra fields merged into the request body'},{key:'temperature',label:'Temperature',type:'number',placeholder:'0.2',hint:'0.0-2.0, lower=deterministic'},{key:'request_timeout',label:'Request Timeout (seconds)',type:'number',placeholder:'3600',hint:'Max seconds per LLM request'}]};
92813
- function modelReasoningStyle(provider,model){const p=String(provider||'').trim().toLowerCase();const m=String(model||'').trim().toLowerCase();if(p==='anthropic'){return ['claude-3-7','claude-3.7','claude-sonnet-4','claude-opus-4','claude-haiku-4','claude-4','-thinking','claude-sonnet-5','claude-opus-5'].some(x=>m.includes(x))?'anthropic':'none'}if(p==='ollama'){return ['r1','qwen3','deepseek','thinking','reasoner','magistral'].some(x=>m.includes(x))?'ollama':'none'}const compat=new Set(['openai_compat','siliconflow','vllm','lmstudio','glm','kimi','openrouter','custom_http']);if(compat.has(p)){if(p==='glm'||m.startsWith('glm-')||m.includes('glm'))return 'glm';if(m.includes('deepseek'))return 'deepseek';if(m.startsWith('o1')||m.startsWith('o3')||m.startsWith('o4')||m.includes('gpt-5')||m.includes('/o1')||m.includes('/o3')||m.includes('/o4')||m.includes('reasoning'))return 'openai';return 'none'}return 'none'}
94133
+ const LLM_PROVIDER_FIELDS={ollama:[{key:'ollama_url',label:'Ollama URL',type:'url',placeholder:'http://127.0.0.1:11434',hint:'Ollama API endpoint'}],vllm:[{key:'vllm_url',label:'vLLM URL',type:'url',placeholder:'http://localhost:8000/v1',hint:'vLLM OpenAI-compat endpoint'},{key:'vllm_model',label:'Model',type:'text',placeholder:'(auto-detect)',hint:'Leave empty to auto-detect'},{key:'vllm_key',label:'API Key (optional)',type:'password',placeholder:'',hint:'Usually not required for local'}],lmstudio:[{key:'lmstudio_url',label:'LM Studio URL',type:'url',placeholder:'http://localhost:1234/v1',hint:'LM Studio server endpoint'},{key:'lmstudio_model',label:'Model',type:'text',placeholder:'(auto-detect)',hint:'Leave empty to auto-detect'}],openai_compat:[{key:'openai_url',label:'API Base URL',type:'url',placeholder:'https://api.openai.com/v1',hint:'OpenAI-compatible endpoint'},{key:'openai_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'Your API key'},{key:'openai_model',label:'Model',type:'text',placeholder:'gpt-4o-mini',hint:'e.g. gpt-4o, claude-sonnet-4-20250514'}],anthropic:[{key:'anthropic_url',label:'API URL',type:'url',placeholder:'https://api.anthropic.com',hint:'Anthropic API endpoint'},{key:'anthropic_key',label:'API Key',type:'password',placeholder:'sk-ant-...',hint:'Anthropic API key'},{key:'anthropic_model',label:'Model',type:'text',placeholder:'claude-sonnet-4-20250514',hint:'e.g. claude-sonnet-4-20250514, claude-opus-4-20250514'}],glm:[{key:'glm_url',label:'API URL',type:'url',placeholder:'https://open.bigmodel.cn/api/paas/v4',hint:'GLM API endpoint'},{key:'glm_key',label:'API Key',type:'password',placeholder:'',hint:'GLM API Key'},{key:'glm_model',label:'Model',type:'text',placeholder:'glm-4-flash',hint:'e.g. glm-4-flash, glm-4-plus, glm-4v'}],kimi:[{key:'kimi_url',label:'API URL',type:'url',placeholder:'https://api.moonshot.cn/v1',hint:'KIMI/Moonshot API endpoint'},{key:'kimi_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'Moonshot API Key'},{key:'kimi_model',label:'Model',type:'text',placeholder:'moonshot-v1-8k',hint:'e.g. moonshot-v1-8k, moonshot-v1-32k, moonshot-v1-128k'}],openrouter:[{key:'openrouter_url',label:'API URL',type:'url',placeholder:'https://openrouter.ai/api/v1',hint:'OpenRouter endpoint'},{key:'openrouter_key',label:'API Key',type:'password',placeholder:'sk-or-...',hint:'OpenRouter API Key'},{key:'openrouter_model',label:'Model',type:'text',placeholder:'meta-llama/llama-3.1-8b-instruct',hint:'Full model slug from openrouter.ai/models'}],siliconflow:[{key:'siliconflow_url',label:'API URL',type:'url',placeholder:'https://api.siliconflow.cn/v1',hint:'SiliconFlow API endpoint'},{key:'siliconflow_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'SiliconFlow API key'},{key:'siliconflow_model',label:'Model',type:'text',placeholder:'Qwen/Qwen3-Next-80B-A3B-Instruct',hint:'Model identifier'}],custom_http:[{key:'custom_name',label:'Provider Name',type:'text',placeholder:'My Provider',hint:'Name shown in model menus'},{key:'custom_title',label:'Provider Title',type:'text',placeholder:'Custom HTTP',hint:'Title used for display and session labels'},{key:'custom_url',label:'API Endpoint URL',type:'url',placeholder:'https://your-api.com/v1/chat/completions',hint:'Full API endpoint URL'},{key:'custom_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'API key (optional)'},{key:'custom_model',label:'Model',type:'text',placeholder:'model-name',hint:'Model identifier'},{key:'custom_headers',label:'Custom Headers (JSON)',type:'textarea',placeholder:'{"Authorization":"Bearer token","X-Custom":"value"}',hint:'JSON object of additional HTTP headers'},{key:'custom_payload',label:'Payload Template (JSON)',type:'textarea',placeholder:'{"custom_param":"value","stream":true}',hint:'Extra fields merged into the request body'},{key:'temperature',label:'Temperature',type:'number',placeholder:'0.2',hint:'0.0-2.0, lower=deterministic'},{key:'request_timeout',label:'Request Timeout (seconds)',type:'number',placeholder:'3600',hint:'Max seconds per LLM request'}]};
94134
+ function modelReasoningStyle(provider,model,capabilities){const p=String(provider||'').trim().toLowerCase();const caps=capabilities&&typeof capabilities==='object'?capabilities:{};const explicit=String(caps.reasoning_style||caps.reasoning_dialect||'').trim().toLowerCase();if(['anthropic','openai','deepseek','glm','ollama','none'].includes(explicit))return explicit;if(caps.reasoning_supported===false)return 'none';if(caps.reasoning_supported===true){if(p==='anthropic')return 'anthropic';if(p==='ollama')return 'ollama';if(p==='glm')return 'glm';return 'openai'}return 'none'}
94135
+ function detectedModelCapabilities(provider,model){const key=String(provider||'').trim().toLowerCase();const target=String(model||'').trim().toLowerCase();const rows=S.llmDetectedModelRecords?.[key];if(Array.isArray(rows)){const row=rows.find(item=>String(item?.id||item?.model||'').trim().toLowerCase()===target);return row?.capabilities&&typeof row.capabilities==='object'?row.capabilities:{}}if(rows&&typeof rows==='object'){const row=rows[model]||rows[target];if(row&&typeof row==='object')return row.capabilities&&typeof row.capabilities==='object'?row.capabilities:row}return {}}
92814
94136
  function effortModelFieldId(provider){if(provider==='ollama')return 'llmF_ollama_model';const fields=LLM_PROVIDER_FIELDS[provider]||[];const mf=fields.find(f=>f.key&&f.key.endsWith('_model'));return mf?('llmF_'+mf.key):''}
92815
94137
  function effortCurrentModel(provider){const id=effortModelFieldId(provider);if(!id)return '';const el=E(id);return el?String(el.value||'').trim():''}
92816
- function refreshEffortAvailability(){const provider=E('llmProvider')?.value||'ollama';const eff=E('llmF_effort');const mx=E('llmF_max_effort');const hint=E('llmEffortHint');if(!eff||!mx)return;const model=effortCurrentModel(provider);const style=modelReasoningStyle(provider,model);const supported=style!=='none';eff.disabled=!supported;mx.disabled=!supported;const wrap=E('llmEffortField');const wrap2=E('llmMaxEffortField');for(const w of [wrap,wrap2]){if(w)w.style.opacity=supported?'1':'0.55'}if(hint){if(!model){hint.textContent=t('llm_effort_need_model')}else if(supported){hint.textContent=t('llm_effort_supported').replace('{style}',style)}else{hint.textContent=t('llm_effort_unsupported')}}}
94138
+ function refreshEffortAvailability(){const provider=E('llmProvider')?.value||'ollama';const eff=E('llmF_effort');const mx=E('llmF_max_effort');const hint=E('llmEffortHint');if(!eff||!mx)return;const model=effortCurrentModel(provider);const style=modelReasoningStyle(provider,model,detectedModelCapabilities(provider,model));const supported=style!=='none';eff.disabled=!supported;mx.disabled=!supported;const wrap=E('llmEffortField');const wrap2=E('llmMaxEffortField');for(const w of [wrap,wrap2]){if(w)w.style.opacity=supported?'1':'0.55'}if(hint){if(!model){hint.textContent=t('llm_effort_need_model')}else if(supported){hint.textContent=t('llm_effort_supported').replace('{style}',style)}else{hint.textContent=t('llm_effort_unsupported')}}}
92817
94139
  function renderLlmFields(provider){const container=E('llmFieldsContainer');if(!container)return;let html='';const openaiCompatProviders=new Set(['openai_compat','siliconflow','vllm','lmstudio','glm','kimi','openrouter','custom_http']);if(provider==='ollama'){const fields=LLM_PROVIDER_FIELDS.ollama;for(const f of fields){html+='<div class=\"llm-field\"><label>'+esc(f.label)+'</label><input type=\"'+f.type+'\" id=\"llmF_'+f.key+'\" placeholder=\"'+esc(f.placeholder||'')+'\" value=\"\"><div class=\"llm-hint\">'+esc(f.hint||'')+'</div></div>'}html+='<div class=\"llm-field\"><label>'+esc(t('llm_model'))+'</label><div style=\"display:flex;gap:8px;align-items:center\"><select id=\"llmF_ollama_model\" style=\"flex:1\"><option value=\"\">-- '+esc(t('llm_scan_first'))+' --</option></select><button type=\"button\" id=\"ollamaScanBtn\" class=\"llm-modal-btn-secondary\" style=\"flex:none;padding:6px 12px\">'+esc(t('llm_scan'))+'</button></div><div class=\"llm-hint\" id=\"ollamaScanHint\">'+esc(t('llm_scan_hint'))+'</div></div>'}else{const fields=LLM_PROVIDER_FIELDS[provider]||[];for(const f of fields){if(f.type==='textarea'){html+='<div class=\"llm-field\"><label>'+esc(f.label)+'</label><textarea id=\"llmF_'+f.key+'\" placeholder=\"'+esc(f.placeholder||'')+'\" rows=\"3\" style=\"width:100%;padding:8px 10px;border:1px solid var(--line,#d9e1ec);border-radius:8px;font-size:.84rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;resize:vertical;box-sizing:border-box\"></textarea><div class=\"llm-hint\">'+esc(f.hint||'')+'</div></div>'}else{html+='<div class=\"llm-field\"><label>'+esc(f.label)+'</label><input type=\"'+(f.type==='number'?'text':f.type)+'\" id=\"llmF_'+f.key+'\" placeholder=\"'+esc(f.placeholder||'')+'\" value=\"\"><div class=\"llm-hint\">'+esc(f.hint||'')+'</div></div>'}}if(openaiCompatProviders.has(provider)){html+='<div class=\"llm-field\"><div style=\"display:flex;gap:8px;align-items:center\"><button type=\"button\" id=\"localScanBtn\" class=\"llm-modal-btn-secondary\" style=\"flex:none;padding:6px 12px\">'+esc(t('llm_scan'))+'</button></div><div class=\"llm-hint\" id=\"localScanHint\">'+esc(t('llm_scan_hint'))+'</div></div>'}}html+='<div class=\"llm-field\"><label>'+esc(t('llm_thinking_stream'))+'</label><select id=\"llmF_thinking_stream\"><option value=\"true\">'+esc(t('llm_enabled'))+'</option><option value=\"false\" selected>'+esc(t('llm_disabled'))+'</option></select></div>';html+='<div class=\"llm-field\"><label>'+esc(t('llm_response_stream'))+'</label><select id=\"llmF_response_stream\"><option value=\"true\">'+esc(t('llm_enabled'))+'</option><option value=\"false\" selected>'+esc(t('llm_disabled'))+'</option></select><div class=\"llm-hint\">'+esc(t('llm_response_stream_hint'))+'</div></div>';html+='<div class=\"llm-field\" id=\"llmEffortField\"><label>'+esc(t('llm_effort'))+'</label><select id=\"llmF_effort\"><option value=\"\" selected>'+esc(t('llm_effort_auto'))+'</option><option value=\"off\">'+esc(t('llm_effort_off'))+'</option><option value=\"low\">'+esc(t('llm_effort_low'))+'</option><option value=\"medium\">'+esc(t('llm_effort_medium'))+'</option><option value=\"high\">'+esc(t('llm_effort_high'))+'</option><option value=\"max\">'+esc(t('llm_effort_max'))+'</option></select></div>';html+='<div class=\"llm-field\" id=\"llmMaxEffortField\"><label>'+esc(t('llm_max_effort'))+'</label><select id=\"llmF_max_effort\"><option value=\"\">'+esc(t('llm_effort_no_ceiling'))+'</option><option value=\"low\">'+esc(t('llm_effort_low'))+'</option><option value=\"medium\">'+esc(t('llm_effort_medium'))+'</option><option value=\"high\">'+esc(t('llm_effort_high'))+'</option><option value=\"max\" selected>'+esc(t('llm_effort_max'))+'</option></select><div class=\"llm-hint\" id=\"llmEffortHint\"></div></div>';container.innerHTML=html;if(provider!=='custom_http'){const defaults=LLM_PROVIDER_FIELDS[provider]||[];for(const f of defaults){if(f.type!=='url')continue;const el=E('llmF_'+f.key);if(el&&!String(el.value||'').trim())el.value=String(f.placeholder||'')}}if(provider==='ollama'){const scanBtn=E('ollamaScanBtn');if(scanBtn)scanBtn.onclick=()=>scanOllamaModels()}if(openaiCompatProviders.has(provider)){const scanBtn=E('localScanBtn');if(scanBtn)scanBtn.onclick=()=>scanOpenAICompatModels(provider)}const _mfid=effortModelFieldId(provider);if(_mfid){const _mfe=E(_mfid);if(_mfe){_mfe.addEventListener('input',refreshEffortAvailability);_mfe.addEventListener('change',refreshEffortAvailability)}}refreshEffortAvailability()}
92818
- async function scanOllamaModels(){const urlEl=E('llmF_ollama_url');const sel=E('llmF_ollama_model');const hint=E('ollamaScanHint');const baseUrl=(urlEl?.value||'').trim()||'http://127.0.0.1:11434';if(hint)hint.textContent=t('llm_scanning');try{const res=await fetch('/api/ollama/models?base_url='+encodeURIComponent(baseUrl));const data=await res.json();if(!data.ok||!data.models?.length){if(hint)hint.textContent=t('llm_scan_empty')+(data.error?' ('+data.error+')':'');return}if(sel){sel.innerHTML='';for(const m of data.models){const op=document.createElement('option');op.value=m;op.textContent=m;sel.appendChild(op)}}if(hint)hint.textContent=t('llm_scan_found').replace('{n}',String(data.models.length))}catch(err){if(hint)hint.textContent=t('llm_scan_error')+': '+(err.message||String(err))}}
92819
- async function scanOpenAICompatModels(provider){const scanMap={openai_compat:{urlKey:'openai_url',modelKey:'openai_model',keyKey:'openai_key',defaultUrl:'https://api.openai.com/v1'},siliconflow:{urlKey:'siliconflow_url',modelKey:'siliconflow_model',keyKey:'siliconflow_key',defaultUrl:'https://api.siliconflow.cn/v1'},vllm:{urlKey:'vllm_url',modelKey:'vllm_model',keyKey:'vllm_key',defaultUrl:'http://localhost:8000/v1'},lmstudio:{urlKey:'lmstudio_url',modelKey:'lmstudio_model',keyKey:'lmstudio_key',defaultUrl:'http://localhost:1234/v1'},glm:{urlKey:'glm_url',modelKey:'glm_model',keyKey:'glm_key',defaultUrl:'https://open.bigmodel.cn/api/paas/v4'},kimi:{urlKey:'kimi_url',modelKey:'kimi_model',keyKey:'kimi_key',defaultUrl:'https://api.moonshot.cn/v1'},openrouter:{urlKey:'openrouter_url',modelKey:'openrouter_model',keyKey:'openrouter_key',defaultUrl:'https://openrouter.ai/api/v1'},custom_http:{urlKey:'custom_url',modelKey:'custom_model',keyKey:'custom_key',defaultUrl:''}};const normalizedProvider=String(provider||'openai_compat').trim()||'openai_compat';const meta=scanMap[normalizedProvider]||scanMap.openai_compat;const urlEl=E('llmF_'+meta.urlKey);const modelEl=E('llmF_'+meta.modelKey);const hint=E('localScanHint');const baseUrl=(urlEl?.value||'').trim()||meta.defaultUrl||'';const apiKey=(E('llmF_'+meta.keyKey)?.value||'').trim();if(hint)hint.textContent=t('llm_scanning');try{let url='/api/openai_compat/models?provider='+encodeURIComponent(normalizedProvider)+'&base_url='+encodeURIComponent(baseUrl);if(apiKey)url+='&api_key='+encodeURIComponent(apiKey);const res=await fetch(url);const data=await res.json();const models=Array.isArray(data.models)?data.models.filter(Boolean):[];if(!data.ok){if(hint)hint.textContent=t('llm_scan_error')+(data.error?' ('+data.error+')':'');return}if(models.length){if(modelEl&&!String(modelEl.value||'').trim())modelEl.value=models[0];try{refreshEffortAvailability()}catch(e){}if(hint)hint.textContent=t('llm_scan_found').replace('{n}',String(models.length))+': '+models.slice(0,3).join(', ');return}if(data.reachable){if(hint)hint.textContent=t('llm_scan_reachable_manual')+(data.error?' ('+data.error+')':'');return}if(hint)hint.textContent=t('llm_scan_empty')+(data.error?' ('+data.error+')':'')}catch(err){if(hint)hint.textContent=t('llm_scan_error')+': '+(err.message||String(err))}}
94140
+ async function scanOllamaModels(){const urlEl=E('llmF_ollama_url');const sel=E('llmF_ollama_model');const hint=E('ollamaScanHint');const baseUrl=(urlEl?.value||'').trim()||'http://127.0.0.1:11434';if(hint)hint.textContent=t('llm_scanning');try{const res=await fetch('/api/ollama/models?base_url='+encodeURIComponent(baseUrl));const data=await res.json();if(!data.ok||!data.models?.length){if(hint)hint.textContent=t('llm_scan_empty')+(data.error?' ('+data.error+')':'');return}if(sel){sel.innerHTML='';for(const m of data.models){const op=document.createElement('option');op.value=m;op.textContent=m;sel.appendChild(op)}}S.llmDetectedModels=S.llmDetectedModels||{};S.llmDetectedModelRecords=S.llmDetectedModelRecords||{};S.llmDetectedModels.ollama=data.models.slice();S.llmDetectedModelRecords.ollama=Array.isArray(data.model_records)?data.model_records.slice():data.models.map(id=>({id,capabilities:{}}));if(hint)hint.textContent=t('llm_scan_found').replace('{n}',String(data.models.length));refreshEffortAvailability()}catch(err){if(hint)hint.textContent=t('llm_scan_error')+': '+(err.message||String(err))}}
94141
+ async function scanOpenAICompatModels(provider){const scanMap={openai_compat:{urlKey:'openai_url',modelKey:'openai_model',keyKey:'openai_key',defaultUrl:'https://api.openai.com/v1'},anthropic:{urlKey:'anthropic_url',modelKey:'anthropic_model',keyKey:'anthropic_key',defaultUrl:'https://api.anthropic.com'},siliconflow:{urlKey:'siliconflow_url',modelKey:'siliconflow_model',keyKey:'siliconflow_key',defaultUrl:'https://api.siliconflow.cn/v1'},vllm:{urlKey:'vllm_url',modelKey:'vllm_model',keyKey:'vllm_key',defaultUrl:'http://localhost:8000/v1'},lmstudio:{urlKey:'lmstudio_url',modelKey:'lmstudio_model',keyKey:'lmstudio_key',defaultUrl:'http://localhost:1234/v1'},glm:{urlKey:'glm_url',modelKey:'glm_model',keyKey:'glm_key',defaultUrl:'https://open.bigmodel.cn/api/paas/v4'},kimi:{urlKey:'kimi_url',modelKey:'kimi_model',keyKey:'kimi_key',defaultUrl:'https://api.moonshot.cn/v1'},openrouter:{urlKey:'openrouter_url',modelKey:'openrouter_model',keyKey:'openrouter_key',defaultUrl:'https://openrouter.ai/api/v1'},custom_http:{urlKey:'custom_url',modelKey:'custom_model',keyKey:'custom_key',defaultUrl:''}};const normalizedProvider=String(provider||'openai_compat').trim()||'openai_compat';const meta=scanMap[normalizedProvider]||scanMap.openai_compat;const urlEl=E('llmF_'+meta.urlKey);const modelEl=E('llmF_'+meta.modelKey);const hint=E('localScanHint');const baseUrl=(urlEl?.value||'').trim()||meta.defaultUrl||'';const apiKey=(E('llmF_'+meta.keyKey)?.value||'').trim();if(hint)hint.textContent=t('llm_scanning');try{let url='/api/openai_compat/models?provider='+encodeURIComponent(normalizedProvider)+'&base_url='+encodeURIComponent(baseUrl);if(apiKey)url+='&api_key='+encodeURIComponent(apiKey);const res=await fetch(url);const data=await res.json();const models=Array.isArray(data.models)?data.models.filter(Boolean):[];const records=Array.isArray(data.model_records)?data.model_records.filter(item=>item&&String(item.id||item.model||'').trim()):models.map(id=>({id,capabilities:{}}));if(!data.ok){if(hint)hint.textContent=t('llm_scan_error')+(data.error?' ('+data.error+')':'');return}if(models.length){S.llmDetectedModels=S.llmDetectedModels||{};S.llmDetectedModelRecords=S.llmDetectedModelRecords||{};S.llmDetectedModels[normalizedProvider]=models.slice();S.llmDetectedModelRecords[normalizedProvider]=records.slice();if(modelEl&&!String(modelEl.value||'').trim())modelEl.value=models[0];try{refreshEffortAvailability()}catch(e){}if(hint)hint.textContent=t('llm_scan_found').replace('{n}',String(models.length))+': '+models.slice(0,3).join(', ');return}if(data.reachable){if(hint)hint.textContent=t('llm_scan_reachable_manual')+(data.error?' ('+data.error+')':'');return}if(hint)hint.textContent=t('llm_scan_empty')+(data.error?' ('+data.error+')':'')}catch(err){if(hint)hint.textContent=t('llm_scan_error')+': '+(err.message||String(err))}}
92820
94142
  function collectLlmConfig(){const provider=E('llmProvider')?.value||'ollama';const config={provider:provider};if(provider==='ollama'){config.ollama_url=(E('llmF_ollama_url')?.value||'').trim()||'http://127.0.0.1:11434';config.ollama_model=E('llmF_ollama_model')?.value||''}else if(provider==='custom_http'){const fields=LLM_PROVIDER_FIELDS.custom_http;for(const f of fields){const el=E('llmF_'+f.key);if(!el)continue;if(f.type==='textarea'){config[f.key]=el.value.trim()}else if(f.key==='temperature'){const v=parseFloat(el.value);if(!isNaN(v))config[f.key]=v}else if(f.key==='request_timeout'){const v=parseInt(el.value,10);if(!isNaN(v)&&v>0)config[f.key]=v}else{config[f.key]=el.value.trim()}}}else{const fields=LLM_PROVIDER_FIELDS[provider]||[];for(const f of fields){const el=E('llmF_'+f.key);if(el){const raw=el.value.trim();config[f.key]=(provider!=='custom_http'&&f.type==='url')?(raw||String(f.placeholder||'').trim()):raw}}}config.thinking_stream=E('llmF_thinking_stream')?.value==='true';config.response_stream=E('llmF_response_stream')?.value==='true';const _eff=E('llmF_effort');if(_eff&&!_eff.disabled){const _ev=String(_eff.value||'').trim();if(_ev)config.effort=_ev}const _mx=E('llmF_max_effort');if(_mx&&!_mx.disabled){const _mv=String(_mx.value||'').trim();if(_mv)config.max_effort=_mv}return config}
92821
94143
  async function submitLlmConfig(){if(!S.activeId){showError(t('select_session_first'));return}const config=collectLlmConfig();try{const payload={filename:'LLM.config.json',mime:'application/json',content_b64:btoa(unescape(encodeURIComponent(JSON.stringify(config,null,2))))};const out=await api('/api/sessions/'+S.activeId+'/uploads',{method:'POST',body:JSON.stringify(payload)});const note=String(out?.note||out?.model_catalog?.note||'').trim();if(!out?.model_catalog){showError(t('config_uploaded_no_profiles'))}else if(note){showError(note)}else{showError('')}const cat=out?.model_catalog||await loadModelCatalog();if(!applyModelCatalog(cat)){renderModelControls()}await refreshSnapshot({forceFull:true,allowWhenFrozen:true});E('llmConfigModal').style.display='none'}catch(err){showError(err.message||String(err))}}
92822
94144
  function openLlmConfigModal(){const modal=E('llmConfigModal');if(!modal)return;modal.style.display='flex';const prov=E('llmProvider');if(prov){renderLlmFields(prov.value)}}
94145
+ async function scanAnthropicModels(){const baseUrl=(E('llmF_anthropic_url')?.value||'').trim()||'https://api.anthropic.com';const apiKey=(E('llmF_anthropic_key')?.value||'').trim();const modelEl=E('llmF_anthropic_model');const hint=E('localScanHint');if(hint)hint.textContent=t('llm_scanning');try{let url='/api/openai_compat/models?provider=anthropic&base_url='+encodeURIComponent(baseUrl);if(apiKey)url+='&api_key='+encodeURIComponent(apiKey);const res=await fetch(url);const data=await res.json();const models=Array.isArray(data.models)?data.models.filter(Boolean):[];const records=Array.isArray(data.model_records)?data.model_records:models.map(id=>({id,capabilities:{}}));if(!data.ok){if(hint)hint.textContent=t('llm_scan_error')+(data.error?' ('+data.error+')':'');return}S.llmDetectedModels.anthropic=models.slice();S.llmDetectedModelRecords.anthropic=records.slice();if(modelEl&&models.length&&!String(modelEl.value||'').trim())modelEl.value=models[0];if(hint)hint.textContent=models.length?t('llm_scan_found').replace('{n}',String(models.length))+': '+models.slice(0,3).join(', '):t('llm_scan_reachable_manual');refreshEffortAvailability()}catch(err){if(hint)hint.textContent=t('llm_scan_error')+': '+(err.message||String(err))}}
92823
94146
  const COMPACT_AUTO_REFRESH_COUNT=3;
94147
+ S.llmDetectedModels=S.llmDetectedModels||{};
94148
+ S.llmDetectedModelRecords=S.llmDetectedModelRecords||{};
94149
+ setTimeout(()=>{
94150
+ const originalScanOpenAI=scanOpenAICompatModels;
94151
+ const originalScanOllama=scanOllamaModels;
94152
+ const originalCollect=collectLlmConfig;
94153
+ const originalRenderLlmFields=renderLlmFields;
94154
+ renderLlmFields=provider=>{originalRenderLlmFields(provider);if(provider!=='anthropic')return;const container=E('llmFieldsContainer');if(!container||E('localScanBtn'))return;const field=document.createElement('div');field.className='llm-field';field.innerHTML='<div style="display:flex;gap:8px;align-items:center"><button type="button" id="localScanBtn" class="llm-modal-btn-secondary" style="flex:none;padding:6px 12px">'+esc(t('llm_scan'))+'</button></div><div class="llm-hint" id="localScanHint">'+esc(t('llm_scan_hint'))+'</div>';const effortField=E('llmEffortField');container.insertBefore(field,effortField||null);E('localScanBtn').onclick=()=>scanOpenAICompatModels('anthropic')};
94155
+ scanOpenAICompatModels=async provider=>{
94156
+ const key=String(provider||'openai_compat');
94157
+ if(key==='anthropic')await scanAnthropicModels();else await originalScanOpenAI(provider);
94158
+ const modelKey={openai_compat:'openai_model',siliconflow:'siliconflow_model',vllm:'vllm_model',lmstudio:'lmstudio_model',anthropic:'anthropic_model',glm:'glm_model',kimi:'kimi_model',openrouter:'openrouter_model',custom_http:'custom_model'}[key];
94159
+ const current=modelKey?String(E('llmF_'+modelKey)?.value||'').trim():'';
94160
+ if(current){const existing=Array.isArray(S.llmDetectedModels[key])?S.llmDetectedModels[key]:[];if(!existing.includes(current))existing.unshift(current);S.llmDetectedModels[key]=existing;const records=Array.isArray(S.llmDetectedModelRecords[key])?S.llmDetectedModelRecords[key]:[];if(!records.some(item=>String(item?.id||item?.model||'').trim()===current))records.unshift({id:current,capabilities:{}});S.llmDetectedModelRecords[key]=records}
94161
+ };
94162
+ scanOllamaModels=async()=>{await originalScanOllama();const sel=E('llmF_ollama_model');const values=[...(sel?.options||[])].map(x=>String(x.value||'').trim()).filter(Boolean);if(values.length)S.llmDetectedModels.ollama=values;if(values.length&&!Array.isArray(S.llmDetectedModelRecords.ollama))S.llmDetectedModelRecords.ollama=values.map(id=>({id,capabilities:{}}))};
94163
+ collectLlmConfig=()=>{const config=originalCollect();const provider=String(config.provider||'');const detected=Array.isArray(S.llmDetectedModels[provider])?S.llmDetectedModels[provider].filter(Boolean):[];const records=Array.isArray(S.llmDetectedModelRecords[provider])?S.llmDetectedModelRecords[provider]:[];if(detected.length){config.models=detected;const modelListKeys={ollama:'ollama_models',openai_compat:'openai_models',siliconflow:'siliconflow_models',vllm:'vllm_models',lmstudio:'lmstudio_models',glm:'glm_models',kimi:'kimi_models',openrouter:'openrouter_models',custom_http:'custom_models',anthropic:'anthropic_models'};const listKey=modelListKeys[provider];if(listKey)config[listKey]=detected;if(provider==='custom_http')config.custom_models=detected}if(records.length){const reasoning={};const settings=(config.model_settings&&typeof config.model_settings==='object')?{...config.model_settings}:{};for(const record of records){const id=String(record?.id||record?.model||'').trim();const caps=record?.capabilities&&typeof record.capabilities==='object'?record.capabilities:{};if(!id)continue;const clean={};for(const key of ['reasoning_supported','reasoning_style','reasoning_dialect'])if(key in caps)clean[key]=caps[key];if(!Object.keys(clean).length)continue;reasoning[id]=clean;settings[id]={...(settings[id]&&typeof settings[id]==='object'?settings[id]:{}),...clean}}if(Object.keys(reasoning).length){config.model_reasoning_capabilities=reasoning;config.model_settings=settings}}return config};
94164
+ const modelOption=()=>{const selected=String(S.config?.model||'');return (S.modelOptions||[]).find(x=>String(x.selection||'')===selected)||null};
94165
+ let showModelSettings=anchor=>{const popup=E('menuPopup');if(!popup)return;const option=modelOption()||{};const caps=option.capabilities||{};const supports=option.reasoning_supported===true||caps.reasoning_supported===true;const effort=String(option.effort||'');const maxEffort=String(option.max_effort||'');popup.classList.remove('is-hidden','prompt-budget-menu');popup.classList.add('agent-model-menu');popup.style.display='block';popup.setAttribute('aria-hidden','false');popup.innerHTML=`<div class="agent-model-summary"><strong>${esc(option.label||option.model||S.config?.model||'Model settings')}</strong><small>Runtime settings</small></div><label class="model-setting-row">Effort<select id="webModelEffort" ${supports?'':'disabled'}><option value="">Auto</option><option value="off">Off</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option><option value="max">Max</option></select></label><label class="model-setting-row">Max effort<select id="webModelMaxEffort" ${supports?'':'disabled'}><option value="">No ceiling</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option><option value="max">Max</option></select></label><label class="model-setting-check"><input id="webModelThinking" type="checkbox" ${option.thinking_stream?'checked':''}> Thinking stream</label><label class="model-setting-check"><input id="webModelResponse" type="checkbox" ${option.response_stream?'checked':''}> Response stream</label><div class="agent-model-actions"><button id="webModelSave" class="agent-model-config">Save</button></div>`;const rect=anchor.getBoundingClientRect();popup.style.left=`${Math.max(4,Math.min(rect.left,window.innerWidth-300))}px`;popup.style.top=`${rect.bottom+6}px`;const eff=E('webModelEffort'),mx=E('webModelMaxEffort');if(eff)eff.value=effort;if(mx)mx.value=maxEffort;E('webModelSave').onclick=async ev=>{ev.stopPropagation();const settings={effort:String(eff?.value||''),max_effort:String(mx?.value||''),thinking_stream:!!E('webModelThinking')?.checked,response_stream:!!E('webModelResponse')?.checked};try{const path=S.activeId?('/api/sessions/'+S.activeId+'/config/model'):'/api/config/model';const out=await api(path,{method:'POST',body:JSON.stringify({selection:S.config?.model,model:S.config?.model,...settings})});applyModelCatalog(out);popup.classList.add('is-hidden')}catch(err){showError(err.message||String(err))}}};
94166
+ const showModelManager=async anchor=>{const popup=E('menuPopup');if(!popup||!anchor)return;popup.classList.remove('is-hidden','prompt-budget-menu');popup.classList.add('agent-model-menu');popup.style.display='block';popup.setAttribute('aria-hidden','false');popup.style.bottom='auto';popup.style.transform='';const place=()=>{const rect=anchor.getBoundingClientRect();const width=Math.min(380,window.innerWidth-8);popup.style.width=`${width}px`;popup.style.left=`${Math.max(4,Math.min(rect.left,window.innerWidth-width-4))}px`;popup.style.top=`${Math.min(window.innerHeight-8,rect.bottom+6)}px`};place();popup.innerHTML='<div class="agent-model-summary"><strong>Loading models...</strong><small>Reading provider model catalog</small></div>';try{const cat=await loadModelCatalog(false);if(!cat||typeof cat!=='object')throw new Error('Model catalog unavailable');applyModelCatalog(cat);const options=Array.isArray(cat.options)?cat.options:[];popup.innerHTML='';const head=document.createElement('div');head.className='agent-model-summary';head.innerHTML='<strong>Model management</strong><small>Select a model or open its settings</small>';popup.appendChild(head);const actions=document.createElement('div');actions.className='agent-model-actions';const refresh=document.createElement('button');refresh.type='button';refresh.className='agent-model-config';refresh.title='Refresh provider models';refresh.setAttribute('aria-label','Refresh provider models');refresh.innerHTML='<span class="codicon codicon-refresh"></span>';refresh.onclick=async ev=>{ev.preventDefault();ev.stopPropagation();refresh.disabled=true;try{const next=await loadModelCatalog(true);applyModelCatalog(next);await showModelManager(anchor)}catch(error){showError(error.message||String(error))}finally{refresh.disabled=false}};const importBtn=document.createElement('button');importBtn.type='button';importBtn.className='agent-model-config';importBtn.title='Import LLM config';importBtn.setAttribute('aria-label','Import LLM config');importBtn.innerHTML='<span class="codicon codicon-cloud-upload"></span>';importBtn.onclick=ev=>{ev.preventDefault();ev.stopPropagation();popup.classList.add('is-hidden');openLlmConfigModal()};actions.append(refresh,importBtn);popup.appendChild(actions);const search=document.createElement('input');search.type='search';search.className='agent-model-search';search.placeholder='Search models';search.setAttribute('aria-label','Search models');popup.appendChild(search);const list=document.createElement('div');list.className='agent-model-list';popup.appendChild(list);const render=()=>{const q=String(search.value||'').trim().toLowerCase();list.innerHTML='';const shown=options.filter(o=>!q||[o.label,o.model,o.provider,o.display_name,o.title].join(' ').toLowerCase().includes(q));for(const option of shown){const row=document.createElement('div');row.className='agent-model-option';const select=document.createElement('button');select.type='button';select.classList.toggle('is-active',String(option.selection||'')===String(cat.selected||''));select.innerHTML=`<span class="codicon codicon-${String(option.selection||'')===String(cat.selected||'')?'check':'hubot'}"></span><span>${esc(option.label||option.selection)}</span>`;select.onclick=ev=>{ev.preventDefault();ev.stopPropagation();applyModelSelection(option.selection,anchor).catch(showError)};const settings=document.createElement('button');settings.type='button';settings.className='agent-model-config';settings.title='Model settings';settings.setAttribute('aria-label',`Settings for ${option.label||option.model||option.selection}`);settings.innerHTML='<span class="codicon codicon-settings-gear"></span>';settings.onclick=ev=>{ev.preventDefault();ev.stopPropagation();showModelSettings(anchor,option)};row.append(select,settings);list.appendChild(row)}if(!shown.length)list.innerHTML='<div class="agent-model-summary">No matching models.</div>'};search.oninput=render;render();place();setTimeout(()=>{try{search.focus()}catch(_){ }},0)}catch(error){popup.innerHTML=`<div class="agent-model-summary"><strong>Model management failed</strong><small>${esc(error.message||String(error))}</small></div>`;place()}};
94167
+ const applyModelSelection=async(selection,anchor)=>{const path=S.activeId?('/api/sessions/'+S.activeId+'/config/model'):'/api/config/model';const out=await api(path,{method:'POST',body:JSON.stringify({selection,model:selection})});applyModelCatalog(out);E('menuPopup').classList.add('is-hidden');showError(out?.queued?(out.note||'Model switch queued.'):'Model switched.');scheduleSnapshot({forceFull:true,delayMs:40,allowWhenFrozen:true})};
94168
+ const originalShowModelSettings=showModelSettings;showModelSettings=(anchor,option)=>{const selected=option||modelOption()||{};const popup=E('menuPopup');if(!popup)return;const caps=selected.capabilities||{};const supports=selected.reasoning_supported===true||caps.reasoning_supported===true;const effort=String(selected.effort||'');const maxEffort=String(selected.max_effort||'');popup.classList.remove('is-hidden','prompt-budget-menu');popup.classList.add('agent-model-menu');popup.style.display='block';popup.setAttribute('aria-hidden','false');popup.innerHTML=`<div class="agent-model-summary"><strong>${esc(selected.label||selected.model||'Model settings')}</strong><small>${esc(selected.display_name||selected.title||selected.provider||'')}</small></div><label class="model-setting-row">Effort<select id="webModelEffort" ${supports?'':'disabled'}><option value="">Auto</option><option value="off">Off</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option><option value="max">Max</option></select></label><label class="model-setting-row">Max effort<select id="webModelMaxEffort" ${supports?'':'disabled'}><option value="">No ceiling</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option><option value="max">Max</option></select></label><label class="model-setting-check"><input id="webModelThinking" type="checkbox" ${selected.thinking_stream?'checked':''}> Thinking stream</label><label class="model-setting-check"><input id="webModelResponse" type="checkbox" ${selected.response_stream?'checked':''}> Response stream</label><div class="agent-model-actions"><button id="webModelBack" class="agent-model-compact" type="button">Back</button><button id="webModelSave" class="agent-model-config" type="button" title="Save model settings" aria-label="Save model settings"><span class="codicon codicon-save"></span></button></div>`;const rect=(anchor||E('modelManageBtn')||E('modelSettingsBtn')).getBoundingClientRect();popup.style.left=`${Math.max(4,Math.min(rect.left,window.innerWidth-380))}px`;popup.style.top=`${rect.bottom+6}px`;popup.style.bottom='auto';E('webModelEffort').value=effort;E('webModelMaxEffort').value=maxEffort;E('webModelBack').onclick=ev=>{ev.stopPropagation();showModelManager(anchor||E('modelManageBtn'))};E('webModelSave').onclick=async ev=>{ev.stopPropagation();const settings={effort:String(E('webModelEffort').value||''),max_effort:String(E('webModelMaxEffort').value||''),thinking_stream:!!E('webModelThinking').checked,response_stream:!!E('webModelResponse').checked};try{const path=S.activeId?('/api/sessions/'+S.activeId+'/config/model'):'/api/config/model';const out=await api(path,{method:'POST',body:JSON.stringify({selection:selected.selection,model:selected.selection,...settings})});applyModelCatalog(out);popup.classList.add('is-hidden');showError('Model settings saved.');scheduleSnapshot({forceFull:true,delayMs:40,allowWhenFrozen:true})}catch(error){showError(error.message||String(error))}}};
94169
+ const bindWebModelManager=()=>{const settingsBtn=E('modelSettingsBtn');if(settingsBtn)settingsBtn.onclick=ev=>{ev.preventDefault();ev.stopPropagation();showModelSettings(settingsBtn)};const manageBtn=E('modelManageBtn');if(manageBtn)manageBtn.onclick=ev=>{ev.preventDefault();ev.stopPropagation();showModelManager(manageBtn)};};
94170
+ bindWebModelManager();
94171
+ const ensureModelConfigAction=()=>{
94172
+ const popup=E('menuPopup'),actions=popup?.querySelector('.agent-model-actions');
94173
+ if(!actions||actions.querySelector('[data-llm-config-action]'))return;
94174
+ const button=actions.querySelector('button[title="Import LLM config"]');
94175
+ if(button){
94176
+ button.dataset.llmConfigAction='1';
94177
+ button.title='Configure LLM';
94178
+ button.setAttribute('aria-label','Configure LLM');
94179
+ button.classList.add('agent-model-config-text');
94180
+ button.innerHTML='<span class="codicon codicon-settings-gear"></span><span>LLM Config</span>';
94181
+ button.style.width='auto';button.style.minWidth='104px';button.style.padding='0 8px';button.style.gap='5px';button.style.fontSize='11px';
94182
+ }
94183
+ };
94184
+ const modelConfigActionObserver=new MutationObserver(ensureModelConfigAction),modelPopup=E('menuPopup');
94185
+ if(modelPopup)modelConfigActionObserver.observe(modelPopup,{childList:true,subtree:true});
94186
+ // Safari may restore this document from bfcache after visiting the IDE;
94187
+ // rebind controls against the restored DOM so the gear remains functional.
94188
+ window.addEventListener('pageshow',bindWebModelManager);
94189
+ },0);
92824
94190
  const COMPACT_AUTO_REFRESH_INTERVAL_MS=260;
92825
94191
  const E=id=>document.getElementById(id);
92826
94192
  const PREVIEW_TOKENS=new Map();
@@ -93243,9 +94609,11 @@ function setTextIfChanged(el,text){
93243
94609
  }
93244
94610
  function closePopups(except=''){
93245
94611
  const keep=String(except||'');
93246
- for(const menu of document.querySelectorAll('.popup-menu')){
94612
+ for(const menu of document.querySelectorAll('.popup-menu,.menu-popup')){
93247
94613
  if(menu.id&&menu.id===keep)continue;
93248
94614
  menu.style.display='none';
94615
+ menu.classList.add('is-hidden');
94616
+ menu.setAttribute('aria-hidden','true');
93249
94617
  }
93250
94618
  S.openPopup=keep;
93251
94619
  }
@@ -93256,9 +94624,13 @@ function setPopupOpen(menuId,open){
93256
94624
  if(open){
93257
94625
  closePopups(id);
93258
94626
  menu.style.display='block';
94627
+ menu.classList.remove('is-hidden');
94628
+ menu.setAttribute('aria-hidden','false');
93259
94629
  S.openPopup=id;
93260
94630
  }else{
93261
94631
  menu.style.display='none';
94632
+ menu.classList.add('is-hidden');
94633
+ menu.setAttribute('aria-hidden','true');
93262
94634
  if(S.openPopup===id)S.openPopup='';
93263
94635
  }
93264
94636
  }
@@ -97462,6 +98834,7 @@ async function refreshAll(forceProbe=false){
97462
98834
  }
97463
98835
  function bindClick(id,fn){const el=E(id);if(el)el.onclick=fn}
97464
98836
  window.addEventListener('DOMContentLoaded',()=>bindClick('programBtn',openProgram));
98837
+ window.addEventListener('DOMContentLoaded',()=>{const select=E('modelSelect');if(select)select.onchange=()=>applyModel();const popup=E('menuPopup');if(popup)popup.addEventListener('click',event=>event.stopPropagation())});
97465
98838
  window.addEventListener('DOMContentLoaded',()=>{bindClick('refreshUserProcessesBtn',()=>refreshUserProcesses(true).catch(err=>showError(err.message)));USER_PROCESS_STATE.timer=setInterval(()=>{if(document.visibilityState!=='hidden')refreshUserProcesses(false).catch(()=>{})},5000)});
97466
98839
  window.addEventListener('DOMContentLoaded',async()=>{for(const id of ['chat','sessionList','todos','tasks','activity','commands','diffs','fileExplorer','catalog']){bindPanelScrollState(id,E(id))}const drop=E('promptComposerShell');const fileInput=E('uploadInput');const promptPick=E('promptFilePick');const promptEl=E('prompt');if(promptPick&&fileInput){promptPick.onclick=(ev)=>{ev.preventDefault();fileInput.click()}}if(drop&&fileInput){let _dragC=0;drop.setAttribute('tabindex','0');drop.addEventListener('click',e=>{if(e.target===drop&&promptEl)promptEl.focus()});fileInput.onchange=()=>uploadFiles(fileInput.files).then(()=>{fileInput.value=''}).catch(err=>showError(err.message));for(const evt of ['dragenter','dragover']){drop.addEventListener(evt,e=>{e.preventDefault();if(evt==='dragenter')_dragC++;drop.classList.add('dragover')})}for(const evt of ['dragleave','dragend']){drop.addEventListener(evt,e=>{e.preventDefault();if(evt==='dragleave')_dragC--;if(_dragC<=0){_dragC=0;drop.classList.remove('dragover')}})}drop.addEventListener('drop',e=>{e.preventDefault();_dragC=0;drop.classList.remove('dragover');const files=e.dataTransfer?.files;if(files&&files.length)uploadFiles(files).catch(err=>showError(err.message))});drop.addEventListener('paste',e=>{const files=clipboardFilesFromEvent(e);if(!files.length)return;e.preventDefault();drop.classList.add('dragover');setTimeout(()=>drop.classList.remove('dragover'),220);uploadFiles(files).catch(err=>showError(err.message||String(err)))})}const configInput=E('configInput');if(configInput){configInput.onchange=()=>uploadLlmConfigFile(configInput.files&&configInput.files[0]).then(()=>{configInput.value=''}).catch(err=>showError(err.message||String(err)))}bindClick('newSessionBtn',createSession);bindClick('renameSessionBtn',renameSession);bindClick('deleteSessionBtn',deleteSession);bindClick('applyModelBtn',applyModel);bindClick('llmConfigBtn',openLlmConfigModal);bindClick('llmModalClose',()=>{E('llmConfigModal').style.display='none'});bindClick('llmConfigConfirm',submitLlmConfig);const llmProv=E('llmProvider');if(llmProv){llmProv.addEventListener('change',()=>renderLlmFields(llmProv.value))}const llmOverlay=E('llmConfigModal');if(llmOverlay){llmOverlay.addEventListener('click',e=>{if(e.target===llmOverlay)llmOverlay.style.display='none'})}bindClick('sendBtn',sendMessage);bindClick('interruptBtn',interruptRun);bindClick('clearStaleTodosBtn',clearStaleTodos);bindClick('planModeBtn',togglePlanMode);bindClick('refreshFilesBtn',()=>refreshFileExplorer(true));bindClick('previewReloadBtn',()=>renderActivePreview(true));bindClick('previewCopyBtn',()=>copyPreviewCode());bindPopupButton('toolsMenuBtn','toolsMenu');bindClick('compactAction',(e)=>{if(e)e.preventDefault();closePopups();compactNow()});bindClick('refreshAction',(e)=>{if(e)e.preventDefault();closePopups();refreshAll(true)});bindPopupButton('levelBtn','levelMenu',(menu)=>{for(const opt of menu.querySelectorAll('.level-option')){opt.addEventListener('click',e=>{e.preventDefault();const lvl=parseInt(opt.getAttribute('data-level')||'0',10);setTaskLevel(lvl);setPopupOpen('levelMenu',false)})}});bindPopupButton('exportMenuBtn','exportMenu',(menu)=>{for(const a of menu.querySelectorAll('.export-item')){a.addEventListener('click',()=>setPopupOpen('exportMenu',false))}});document.addEventListener('click',()=>closePopups());const langSel=E('langSelect');if(langSel){langSel.onchange=()=>setLanguage(langSel.value).then(()=>applyApplicationI18n()).catch(err=>showError(err.message||String(err)))}if(promptEl){promptEl.addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){e.preventDefault();sendMessage()}})}bindApplicationStore();applyUiStyle();applyStaticUiClass();applyMainI18n();applyApplicationI18n();_bindPreviewCopyGuard();try{await refreshAll(false);if(!S.sessions.length){const bootCreate=()=>createSession({prompt:false}).catch(err=>showError(err.message||String(err)));if(typeof requestAnimationFrame==='function'){requestAnimationFrame(()=>setTimeout(bootCreate,0))}else{setTimeout(bootCreate,0)}}}catch(err){showError(err.message||String(err))}_deltaStartWatchdog();scheduleSessionPoll(false);document.addEventListener('visibilitychange',()=>{const next=document.visibilityState||'visible';if(next===S.lastVisibilityState)return;S.lastVisibilityState=next;if(next==='hidden'){if(S.deltaWatchdogTimer){clearTimeout(S.deltaWatchdogTimer);S.deltaWatchdogTimer=null}if(S.sessionPollTimer){clearTimeout(S.sessionPollTimer);S.sessionPollTimer=null}if(S.staticMode)freezeAutoUpdates();return}if(S.staticMode&&S.frozen)resumeAutoUpdates();_deltaStartWatchdog();scheduleSessionPoll(true);scheduleSnapshot({forceFull:false,delayMs:40,allowWhenFrozen:true})})})
97467
98840
  function kernelNoticeDeviceId(){let value=localStorage.getItem('clouds_kernel_notice_device')||'';if(value.length<24){const bytes=new Uint8Array(18);crypto.getRandomValues(bytes);value='web_'+Array.from(bytes,x=>x.toString(16).padStart(2,'0')).join('');localStorage.setItem('clouds_kernel_notice_device',value)}return value}
@@ -97473,6 +98846,9 @@ window.addEventListener('DOMContentLoaded',()=>{const search=E('sessionSearch');
97473
98846
 
97474
98847
  APP_CSS += r"""
97475
98848
  :root{--web-scrollbar-size:8px;--web-scrollbar-thumb:rgba(98,116,142,.44);--web-scrollbar-thumb-hover:rgba(76,98,128,.7)}
98849
+ .model-picker{display:inline-flex;align-items:center;gap:6px;max-width:min(46vw,520px);height:44px}.model-picker select{box-sizing:border-box;min-width:180px;max-width:32vw;height:44px;min-height:44px;padding:0 34px 0 12px;line-height:1.2;appearance:none;-webkit-appearance:none;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4l3 3 3-3M3 8l3-3 3 3' fill='none' stroke='%235e6c84' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 11px center}.model-picker .hidden{display:none}.model-picker button{box-sizing:border-box;flex:0 0 auto;width:44px;height:44px;min-width:44px;min-height:44px;padding:0;display:inline-flex;align-items:center;justify-content:center;line-height:1}.model-picker #modelManageBtn{font-size:25px!important;font-weight:600;line-height:1}
98850
+ .model-setting-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:6px 10px;font-size:.78rem;color:var(--fg,#0f1b2d)}.model-setting-row select{min-width:130px;padding:4px 6px;border:1px solid var(--line,#d9e1ec);border-radius:6px;background:#fff}.model-setting-check{display:flex;align-items:center;gap:8px;padding:5px 10px;font-size:.78rem}.model-setting-check input{accent-color:var(--brand,#1f6feb)}
98851
+ .menu-popup{position:fixed;z-index:10020}.menu-popup.is-hidden{display:none!important}.menu-popup:not(.is-hidden){display:block}.menu-popup.agent-model-menu{width:min(380px,calc(100vw - 8px));max-height:min(460px,72vh);overflow:auto;padding:4px;background:var(--panel,#fff);border:1px solid var(--line,#d9e1ec);border-radius:8px;box-shadow:0 10px 28px rgba(17,31,53,.22);color:var(--fg,#0f1b2d)}.agent-model-menu button{box-sizing:border-box;height:40px!important;min-height:40px!important;line-height:1.2!important;display:flex;align-items:center;color:var(--fg,#0f1b2d);border-radius:7px;padding:8px 10px!important}.agent-model-option{height:42px;padding:1px 0}.agent-model-option>button:first-child{justify-content:flex-start;text-align:left}.agent-model-option .agent-model-config{height:36px!important;min-height:36px!important;justify-content:center;padding:0!important}.agent-model-menu button:hover{background:var(--hover,#edf3fb)}.agent-model-menu button.is-active{color:var(--brand,#1f6feb);font-weight:600}.agent-model-summary{padding:8px 10px;border-bottom:1px solid var(--line,#d9e1ec);color:var(--muted,#667085);font-size:.75rem}.agent-model-summary strong,.agent-model-summary small{display:block}.agent-model-summary strong{color:var(--fg,#0f1b2d);font-size:.82rem}.agent-model-summary small{margin-top:2px}.agent-model-option{display:flex;align-items:center;gap:4px;min-width:0}.agent-model-option>button:first-child{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.agent-model-config{display:inline-grid!important;place-items:center!important;width:26px!important;min-width:26px!important;height:24px!important;min-height:24px!important;padding:0!important;border:1px solid var(--line,#d9e1ec)!important;background:transparent!important;color:var(--muted,#667085)!important}.agent-model-config:hover{background:var(--hover,#edf3fb)!important;color:var(--brand,#1f6feb)!important}.agent-model-actions{display:flex;align-items:center;justify-content:flex-end;gap:5px;padding:6px 4px}.agent-model-search{display:block;width:calc(100% - 16px);margin:7px 8px;padding:6px 8px;border:1px solid var(--line,#d9e1ec);border-radius:6px;background:var(--input,#fff);color:var(--fg,#0f1b2d);box-sizing:border-box}.agent-model-list{min-height:0}.agent-model-menu .codicon:before{display:block;font:600 14px/1 sans-serif}.agent-model-menu .codicon-refresh:before{content:"\21bb"}.agent-model-menu .codicon-cloud-upload:before{content:"\2191"}.agent-model-menu .codicon-settings-gear:before{content:"\2699"}.agent-model-menu .codicon-save:before{content:"\2713"}.agent-model-menu .codicon-check:before{content:"\2713"}.agent-model-menu .codicon-hubot:before{content:"\25c7"}
97476
98852
  .session-history-badge{box-sizing:border-box;position:absolute;right:-3px;bottom:-3px;display:flex;align-items:center;justify-content:center;width:15px;height:15px;padding:0;overflow:hidden;border:1px solid #252526;border-radius:50%;background:#c586c0;color:#fff;pointer-events:none}.session-history-badge>.codicon{box-sizing:border-box;position:relative;display:block;flex:0 0 9px;width:9px;height:9px;margin:0;border:1px solid currentColor;border-radius:50%;font-size:0;line-height:0;text-align:center}.session-history-badge>.codicon::before{content:"";position:absolute;left:3px;top:1px;width:1px;height:3px;background:currentColor;transform-origin:50% 100%;transform:rotate(0deg)}.session-history-badge>.codicon::after{content:"";position:absolute;left:4px;top:4px;width:3px;height:1px;background:currentColor;transform-origin:left center;transform:rotate(35deg)}
97477
98853
  html,body{scrollbar-gutter:stable}
97478
98854
  *{scrollbar-width:thin;scrollbar-color:transparent transparent!important}
@@ -97486,6 +98862,7 @@ html,body{scrollbar-gutter:stable}
97486
98862
  #chat,#sessionList,.chat-tabs,.preview-body,.preview-code-scroll,.msg-md .md-code,.msg-code-shell,.msg-diff-shell,#activity,#commands,#diffs,#fileExplorer,#catalog,.popup-menu,.application-list,.application-editor-body,.application-skill-catalog,.modal,.modal-body{scrollbar-gutter:stable}
97487
98863
  @media(hover:none),(pointer:coarse){*:active{scrollbar-color:var(--web-scrollbar-thumb) transparent!important}*:active::-webkit-scrollbar-thumb{background:var(--web-scrollbar-thumb)!important;background-clip:padding-box!important}}
97488
98864
  @media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
98865
+ .agent-model-menu button{height:34px!important;min-height:34px!important;padding:6px 9px!important;border-radius:7px!important}.agent-model-option{height:36px!important}.agent-model-option .agent-model-config{width:32px!important;min-width:32px!important;height:32px!important;min-height:32px!important}.agent-model-actions{gap:6px;padding:5px 4px}.agent-model-actions .agent-model-config{height:34px!important;min-height:34px!important}.agent-model-config-text{display:inline-flex!important;align-items:center!important;justify-content:center!important;place-items:unset!important;width:auto!important;min-width:104px!important;height:34px!important;min-height:34px!important;padding:0 8px!important;gap:5px!important;color:var(--brand,#1f6feb)!important;font-size:11px!important;line-height:1!important;white-space:nowrap!important}.agent-model-config-text .codicon{display:inline-block!important;flex:0 0 auto}
97489
98866
  """
97490
98867
 
97491
98868
  APP_TS = """type SessionSummary={id:string;title:string;running:boolean;updated_at:number};
@@ -98079,14 +99456,21 @@ ADMIN_INDEX_HTML = """<!doctype html>
98079
99456
  <label>History versions<input id="evolutionHistoryDepth" type="number" min="0" max="12" value="2"></label>
98080
99457
  <label>Start date<input id="evolutionStartDate" type="date"></label>
98081
99458
  <label>End date<input id="evolutionEndDate" type="date"></label>
98082
- <label>Generator profile<input id="evolutionGeneratorProfile" placeholder="global profile id"></label>
98083
- <label>Judge profile<input id="evolutionJudgeProfile" placeholder="independent profile id"></label>
99459
+ <label>生成模型<select id="evolutionGeneratorProfile" data-admin-no-i18n></select></label>
99460
+ <label>评审模型<select id="evolutionJudgeProfile" data-admin-no-i18n></select></label>
99461
+ <div class="evolution-model-info evolution-span">
99462
+ <div id="evolutionModelSources" class="evolution-model-sources" aria-live="polite"></div>
99463
+ <p id="evolutionModelResolved" class="evolution-model-resolved" data-admin-no-i18n></p>
99464
+ </div>
98084
99465
  <label class="evolution-span">User scope<input id="evolutionUserScope" placeholder="* or comma-separated user ids"></label>
98085
99466
  <label class="evolution-span">Session scope<input id="evolutionSessionScope" placeholder="* or comma-separated session ids"></label>
98086
99467
  <label class="switch-row evolution-span"><input id="evolutionEventTriggers" type="checkbox">Enable metric/event triggers</label>
98087
99468
  </div>
98088
- <div class="inline-actions evolution-actions"><button id="saveEvolutionBtn" type="button">Save Policy</button><button id="runEvolutionBtn" class="secondary" type="button">Run Now</button><button id="killEvolutionBtn" class="danger" type="button">Emergency Off</button></div>
98089
- <div id="evolutionPolicyNote" class="notice hidden"></div>
99469
+ <div id="evolutionPolicyNote" class="notice hidden" role="status" aria-live="polite"></div>
99470
+ <div class="evolution-toolbar">
99471
+ <div class="evolution-actions"><button id="saveEvolutionBtn" type="button">Save Policy</button><button id="runEvolutionBtn" class="secondary" type="button">Run Now</button><button id="killEvolutionBtn" class="danger" type="button">Emergency Off</button></div>
99472
+ <div class="evolution-secondary-actions"><button id="refreshEvolutionModelsBtn" class="ghost" type="button">刷新模型列表</button><button id="reloadEvolutionConfigBtn" class="ghost" type="button">重新载入配置</button></div>
99473
+ </div>
98090
99474
  <div id="evolutionBootstrap" class="notice">Restart source is configured in Startup Parameters: <code>inherit</code> keeps existing kernel history; <code>inject</code> adds the embedded kernel without deleting old versions.</div>
98091
99475
  </article>
98092
99476
  <article class="card evolution-current">
@@ -98351,6 +99735,30 @@ th{color:var(--muted);font-size:.72rem;text-transform:uppercase;letter-spacing:.
98351
99735
  .process-bulk-bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 12px;margin-bottom:12px;background:#fff;border:1px solid var(--line);border-radius:10px}.process-select-all{display:flex;flex-direction:row;align-items:center;gap:7px}.process-select-all input,.process-row-check{width:16px;height:16px;margin:0}.process-bulk-bar span{color:var(--muted);font-size:.78rem;margin-right:auto}
98352
99736
  .process-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(280px,360px);gap:12px;align-items:start}.process-table{min-height:250px}.process-table table{min-width:980px}.process-table td{vertical-align:top}.process-command{display:block;max-width:360px;white-space:normal;overflow-wrap:anywhere;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.72rem}.process-cell-meta{display:block;color:var(--muted);font-size:.68rem;margin-top:3px}.process-row-actions{display:flex;gap:5px}.process-row-actions button{padding:5px 8px;font-size:.7rem}.process-detail{position:sticky;top:76px;padding:14px;min-height:250px}.process-detail h3{margin:0 0 8px}.process-detail-grid{display:grid;grid-template-columns:100px 1fr;gap:6px 9px;font-size:.76rem}.process-detail-grid dt{color:var(--muted)}.process-detail-grid dd{margin:0;overflow-wrap:anywhere}.process-output{margin:12px 0 0;max-height:360px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;background:#111827;color:#e5edf8;border-radius:8px;padding:10px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace}
98353
99737
  .evolution-grid{display:grid;grid-template-columns:minmax(360px,1.15fr) minmax(320px,.85fr);gap:14px;margin-bottom:14px}.evolution-form-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.evolution-span{grid-column:1/-1}.evolution-actions{margin-top:14px}.evolution-current-body{padding:12px;border:1px solid var(--line);border-radius:10px;background:#f8fafc;margin-bottom:14px;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;overflow-wrap:anywhere}.evolution-current h4{margin:8px 0}.evolution-pipeline{display:flex;flex-wrap:wrap;gap:7px}.evolution-step{padding:7px 9px;border:1px solid var(--line);border-radius:999px;background:#fff;color:var(--muted);font-size:.72rem}.evolution-step.active{border-color:#8cb2f7;background:#eaf1ff;color:#1849a9}.evolution-step.done{border-color:#a6e0c6;background:#ecfdf3;color:#067647}.evolution-lineage{display:flex;gap:10px;overflow:auto;padding:8px 2px 14px}.evolution-node{min-width:210px;padding:12px;border:1px solid var(--line);border-top:4px solid #98a2b3;border-radius:11px;background:#fff;box-shadow:0 7px 18px rgba(27,39,71,.05)}.evolution-node.active{border-top-color:#12b76a}.evolution-node.canary{border-top-color:#f79009}.evolution-node.rolled_back{border-top-color:#f04438}.evolution-node strong,.evolution-node small{display:block;overflow-wrap:anywhere}.evolution-node small{margin-top:5px;color:var(--muted)}.evolution-detail pre{max-height:620px;overflow:auto;padding:12px;border-radius:9px;background:#101828;color:#e4e7ec;white-space:pre-wrap;overflow-wrap:anywhere}.evolution-score{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:10px 0}.evolution-score div{padding:9px;border:1px solid var(--line);border-radius:9px;background:#f8fafc}.evolution-score strong{display:block;font-size:1.05rem}.evolution-tables table{min-width:720px}
99738
+ .evolution-grid{align-items:start}
99739
+ .evolution-grid>.card,.evolution-tables>.card{min-width:0}
99740
+ .evolution-form-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
99741
+ .evolution-form-grid>label{min-width:0;line-height:1.5}
99742
+ .evolution-form-grid input:not([type="checkbox"]),.evolution-form-grid select{min-width:0;max-width:100%;min-height:42px}
99743
+ .evolution-form-grid select{text-overflow:ellipsis}
99744
+ .evolution-form-grid .switch-row{flex-direction:row;justify-content:flex-start;align-items:center;gap:9px;text-align:left}
99745
+ .evolution-form-grid .switch-row input{flex:0 0 18px}
99746
+ .evolution-model-info{min-width:0;padding:11px 12px;border:1px solid var(--line);border-radius:10px;background:var(--surface2)}
99747
+ .evolution-model-sources{display:flex;flex-wrap:wrap;align-items:center;gap:7px}
99748
+ .evolution-model-sources .badge{max-width:100%;white-space:normal;overflow-wrap:anywhere;font-size:.72rem;font-weight:600}
99749
+ .evolution-model-resolved{margin:9px 0 0;font-size:.75rem;line-height:1.6;color:var(--muted);white-space:pre-line;overflow-wrap:anywhere}
99750
+ .evolution-toolbar{display:grid;gap:10px;margin:14px 0}
99751
+ .evolution-actions{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:0;align-items:stretch}
99752
+ .evolution-secondary-actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;align-items:stretch}
99753
+ .evolution-toolbar button{display:flex;align-items:center;justify-content:center;min-width:0;min-height:42px;margin:0;padding:9px 10px;font-size:.84rem;line-height:1.4;white-space:normal}
99754
+ .evolution-settings .card-head{flex-wrap:wrap}
99755
+ .evolution-settings .notice{font-size:.8rem;line-height:1.6;overflow-wrap:anywhere}
99756
+ #evolutionPolicyNote{margin:12px 0 0}
99757
+ #evolutionBootstrap{margin-bottom:0}
99758
+ .evolution-current-body{white-space:pre-line}
99759
+ .evolution-tables{grid-template-columns:repeat(2,minmax(0,1fr))}
99760
+ @media(max-width:720px){.evolution-tables{grid-template-columns:minmax(0,1fr)}}
99761
+ @media(max-width:480px){.evolution-actions{grid-template-columns:repeat(2,minmax(0,1fr))}.evolution-actions .danger{grid-column:1/-1}}
98354
99762
  .login-overlay{position:fixed;inset:0;background:rgba(10,18,34,.66);display:flex;align-items:center;justify-content:center;z-index:100;backdrop-filter:blur(8px);padding:18px}
98355
99763
  .login-overlay.hidden{display:none}
98356
99764
  .login-card{width:min(440px,100%);background:#fff;border-radius:18px;padding:25px;box-shadow:0 28px 80px rgba(0,0,0,.28);display:flex;flex-direction:column;gap:14px}
@@ -98365,7 +99773,7 @@ th{color:var(--muted);font-size:.72rem;text-transform:uppercase;letter-spacing:.
98365
99773
  .toast.error{background:#912018}
98366
99774
  .collab-service-bar{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:12px 0;margin-bottom:8px;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.collab-service-copy>div{display:flex;align-items:center;gap:9px}.collab-service-copy strong{font-size:.92rem}.collab-service-copy p{margin:5px 0 0;color:var(--muted);font-size:.78rem}.collab-service-actions{justify-content:flex-end}.collab-service-bar+.notice{margin:0 0 12px}.collab-admin-layout{display:grid;grid-template-columns:minmax(310px,390px) minmax(0,1fr);gap:14px;align-items:start}.collab-project-column{display:flex;flex-direction:column;gap:10px}.collab-create-form{display:flex;flex-direction:column;gap:9px}.collab-create-form label{margin:0}.collab-filter-row,.collab-member-filter{display:grid;grid-template-columns:minmax(0,1fr) 130px;gap:8px}.collab-project-list,.collab-member-list,.collab-conflict-list{display:flex;flex-direction:column;gap:8px}.collab-project-row,.collab-member-row,.collab-conflict-row{background:#fff;border:1px solid var(--line);border-radius:10px;padding:11px}.collab-project-row{cursor:pointer}.collab-project-row.active{border-color:#8cb2f7;background:#f1f6ff}.collab-project-row h3,.collab-member-row h4,.collab-conflict-row h4,.collab-detail-head h3{margin:0}.collab-project-row p,.collab-member-row p,.collab-conflict-row p{margin:5px 0;color:var(--muted);font-size:.76rem}.collab-detail-head{display:flex;justify-content:space-between;gap:14px;align-items:flex-start;background:#fff;border:1px solid var(--line);border-radius:10px;padding:14px;margin-bottom:10px}.collab-member-filter{margin-bottom:10px}.collab-member-row-head,.collab-conflict-head{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}.collab-device-list{margin-top:8px;border-top:1px solid var(--line);padding-top:7px}.collab-device-row{display:flex;align-items:center;gap:8px;padding:5px 0;font-size:.75rem}.collab-device-row span:first-child{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.collab-device-row .inline-actions{margin-left:auto}.collab-conflict-row{border-color:#f3d39d;background:#fffaf0}.collab-conflict-row .conflict-meta{display:flex;gap:6px;flex-wrap:wrap;margin-top:7px}.collab-conflict-row .conflict-meta span{font-size:.69rem;background:#fff;border:1px solid #f3d39d;border-radius:999px;padding:3px 6px;color:#7a2e0e}.section-head.compact{margin-top:18px;padding-bottom:8px}.section-head.compact h3{margin:0}.section-head.compact p{font-size:.76rem}.collab-detail-column>.empty{margin:0}.collab-member-row .inline-actions button,.collab-conflict-row button,.collab-detail-head .inline-actions button{padding:5px 8px;font-size:.72rem}
98367
99775
  @media(max-width:1100px){.metric-grid{grid-template-columns:repeat(3,1fr)}.config-grid{grid-template-columns:repeat(2,minmax(220px,1fr))}.apps-layout,.collab-admin-layout,.process-layout,.evolution-grid{grid-template-columns:1fr}.process-detail{position:static}.process-toolbar{grid-template-columns:repeat(2,minmax(0,1fr))}.app-builder{position:static}.focus-user-summary{grid-template-columns:repeat(3,minmax(0,1fr))}}
98368
- @media(max-width:720px){.admin-shell{padding:14px}.admin-header,.section-head,.collab-detail-head,.collab-service-bar{flex-direction:column;align-items:stretch}.header-actions{justify-content:flex-start}.metric-grid{grid-template-columns:repeat(2,1fr)}.two-column,.chart-grid{grid-template-columns:1fr}.config-grid,.process-toolbar,.evolution-form-grid{grid-template-columns:1fr}.evolution-span{grid-column:auto}.admin-nav{overflow:auto}.action-bar button{flex:1 1 145px}.user-tracking-head{align-items:stretch}.user-filter{align-items:stretch;flex-direction:column}.focus-user-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.chart-host{min-height:215px}.collab-filter-row,.collab-member-filter{grid-template-columns:1fr}.collab-service-actions{justify-content:flex-start}.collab-service-actions>*{flex:1 1 180px;text-align:center}}
99776
+ @media(max-width:720px){.admin-shell{padding:14px}.admin-header,.section-head,.collab-detail-head,.collab-service-bar{flex-direction:column;align-items:stretch}.header-actions{justify-content:flex-start}.metric-grid{grid-template-columns:repeat(2,1fr)}.two-column,.chart-grid{grid-template-columns:1fr}.config-grid,.process-toolbar,.evolution-form-grid{grid-template-columns:1fr}.evolution-span{grid-column:auto}.admin-nav{overflow:auto}.admin-nav .nav-tab{flex:0 0 auto;white-space:nowrap}.action-bar button{flex:1 1 145px}.user-tracking-head{align-items:stretch}.user-filter{align-items:stretch;flex-direction:column}.focus-user-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.chart-host{min-height:215px}.collab-filter-row,.collab-member-filter{grid-template-columns:1fr}.collab-service-actions{justify-content:flex-start}.collab-service-actions>*{flex:1 1 180px;text-align:center}}
98369
99777
  """
98370
99778
 
98371
99779
  ADMIN_JS = r"""
@@ -98431,6 +99839,20 @@ const ADMIN_I18N_ROWS=[
98431
99839
  ['导出 JSON','导出 JSON','匯出 JSON','JSON をエクスポート','Export JSON'],
98432
99840
  ['Liquid Kernel Evolution','液态内核演进','液態核心演進','Liquid Kernel 進化','Liquid Kernel Evolution'],
98433
99841
  ['版本化 Agent 内核、自主评估、随机盲评、Canary 与可回滚升级链路。','版本化 Agent 内核、自主评估、随机盲评、Canary 与可回滚升级链路。','版本化 Agent 核心、自主評估、隨機盲評、Canary 與可回復升級鏈路。','バージョン管理された Agent カーネル、自律評価、ランダム盲検、Canary、ロールバック可能な更新フロー。','Versioned agent kernel with autonomous evaluation, randomized blind judging, canary rollout, and rollback.'],
99842
+ ['Use global default','沿用全局默认','沿用全域預設','グローバル既定を使用','Use global default'],
99843
+ ['Global models','全局模型','全域模型','グローバルモデル','Global models'],
99844
+ ['Agent chat models','Agent 聊天模型','Agent 聊天模型','Agent チャットモデル','Agent chat models'],
99845
+ ['IDE account models','IDE 账号模型','IDE 帳號模型','IDE アカウントモデル','IDE account models'],
99846
+ ['available models','个可选模型','個可選模型','利用可能なモデル','available models'],
99847
+ ['Sign in to IDE to use this source.','未登录 IDE,此来源暂不可用','未登入 IDE,此來源暫不可用','IDE にログインすると利用できます','Sign in to IDE to use this source.'],
99848
+ ['private model configuration no longer exists','未找到该来源的私有模型配置','未找到此來源的私有模型設定','このソースの個人モデル設定がありません','private model configuration no longer exists'],
99849
+ ['Saved generator','已保存的生成模型','已儲存的生成模型','保存済み生成モデル','Saved generator'],
99850
+ ['Saved judge','已保存的评审模型','已儲存的評審模型','保存済み評価モデル','Saved judge'],
99851
+ ['Model directory unavailable','模型目录暂不可用','模型目錄暫不可用','モデル一覧を利用できません','Model directory unavailable'],
99852
+ ['Loading models','正在读取模型','正在讀取模型','モデルを読込中','Loading models'],
99853
+ ['selected model configuration is disabled','模型配置已停用','模型設定已停用','モデル設定は無効です','selected model configuration is disabled'],
99854
+ ['selected model has no service address','模型未配置服务地址','模型未設定服務位址','モデルのサービス URL がありません','selected model has no service address'],
99855
+ ['No model configured.','尚未配置具体模型','尚未設定具體模型','モデルが未設定です','No model configured.'],
98434
99856
  ['Evolution Policy','演进策略','演進策略','進化ポリシー','Evolution Policy'],
98435
99857
  ['Schedule','计划周期','排程週期','スケジュール','Schedule'],
98436
99858
  ['Timezone','时区','時區','タイムゾーン','Timezone'],
@@ -98615,7 +100037,7 @@ const ADMIN_I18N_ALL=[...ADMIN_I18N_ROWS,...ADMIN_CONFIG_I18N_ROWS,...ADMIN_DYNA
98615
100037
  const ADMIN_I18N_EXACT=new Map(ADMIN_I18N_ALL.map(row=>[row[0],row]));
98616
100038
  const ADMIN_I18N_FRAGMENTS=[...ADMIN_I18N_ALL].filter(row=>String(row[0]||'').length>1).sort((a,b)=>b[0].length-a[0].length);
98617
100039
  const ADMIN_TRADITIONAL_CHARS={'后':'後','发':'發','复':'復','务':'務','进':'進','启':'啟','关':'關','闭':'閉','应':'應','用':'用','网':'網','项':'項','选':'選','择':'擇','读':'讀','写':'寫','数':'數','据':'據','认':'認','证':'證','录':'錄','审':'審','计':'計','时':'時','间':'間','过':'過','滤':'濾','错':'錯','误':'誤','输':'輸','访':'訪','问':'問','标':'標','识':'識','获':'獲','显':'顯','详':'詳','页':'頁','签':'簽','态':'態','动':'動','实':'實','现':'現','历':'歷','辖':'轄','围':'圍','并':'並','与':'與','开':'開','创':'創','删':'刪','库':'庫','设':'設','置':'置','户':'戶','类':'類','总':'總','长':'長','径':'徑','轮':'輪','调':'調','整':'整','级':'級','备':'備','份':'份','从':'從','将':'將','为':'為','这':'這','个':'個','没':'沒','无':'無','对':'對','话':'話','体':'體','内':'內','链':'鏈','滚':'滾','线':'線','异':'異','步':'步','结':'結','构':'構','继':'繼','续':'續','档':'檔','归':'歸','离':'離','冻':'凍','检':'檢','测':'測','满':'滿','压':'壓','缩':'縮','较':'較','评':'評','毁':'毀','拥':'擁','护':'護','载':'載','节':'節','点':'點','弹':'彈','窗':'窗','语':'語','简':'簡','统':'統','协':'協','区':'區','称':'稱','码':'碼','险':'險','丢':'丟','仅':'僅','侧':'側','准':'準','击':'擊','叠':'疊','号':'號','听':'聽','员':'員','处':'處','尔':'爾','弃':'棄','当':'當','报':'報','换':'換','断':'斷','暂':'暫','机':'機','权':'權','来':'來','紧':'緊','约':'約','细':'細','终':'終','绑':'綁','绝':'絕','状':'狀','监':'監','盖':'蓋','确':'確','范':'範','许':'許','试':'試','说':'說','请':'請','账':'帳','跃':'躍','运':'運','远':'遠','采':'採','钟':'鐘','销':'銷','队':'隊','际':'際','验':'驗','黄':'黃','联':'聯','筛':'篩'};
98618
- const A={token:sessionStorage.getItem('clouds_coder_admin_token')||'',config:null,metrics:null,metricUserHash:'',metricResizeTimer:0,apps:[],skills:[],reviewStatus:'pending',selectedSkills:[],toastTimer:0,serverErrors:[],bootId:'',restartNonce:'',collabProjects:[],collabProject:null,collabMembers:[],collabConflicts:[],collabAudit:[],processPayload:null,selectedProcesses:[],processDetailId:'',evolution:null,evolutionTimer:0,language:'zh-CN',languageSource:'webui_user_preference',languageObserver:null,languageSyncBusy:false};
100040
+ const A={token:sessionStorage.getItem('clouds_coder_admin_token')||'',config:null,metrics:null,metricUserHash:'',metricResizeTimer:0,apps:[],skills:[],reviewStatus:'pending',selectedSkills:[],toastTimer:0,serverErrors:[],bootId:'',restartNonce:'',collabProjects:[],collabProject:null,collabMembers:[],collabConflicts:[],collabAudit:[],processPayload:null,selectedProcesses:[],processDetailId:'',evolution:null,evolutionTimer:0,evolutionDraft:null,evolutionDirty:false,evolutionBusy:false,evolutionModels:null,evolutionError:'',evolutionLoading:null,language:'zh-CN',languageSource:'webui_user_preference',languageObserver:null,languageSyncBusy:false};
98619
100041
  const E=id=>document.getElementById(id);
98620
100042
  function normalizeAdminLanguage(value){const raw=String(value||'').trim();if(ADMIN_LANGUAGE_CODES.includes(raw))return raw;const low=raw.toLowerCase();if(['zh','zh-cn','cn'].includes(low))return'zh-CN';if(['zh-tw','zh-hk','tc'].includes(low))return'zh-TW';if(['ja-jp','jp'].includes(low))return'ja';if(['en-us','en-gb'].includes(low))return'en';return'zh-CN'}
98621
100043
  function adminTraditional(value){return Array.from(String(value||''),ch=>ADMIN_TRADITIONAL_CHARS[ch]||ch).join('')}
@@ -98627,7 +100049,7 @@ function translateAdminAttributes(element,capture=false){if(!element||element.no
98627
100049
  function applyAdminI18n(root=document){const target=root?.nodeType?root:document;if(target.nodeType===3){translateAdminTextNode(target);return}if(target.nodeType===1)translateAdminAttributes(target);const walker=document.createTreeWalker(target,NodeFilter.SHOW_TEXT|NodeFilter.SHOW_ELEMENT);let current;while((current=walker.nextNode())){if(current.nodeType===3)translateAdminTextNode(current);else translateAdminAttributes(current)}document.documentElement.lang=A.language;document.title=adminTranslate('Clouds Coder Admin');renderAdminLanguageControl()}
98628
100050
  function observeAdminI18n(){if(A.languageObserver)A.languageObserver.disconnect();A.languageObserver=new MutationObserver(records=>{for(const record of records){if(record.type==='characterData'){if(ADMIN_INTERNAL_TEXT.has(record.target)){ADMIN_INTERNAL_TEXT.delete(record.target);continue}translateAdminTextNode(record.target,true);continue}for(const added of record.addedNodes||[])applyAdminI18n(added)}});A.languageObserver.observe(document.body,{subtree:true,childList:true,characterData:true})}
98629
100051
  function renderAdminLanguageControl(){const selects=[E('adminLanguageSelect'),E('adminLoginLanguageSelect')].filter(Boolean),labels=[E('adminLanguageLabel'),E('adminLoginLanguageLabel')].filter(Boolean),source=E('adminLanguageSource'),labelText={'zh-CN':'界面语言','zh-TW':'介面語言','ja':'表示言語','en':'Language'}[A.language],sourceText={'zh-CN':'与 WebUI 用户语言同步','zh-TW':'與 WebUI 使用者語言同步','ja':'WebUI のユーザー言語と同期','en':'Synced with WebUI language'}[A.language];for(const select of selects){if(select.value!==A.language)select.value=A.language;select.setAttribute('aria-label',labelText)}for(const label of labels)label.textContent=labelText;if(source)source.textContent=sourceText}
98630
- function applyAdminLanguage(language){A.language=normalizeAdminLanguage(language);try{localStorage.setItem('clouds_coder_admin_language',A.language)}catch(_){}applyAdminI18n(document)}
100052
+ function applyAdminLanguage(language){A.language=normalizeAdminLanguage(language);try{localStorage.setItem('clouds_coder_admin_language',A.language)}catch(_){}applyAdminI18n(document);if(A.evolutionModels&&A.evolutionDraft){A.evolutionDraft=collectEvolutionConfig();renderEvolutionModels()}}
98631
100053
  async function loadAdminLanguage(quiet=false){if(A.languageSyncBusy)return;A.languageSyncBusy=true;try{const out=await request('/api/admin/language');A.languageSource=String(out.source||'webui_user_preference');applyAdminLanguage(out.language)}catch(err){if(!quiet)console.warn('admin language sync failed',err)}finally{A.languageSyncBusy=false}}
98632
100054
  async function saveAdminLanguage(language){const previous=A.language;applyAdminLanguage(language);try{const out=await request('/api/admin/language',{method:'POST',body:JSON.stringify({language:A.language})});A.languageSource=String(out.source||'webui_user_preference');applyAdminLanguage(out.language)}catch(err){applyAdminLanguage(previous);toast(err.message,true)}}
98633
100055
  const ADMIN_NATIVE_CONFIRM=window.confirm.bind(window),ADMIN_NATIVE_PROMPT=window.prompt.bind(window);window.confirm=message=>ADMIN_NATIVE_CONFIRM(adminTranslate(message));window.prompt=(message,defaultValue)=>ADMIN_NATIVE_PROMPT(adminTranslate(message),defaultValue);
@@ -98636,7 +100058,7 @@ function toast(message,error=false){const el=E('toast');el.textContent=adminTran
98636
100058
  function setAuthenticated(ok){E('loginOverlay').classList.toggle('hidden',!!ok);E('connectionBadge').textContent=ok?'已认证':'未认证';E('connectionBadge').className='badge '+(ok?'good':'muted')}
98637
100059
  async function request(path,opt={}){const headers={...(opt.headers||{})};if(opt.body!==undefined&&!headers['Content-Type'])headers['Content-Type']='application/json';const r=await fetch(path,{...opt,headers});const raw=await r.text();let body={};try{body=raw?JSON.parse(raw):{}}catch(_){body={error:raw||'请求失败'}}if(!r.ok){const details=Array.isArray(body.errors)?body.errors:[];const msg=[body.error||'请求失败',...details.map(x=>String(x.key||'参数')+': '+String(x.error||'无效'))].filter(Boolean).join('\n');const err=new Error(msg);err.status=r.status;err.code=String(body.code||'');err.details=details;err.body=body;throw err}return body}
98638
100060
  async function api(path,opt={}){const headers={...(opt.headers||{})};if(A.token)headers.Authorization='Bearer '+A.token;try{return await request(path,{...opt,headers})}catch(err){if(err.status===401){clearSession();setAuthenticated(false);E('loginError').textContent='登录会话已过期,请重新登录';loadAuthStatus().catch(()=>{});}throw err}}
98639
- function clearSession(){sessionStorage.removeItem('clouds_coder_admin_token');A.token='';A.config=null;A.metrics=null;A.apps=[];A.skills=[];A.collabProjects=[];A.collabProject=null;A.collabMembers=[];A.collabConflicts=[];A.collabAudit=[];A.processPayload=null;A.selectedProcesses=[];A.processDetailId=''}
100061
+ function clearSession(){A.evolutionDraft=null;A.evolutionModels=null;A.evolutionDirty=false;sessionStorage.removeItem('clouds_coder_admin_token');A.token='';A.config=null;A.metrics=null;A.apps=[];A.skills=[];A.collabProjects=[];A.collabProject=null;A.collabMembers=[];A.collabConflicts=[];A.collabAudit=[];A.processPayload=null;A.selectedProcesses=[];A.processDetailId=''}
98640
100062
  function setAuthBusy(form,busy){const btn=form?.querySelector('button[type="submit"]');if(btn)btn.disabled=!!busy}
98641
100063
  function authError(err){const retry=Number(err?.body?.retry_after||0);E('loginError').textContent=retry?String(err.message)+'(约 '+retry+' 秒后重试)':String(err?.message||err||'认证失败')}
98642
100064
  function renderAuthState(status){const setup=!!status?.setup_required;E('retryAuthBtn').classList.add('hidden');E('setupForm').classList.toggle('hidden',!setup);E('passwordLoginForm').classList.toggle('hidden',setup);E('tokenLoginDetails').classList.toggle('hidden',!status?.token_login_enabled);E('tokenLoginBtn').textContent=setup?'用此 Token 创建管理员':'使用 Token 进入';E('tokenLoginHelp').textContent=setup?'远程首次初始化:输入现有 Admin Token,然后在上方填写账号密码并点击“创建管理员”。本机首次运行无需填写 Token。':'验证后只保存换取的短期会话,不保存原始 Admin Token。';E('authTitle').textContent=setup?'创建管理员账号':'管理员登录';if(setup){E('authDescription').textContent=status?.local_setup_allowed?'这是首次运行。请创建唯一管理员账号,密码只以强哈希形式保存在本机。':'首次创建仅允许在本机完成;远程初始化请展开高级入口并使用 Admin Token。';setTimeout(()=>E('setupUsername').focus(),0)}else{E('authDescription').textContent='使用管理员账号和密码登录。登录成功后本标签页只保存短期会话。';setTimeout(()=>E('loginUsername').focus(),0)}}
@@ -98647,7 +100069,7 @@ async function loginWithPassword(){const form=E('passwordLoginForm');setAuthBusy
98647
100069
  async function loginWithToken(){if(!E('setupForm').classList.contains('hidden')){await registerAdmin();return}const form=E('tokenLoginForm'),candidate=E('tokenInput').value.trim();if(!candidate){E('loginError').textContent='请输入 Admin Token';return}setAuthBusy(form,true);E('loginError').textContent='';try{const out=await request('/api/admin/auth/token-login',{method:'POST',headers:{Authorization:'Bearer '+candidate},body:'{}'});await acceptSession(out.access_token)}catch(err){authError(err);E('tokenInput').value='';E('tokenInput').focus()}finally{setAuthBusy(form,false)}}
98648
100070
  async function logoutAdmin(){const token=A.token;try{if(token)await request('/api/admin/auth/logout',{method:'POST',headers:{Authorization:'Bearer '+token},body:'{}'})}catch(_){}finally{clearSession();setAuthenticated(false);await loadAuthStatus().catch(err=>authError(err))}}
98649
100071
  async function bootstrapAuth(){setAuthenticated(false);if(A.token){try{const state=await request('/api/admin/auth/session',{headers:{Authorization:'Bearer '+A.token}});if(state.auth_kind==='token'){const out=await request('/api/admin/auth/token-login',{method:'POST',headers:{Authorization:'Bearer '+A.token},body:'{}'});await acceptSession(out.access_token)}else await acceptSession(A.token);return}catch(err){if(err.status===401)clearSession();else{E('authTitle').textContent='认证服务不可用';E('authDescription').textContent='暂时无法验证已保存的会话,请重试。';authError(err);E('retryAuthBtn').classList.remove('hidden');return}}}await loadAuthStatus()}
98650
- function node(tag,attrs={},text=''){const el=document.createElement(tag);for(const [k,v] of Object.entries(attrs||{})){if(k==='class')el.className=v;else if(k==='dataset')Object.assign(el.dataset,v);else if(k==='type')el.type=v;else el.setAttribute(k,String(v))}if(text!==undefined&&text!==null)el.textContent=String(text);return el}
100072
+ function node(tag,attrs={},text=''){const el=document.createElement(tag);for(const [k,v] of Object.entries(attrs||{})){if(k==='class')el.className=v;else if(k==='dataset')Object.assign(el.dataset,v);else if(k==='type')el.type=v;else if(typeof v==='boolean'&&typeof el[k]==='boolean')el[k]=v;else el.setAttribute(k,String(v))}if(text!==undefined&&text!==null)el.textContent=String(text);return el}
98651
100073
  function switchView(name){document.querySelectorAll('.nav-tab').forEach(x=>x.classList.toggle('active',x.dataset.view===name));document.querySelectorAll('.admin-view').forEach(x=>x.classList.toggle('active',x.id===name+'View'));if(name==='metrics')loadMetrics().catch(err=>toast(err.message,true));if(name==='processes')loadProcesses().catch(err=>toast(err.message,true));if(name==='config'&&!A.config)loadConfig().catch(err=>toast(err.message,true));if(name==='evolution')loadEvolution().catch(err=>toast(err.message,true));if(name==='apps')loadApps().catch(err=>toast(err.message,true));if(name==='collaboration')loadCollaboration().catch(err=>toast(err.message,true))}
98652
100074
  function table(headers,rows){if(!rows.length)return node('div',{class:'empty'},'暂无数据');const t=node('table');const thead=node('thead'),tr=node('tr');headers.forEach(h=>tr.appendChild(node('th',{},h[0])));thead.appendChild(tr);t.appendChild(thead);const tb=node('tbody');rows.forEach(row=>{const r=node('tr');headers.forEach(h=>r.appendChild(node('td',{},h[1](row))));tb.appendChild(r)});t.appendChild(tb);return t}
98653
100075
  const SVG_NS='http://www.w3.org/2000/svg',METRIC_COLORS=['series-1','series-2','series-3','series-4','series-danger','series-muted'];
@@ -98714,23 +100136,47 @@ async function stopProcessIds(ids,label){const unique=[...new Set((ids||[]).filt
98714
100136
  async function stopSelectedProcesses(){await stopProcessIds(A.selectedProcesses,'所选')}
98715
100137
  async function stopFilteredProcesses(){const rows=Array.isArray(A.processPayload?.processes)?A.processPayload.processes:[];await stopProcessIds(rows.filter(row=>row.can_stop).map(row=>row.id),'当前筛选结果中')}
98716
100138
  function bindProcesses(){E('refreshProcessesBtn').onclick=()=>loadProcesses().catch(err=>toast(err.message,true));E('filterProcessesBtn').onclick=()=>loadProcesses().catch(err=>toast(err.message,true));E('processSearch').onkeydown=ev=>{if(ev.key==='Enter')loadProcesses().catch(err=>toast(err.message,true))};E('processStatus').onchange=()=>loadProcesses().catch(err=>toast(err.message,true));E('selectAllProcesses').onchange=()=>{const rows=Array.isArray(A.processPayload?.processes)?A.processPayload.processes:[];A.selectedProcesses=E('selectAllProcesses').checked?rows.filter(row=>row.can_stop).map(row=>String(row.id)):[];renderProcesses()};E('stopSelectedProcessesBtn').onclick=()=>stopSelectedProcesses().catch(err=>toast(err.message,true));E('stopFilteredProcessesBtn').onclick=()=>stopFilteredProcesses().catch(err=>toast(err.message,true))}
98717
- const EVOLUTION_PIPELINE=['queued','collecting','assessing','proposal_ready','validating_patch','benchmarking','awaiting_approval','canary','promoted'];
100139
+ const EVOLUTION_PIPELINE=['queued','collecting','assessing','generating','proposal_ready','validating_patch','benchmarking','judging','awaiting_approval','canary','promoted'];
98718
100140
  function evolutionTime(value){const ts=Number(value||0);return ts?new Date(ts*1000).toLocaleString(A.language):'-'}
98719
100141
  function evolutionScopes(value){return String(value||'').split(',').map(x=>x.trim()).filter(Boolean).slice(0,500)}
98720
- function renderEvolution(){const data=A.evolution||{},cfg=data.config||{},active=data.active||{},bootstrap=data.bootstrap||{},runs=Array.isArray(data.runs?.runs)?data.runs.runs:[],versions=Array.isArray(data.versions?.versions)?data.versions.versions:[];E('evolutionMode').value=cfg.mode||'Off';E('evolutionSchedule').value=cfg.schedule||'off';E('evolutionTimezone').value=cfg.timezone||'Asia/Shanghai';E('evolutionHistoryDepth').value=Number(cfg.history_version_depth??2);E('evolutionStartDate').value=cfg.history_start_date||'';E('evolutionEndDate').value=cfg.history_end_date||'';E('evolutionGeneratorProfile').value=cfg.generator_profile||'';E('evolutionJudgeProfile').value=cfg.judge_profile||'';E('evolutionUserScope').value=(cfg.user_scope||['*']).join(', ');E('evolutionSessionScope').value=(cfg.session_scope||['*']).join(', ');E('evolutionEventTriggers').checked=!!cfg.event_triggers;E('evolutionRevision').textContent='rev '+String(cfg.revision||1);E('evolutionActiveBadge').textContent=cfg.mode||'Off';E('evolutionActiveBadge').className='badge '+(cfg.mode==='Off'?'muted':cfg.mode==='Aggressive'?'bad':'good');const canary=active.canary||null;E('evolutionCanaryBadge').textContent=canary?`canary ${Number(canary.percent||0)}%`:'stable';E('evolutionCanaryBadge').className='badge '+(canary?'warn':'good');E('evolutionBootstrap').textContent=`startup policy=${data.startup_policy||'inherit'} · request=${bootstrap.requested_policy||'inherit'} · ${bootstrap.history_action||'history status unavailable'}${bootstrap.injected_version?' · injected='+bootstrap.injected_version:''}`;E('runEvolutionBtn').disabled=cfg.mode==='Off'||!!data.active_run_id;E('evolutionCurrent').textContent=`active=${active.version||'-'}\ncanary=${canary?canary.version+' @ '+canary.percent+'%':'none'}\nmutable_surface=${cfg.mutable_surface||'none'}\nweights=hard 30% / LLM blind judge 70%\nminimum_gain=${Number(cfg.minimum_gain||0).toFixed(1)}\nhistory=current + ${Number(cfg.history_version_depth||0)} previous versions\nimmutable=${(data.immutable_control_components||[]).join(', ')}`;
100142
+ function renderEvolution(){const data=A.evolution||{},cfg=data.config||{},active=data.active||{},bootstrap=data.bootstrap||{},runs=Array.isArray(data.runs?.runs)?data.runs.runs:[],versions=Array.isArray(data.versions?.versions)?data.versions.versions:[];evolutionControls();E('evolutionActiveBadge').textContent=cfg.mode||'Off';E('evolutionActiveBadge').className='badge '+(cfg.mode==='Off'?'muted':cfg.mode==='Aggressive'?'bad':'good');const canary=active.canary||null;E('evolutionCanaryBadge').textContent=canary?`canary ${Number(canary.percent||0)}%`:'stable';E('evolutionCanaryBadge').className='badge '+(canary?'warn':'good');E('evolutionBootstrap').textContent=`startup policy=${data.startup_policy||'inherit'} · request=${bootstrap.requested_policy||'inherit'} · ${bootstrap.history_action||'history status unavailable'}${bootstrap.injected_version?' · injected='+bootstrap.injected_version:''}`;E('evolutionCurrent').textContent=`active=${active.version||'-'}\ncanary=${canary?canary.version+' @ '+canary.percent+'%':'none'}\nmutable_surface=${cfg.mutable_surface||'none'}\nweights=hard 30% / LLM blind judge 70%\nminimum_gain=${Number(cfg.minimum_gain||0).toFixed(1)}\nhistory=current + ${Number(cfg.history_version_depth||0)} previous versions\nimmutable=${(data.immutable_control_components||[]).join(', ')}`;
98721
100143
  const currentRun=runs.find(row=>!['no_change','rejected','failed','cancelled','promoted'].includes(String(row.status||'')))||runs[0]||{};const pipeline=E('evolutionPipeline');pipeline.innerHTML='';const currentIndex=EVOLUTION_PIPELINE.indexOf(String(currentRun.status||''));for(const [index,status] of EVOLUTION_PIPELINE.entries()){pipeline.appendChild(node('span',{class:'evolution-step '+(index<currentIndex?'done':index===currentIndex?'active':'')},status))}
98722
100144
  const lineage=E('evolutionLineage');lineage.innerHTML='';for(const row of [...versions].reverse()){const card=node('button',{type:'button',class:'evolution-node '+String(row.status||'')});card.append(node('strong',{},row.version||'-'),node('small',{},'parent '+(row.parent_version||'baseline')),node('small',{},String(row.status||'')+' · score '+Number(row.mixed_score||0).toFixed(1)),node('small',{},evolutionTime(row.promoted_at||row.created_at)));card.onclick=()=>showEvolutionVersion(row.version);lineage.appendChild(card)}if(!versions.length)lineage.appendChild(node('div',{class:'empty'},'No kernel versions'));
98723
100145
  const runHost=E('evolutionRuns');runHost.innerHTML='';if(!runs.length)runHost.appendChild(node('div',{class:'empty'},'No evolution runs'));else{const table=node('table'),head=node('tr');for(const label of ['Run','Mode','Status','Candidate','Created','Actions'])head.appendChild(node('th',{},label));table.appendChild(head);for(const row of runs){const tr=node('tr'),actions=node('td');const view=node('button',{type:'button',class:'ghost'},'View');view.onclick=()=>showEvolutionRun(row.run_id);actions.appendChild(view);if(row.status==='awaiting_approval'){const approve=node('button',{type:'button'},'Approve'),reject=node('button',{type:'button',class:'danger'},'Reject');approve.onclick=()=>evolutionRunAction(row.run_id,'approve');reject.onclick=()=>evolutionRunAction(row.run_id,'reject');actions.append(approve,reject)}else if(!['no_change','rejected','failed','cancelled','promoted','rolled_back'].includes(row.status)){const cancel=node('button',{type:'button',class:'danger'},'Cancel');cancel.onclick=()=>evolutionRunAction(row.run_id,'cancel');actions.appendChild(cancel)}for(const value of [row.run_id,row.mode,row.status,row.candidate_version||'-',evolutionTime(row.created_at)])tr.appendChild(node('td',{},value));tr.appendChild(actions);table.appendChild(tr)}runHost.appendChild(table)}
98724
100146
  const versionHost=E('evolutionVersions');versionHost.innerHTML='';if(!versions.length)versionHost.appendChild(node('div',{class:'empty'},'No versions'));else{const table=node('table'),head=node('tr');for(const label of ['Version','Status','Hard','LLM','Mixed','Actions'])head.appendChild(node('th',{},label));table.appendChild(head);for(const row of versions){const tr=node('tr'),actions=node('td'),view=node('button',{type:'button',class:'ghost'},'Diff');view.onclick=()=>showEvolutionVersion(row.version,true);actions.appendChild(view);if(row.status==='canary'&&String(active.canary?.version||'')===String(row.version||'')&&Number(active.canary?.percent||0)>=100){const promote=node('button',{type:'button'},'Promote');promote.onclick=()=>evolutionVersionAction(row.version,'promote');actions.appendChild(promote)}if(row.parent_version){const rollback=node('button',{type:'button',class:'danger'},'Rollback');rollback.onclick=()=>evolutionVersionAction(row.parent_version,'rollback');actions.appendChild(rollback)}for(const value of [row.version,row.status,Number(row.hard_score||0).toFixed(1),Number(row.soft_score||0).toFixed(1),Number(row.mixed_score||0).toFixed(1)])tr.appendChild(node('td',{},value));tr.appendChild(actions);table.appendChild(tr)}versionHost.appendChild(table)}
98725
100147
  }
98726
- async function loadEvolution(){clearTimeout(A.evolutionTimer);A.evolution=await api('/api/admin/evolution');renderEvolution();if(E('evolutionView').classList.contains('active')){const live=!!A.evolution.active_run_id||!!A.evolution.active?.canary;A.evolutionTimer=setTimeout(()=>loadEvolution().catch(err=>toast(err.message,true)),live?2500:15000)}return A.evolution}
98727
- function collectEvolutionConfig(){const current=A.evolution?.config||{};return{...current,mode:E('evolutionMode').value,schedule:E('evolutionSchedule').value,timezone:E('evolutionTimezone').value.trim(),history_version_depth:Number(E('evolutionHistoryDepth').value||2),history_start_date:E('evolutionStartDate').value,history_end_date:E('evolutionEndDate').value,user_scope:evolutionScopes(E('evolutionUserScope').value),session_scope:evolutionScopes(E('evolutionSessionScope').value),generator_profile:E('evolutionGeneratorProfile').value.trim(),judge_profile:E('evolutionJudgeProfile').value.trim(),event_triggers:E('evolutionEventTriggers').checked}}
98728
- async function saveEvolution(){const cfg=collectEvolutionConfig(),out=await api('/api/admin/evolution/config',{method:'POST',body:JSON.stringify({revision:Number(A.evolution?.config?.revision||0),values:cfg})});A.evolution.config=out.config;renderEvolution();toast('Evolution policy saved')}
98729
- async function runEvolution(){if(!confirm('Start a new liquid-kernel evolution run?'))return;const out=await api('/api/admin/evolution/runs',{method:'POST',body:JSON.stringify({trigger:'manual'})});toast('Evolution run queued: '+out.run_id);await loadEvolution()}
100148
+ const EVOLUTION_FIELDS=['Mode','Schedule','Timezone','HistoryDepth','StartDate','EndDate','GeneratorProfile','JudgeProfile','UserScope','SessionScope','EventTriggers'];
100149
+ function evolutionRefKey(ref){return ref?JSON.stringify([ref.source,ref.owner,ref.profile_id,ref.model,ref.fingerprint||'']):''}
100150
+ function renderEvolutionModels(){const catalog=A.evolutionModels||{},cfg=A.evolutionDraft||{};
100151
+ for(const role of ['generator','judge']){const el=E(role==='generator'?'evolutionGeneratorProfile':'evolutionJudgeProfile'),ref=cfg[role+'_model_ref'],legacy=cfg[role+'_profile']||'';el.innerHTML='';el.appendChild(node('option',{value:''},adminTranslate('Use global default')));
100152
+ let selected='';for(const [index,row] of (catalog.options||[]).entries()){const value=String(index);el.appendChild(node('option',{value,disabled:!row.available},row.label+(row.reason?' · '+adminTranslate(row.reason):'')));if(ref&&(row.aliases||[row.ref]).some(alias=>evolutionRefKey(alias)===evolutionRefKey(ref)))selected=value}
100153
+ if(ref&&selected===''){selected='saved';el.appendChild(node('option',{value:selected},`[${ref.source}] ${ref.model} · ${ref.profile_id} (已保存)`))}
100154
+ else if(!ref&&legacy){selected='legacy';el.appendChild(node('option',{value:selected},'旧配置 · '+legacy))}
100155
+ el.value=selected;el.title=el.selectedOptions[0]?.textContent||'';
100156
+ }
100157
+ const sourceNames={global:'Global models',agent:'Agent chat models',ide:'IDE account models'},sourceHost=E('evolutionModelSources');sourceHost.replaceChildren();
100158
+ for(const row of catalog.sources||[]){const count=(catalog.options||[]).filter(option=>option.available&&(option.sources||[]).includes(row.source)).length;const label=adminTranslate(sourceNames[row.source]||row.source)+' · '+(row.available?count+' '+adminTranslate('available models'):adminTranslate(row.reason));sourceHost.appendChild(node('span',{class:'badge '+(row.available&&count?'good':'muted'),'data-admin-no-i18n':''},label))}
100159
+ E('evolutionModelResolved').textContent=['generator','judge'].map(role=>{const row=catalog.resolved?.[role];return adminTranslate(role==='generator'?'Saved generator':'Saved judge')+': '+(row?.available?`[${row.ref.source}] ${row.ref.model} · ${row.ref.profile_id}`:adminTranslate(row?.reason||'Loading models'))}).join('\n');
100160
+ }
100161
+ function fillEvolutionForm(cfg){A.evolutionDraft=structuredClone(cfg);A.evolutionDirty=false;A.evolutionError='';E('evolutionMode').value=cfg.mode||'Off';E('evolutionSchedule').value=cfg.schedule||'off';E('evolutionTimezone').value=cfg.timezone||'Asia/Shanghai';E('evolutionHistoryDepth').value=Number(cfg.history_version_depth??2);E('evolutionStartDate').value=cfg.history_start_date||'';E('evolutionEndDate').value=cfg.history_end_date||'';E('evolutionUserScope').value=(cfg.user_scope||['*']).join(', ');E('evolutionSessionScope').value=(cfg.session_scope||['*']).join(', ');E('evolutionEventTriggers').checked=!!cfg.event_triggers;renderEvolutionModels()}
100162
+ function evolutionControls(){const cfg=A.evolutionDraft||{},server=A.evolution?.config||{},conflict=!!cfg.revision&&cfg.revision!==server.revision;
100163
+ E('evolutionRevision').textContent='rev '+(cfg.revision||'-')+(A.evolutionDirty?' · 未保存':'')+(conflict?' · 服务端已更新':'');
100164
+ E('runEvolutionBtn').disabled=!cfg.revision||A.evolutionDirty||A.evolutionBusy||conflict||server.mode==='Off'||!!A.evolution?.active_run_id;
100165
+ E('saveEvolutionBtn').disabled=!cfg.revision||!!A.evolutionBusy||conflict;
100166
+ for(const field of EVOLUTION_FIELDS)E('evolution'+field).disabled=!!A.evolutionBusy;
100167
+ const note=A.evolutionError||(conflict?'配置已被其他页面修改。点击“重新载入配置”后再保存。':A.evolutionDirty?'有未保存的更改,请先保存策略,再启动。':A.evolution?.last_trigger_error?.error|| (server.mode==='Off'?'当前为 Off,请选择运行模式并保存。':'策略已保存,可启动。'));
100168
+ E('evolutionPolicyNote').textContent=note;E('evolutionPolicyNote').classList.remove('hidden');
100169
+ }
100170
+ async function loadEvolutionModels(){const catalog=await api('/api/admin/evolution/models'),draft=A.evolutionDraft?collectEvolutionConfig():null;A.evolutionModels=catalog;if(draft)A.evolutionDraft=draft;renderEvolutionModels()}
100171
+ async function loadEvolution(){clearTimeout(A.evolutionTimer);if(A.evolutionLoading)return A.evolutionLoading;A.evolutionLoading=(async()=>{try{const data=await api('/api/admin/evolution');if(data.config.revision<(A.evolution?.config?.revision||0))return A.evolution;A.evolution=data;if(!A.evolutionDraft||(!A.evolutionDirty&&!A.evolutionBusy&&A.evolutionDraft.revision!==data.config.revision))fillEvolutionForm(data.config);renderEvolution();if(!A.evolutionModels)await loadEvolutionModels();return data}finally{A.evolutionLoading=null;if(E('evolutionView').classList.contains('active')){const live=!!A.evolution?.active_run_id||!!A.evolution?.active?.canary;A.evolutionTimer=setTimeout(()=>loadEvolution().catch(err=>toast(err.message,true)),live?2500:15000)}}})();return A.evolutionLoading}
100172
+ function collectEvolutionConfig(){const current=A.evolutionDraft||{},out={...current,mode:E('evolutionMode').value,schedule:E('evolutionSchedule').value,timezone:E('evolutionTimezone').value.trim(),history_version_depth:Number(E('evolutionHistoryDepth').value),history_start_date:E('evolutionStartDate').value,history_end_date:E('evolutionEndDate').value,user_scope:evolutionScopes(E('evolutionUserScope').value),session_scope:evolutionScopes(E('evolutionSessionScope').value),event_triggers:E('evolutionEventTriggers').checked};for(const role of ['generator','judge']){const value=E(role==='generator'?'evolutionGeneratorProfile':'evolutionJudgeProfile').value;if(value==='saved'||value==='legacy')continue;out[role+'_model_ref']=value===''?null:A.evolutionModels.options[Number(value)].ref;out[role+'_profile']=''}return out}
100173
+ async function saveEvolution(){if(A.evolutionBusy)return;const cfg=collectEvolutionConfig();A.evolutionBusy=true;A.evolutionError='';evolutionControls();try{const out=await api('/api/admin/evolution/config',{method:'POST',body:JSON.stringify({revision:cfg.revision,values:cfg})});A.evolution.config=out.config;fillEvolutionForm(out.config);toast('Evolution policy saved');await loadEvolutionModels()}catch(err){A.evolutionError=err.message;if(err.status===409)await loadEvolution();throw err}finally{A.evolutionBusy=false;evolutionControls()}}
100174
+ async function runEvolution(){if(A.evolutionBusy||E('runEvolutionBtn').disabled)return;A.evolutionBusy=true;A.evolutionError='';evolutionControls();try{const out=await api('/api/admin/evolution/runs',{method:'POST',body:JSON.stringify({revision:A.evolutionDraft.revision})});toast('任务已启动: '+out.run_id+' · '+(out.models?.generator?.model||'')+' / '+(out.models?.judge?.model||''));await loadEvolution()}catch(err){A.evolutionError=err.message;await loadEvolution();throw err}finally{A.evolutionBusy=false;evolutionControls()}}
100175
+ function bindEvolutionForm(){for(const field of EVOLUTION_FIELDS)E('evolution'+field).addEventListener('input',()=>{A.evolutionDirty=true;A.evolutionError='';for(const id of ['evolutionGeneratorProfile','evolutionJudgeProfile'])E(id).title=E(id).selectedOptions[0]?.textContent||'';evolutionControls()});E('reloadEvolutionConfigBtn').onclick=async()=>{if(A.evolutionDirty&&!confirm('放弃未保存的 Evolution 更改并重新载入?'))return;try{await loadEvolution();fillEvolutionForm(A.evolution.config);await loadEvolutionModels();evolutionControls()}catch(err){toast(err.message,true)}};E('refreshEvolutionModelsBtn').onclick=()=>loadEvolutionModels().catch(err=>toast(err.message,true))}
98730
100176
  async function emergencyEvolutionOff(){if(!confirm('Set evolution to Off, cancel the active run, and abort any Canary?'))return;await api('/api/admin/evolution/emergency-off',{method:'POST',body:'{}'});await loadEvolution();toast('Liquid kernel evolution is Off')}
98731
100177
  async function evolutionRunAction(runId,action){if(!confirm(action+' evolution run '+runId+'?'))return;await api('/api/admin/evolution/runs/'+encodeURIComponent(runId)+'/'+action,{method:'POST',body:JSON.stringify({reason:'Admin '+action})});await loadEvolution();toast('Run '+action+' accepted')}
98732
100178
  async function evolutionVersionAction(version,action){if(!confirm(action+' kernel '+version+'?'))return;await api('/api/admin/evolution/versions/'+encodeURIComponent(version)+'/'+action,{method:'POST',body:JSON.stringify({reason:'Admin '+action})});await loadEvolution();toast('Kernel '+action+' accepted')}
98733
- async function showEvolutionRun(runId){const detail=await api('/api/admin/evolution/runs/'+encodeURIComponent(runId));E('evolutionDetailCard').classList.remove('hidden');E('evolutionDetailTitle').textContent='Run '+runId;E('evolutionDetail').innerHTML='';const result=detail.result||{},scores=node('div',{class:'evolution-score'});for(const [label,value] of [['Hard',result.candidate?.hard],['LLM',result.candidate?.soft],['Mixed',result.candidate?.mixed]])scores.appendChild(node('div',{},`${label}\n${Number(value||0).toFixed(1)}`));E('evolutionDetail').append(scores,node('pre',{},JSON.stringify(detail,null,2)));E('evolutionDetailCard').scrollIntoView({behavior:'smooth'})}
100179
+ async function showEvolutionRun(runId){const detail=await api('/api/admin/evolution/runs/'+encodeURIComponent(runId));E('evolutionDetailCard').classList.remove('hidden');E('evolutionDetailTitle').textContent='Run '+runId;E('evolutionDetail').innerHTML='';const result=detail.result||{},scores=node('div',{class:'evolution-score'});for(const [label,value] of [['Hard',result.candidate?.hard],['LLM',result.candidate?.soft],['Mixed',result.candidate?.mixed]])scores.appendChild(node('div',{},`${label}\n${Number(value||0).toFixed(1)}`));const refs=detail.config||{};E('evolutionDetail').append(node('p',{},'状态: '+detail.status+' · 生成: '+(refs.generator_model_ref?.model||refs.generator_profile||'全局默认')+' · 评审: '+(refs.judge_model_ref?.model||refs.judge_profile||'全局默认')+(detail.error?' · '+detail.error:'')),scores,node('pre',{},JSON.stringify(detail,null,2)));E('evolutionDetailCard').scrollIntoView({behavior:'smooth'})}
98734
100180
  async function showEvolutionVersion(version,diffOnly=false){const path='/api/admin/evolution/versions/'+encodeURIComponent(version)+(diffOnly?'/diff':'');const detail=await api(path);E('evolutionDetailCard').classList.remove('hidden');E('evolutionDetailTitle').textContent=(diffOnly?'Diff ':'Version ')+version;E('evolutionDetail').innerHTML='';E('evolutionDetail').appendChild(node('pre',{},diffOnly?detail.diff||'No diff':JSON.stringify(detail,null,2)));E('evolutionDetailCard').scrollIntoView({behavior:'smooth'})}
98735
100181
  async function loadApps(){const [apps,skills]=await Promise.all([api('/api/admin/apps'),api('/api/apps/skills')]);A.apps=Array.isArray(apps)?apps:[];A.skills=Array.isArray(skills)?skills:[];renderSkillCatalog();renderAdminApps()}
98736
100182
  function renderSkillCatalog(){const q=String(E('adminSkillSearch').value||'').trim().toLowerCase(),host=E('adminSkillCatalog');host.innerHTML='';const selected=new Set(A.selectedSkills);const rows=A.skills.filter(s=>!q||[s.id,s.name,s.description].join(' ').toLowerCase().includes(q));if(!rows.length){host.appendChild(node('div',{class:'empty'},'没有匹配的 Skill'));return}rows.forEach(s=>{const label=node('label',{class:'skill-option'+(selected.has(s.id)?' selected':'')}),check=node('input',{type:'checkbox'});check.checked=selected.has(s.id);check.addEventListener('change',()=>toggleAdminSkill(s.id));const text=node('div');text.append(node('strong',{},s.name||s.id),node('span',{},s.id),node('span',{},s.description||''));label.append(check,text);host.appendChild(label)});renderSelectedSkills()}
@@ -98740,7 +100186,7 @@ async function createSharedApp(){const payload={name:E('adminAppName').value.tri
98740
100186
  function renderAdminApps(){const host=E('adminAppList');host.innerHTML='';document.querySelectorAll('.review-tab').forEach(x=>x.classList.toggle('active',x.dataset.status===A.reviewStatus));const rows=A.apps.filter(x=>x.status===A.reviewStatus);if(!rows.length){host.appendChild(node('div',{class:'card empty'},'此分类暂无应用'));return}rows.sort((a,b)=>(b.updated_at||0)-(a.updated_at||0)).forEach(app=>{const card=node('article',{class:'admin-app-card'}),title=node('div',{class:'admin-app-title'}),left=node('div'),h=node('h3',{},(app.icon?app.icon+' ':'')+(app.name||'未命名应用'));left.appendChild(h);title.append(left,node('span',{class:'badge '+(app.status==='published'?'good':app.status==='rejected'?'muted':'warn')},app.status));card.append(title,node('p',{},app.description||'暂无说明'));const meta=node('div',{class:'app-meta'});meta.append(node('span',{},'revision '+(app.submitted_revision||app.revision||1)),node('span',{},'owner '+(app.owner_hash||'admin')));card.appendChild(meta);const skills=node('div',{class:'app-skills'});(app.skills||[]).sort((a,b)=>(a.order||0)-(b.order||0)).forEach(s=>skills.appendChild(node('span',{},s.name||s.id)));card.appendChild(skills);if(app.review?.note)card.appendChild(node('p',{},'审核备注:'+app.review.note));const history=Array.isArray(app.lifecycle_history)?app.lifecycle_history:[];if(history.length){const lifecycle=node('div',{class:'app-lifecycle'});lifecycle.appendChild(node('strong',{},'治理历史'));[...history].reverse().forEach(item=>{const at=item.at?new Date(Number(item.at)*1000).toLocaleString('zh-CN'):'-';const text=at+' · '+String(item.action||'')+' · '+String(item.from||'')+' -> '+String(item.to||'')+' · revision '+String(item.revision||'')+(item.note?' · '+String(item.note):'');lifecycle.appendChild(node('div',{},text))});card.appendChild(lifecycle)}if(app.status==='pending'){const actions=node('div',{class:'review-actions'}),note=node('input',{placeholder:'审核备注(可选)',maxlength:'1000'}),approve=node('button',{type:'button'},'通过并发布'),reject=node('button',{type:'button',class:'danger'},'拒绝');approve.onclick=()=>reviewApp(app,true,note.value).catch(err=>toast(err.message,true));reject.onclick=()=>reviewApp(app,false,note.value).catch(err=>toast(err.message,true));actions.append(note,approve,reject);card.appendChild(actions)}else if(app.status==='published'||app.status==='unpublished'){const actions=node('div',{class:'review-actions'}),note=node('input',{placeholder:'治理备注(可选)',maxlength:'1000'}),publish=app.status==='unpublished',action=node('button',{type:'button',class:publish?'':'danger'},publish?'重新上架':'下架');action.onclick=()=>changePublication(app,publish,note.value).catch(err=>toast(err.message,true));actions.append(note,action);card.appendChild(actions)}host.appendChild(card)})}
98741
100187
  async function reviewApp(app,approve,note){await api('/api/admin/apps/'+encodeURIComponent(app.id)+'/'+(approve?'approve':'reject'),{method:'POST',body:JSON.stringify({note,revision:app.submitted_revision||app.revision})});await loadApps();toast(approve?'应用已发布':'应用已拒绝')}
98742
100188
  async function changePublication(app,publish,note){const label=publish?'重新上架':'下架';if(!confirm('确定'+label+'此共享应用吗?'))return;await api('/api/admin/apps/'+encodeURIComponent(app.id)+'/'+(publish?'republish':'unpublish'),{method:'POST',body:JSON.stringify({note,revision:app.submitted_revision||app.revision,lifecycle_revision:app.lifecycle_revision||0})});await loadApps();toast('应用已'+label)}
98743
- function bind(){document.querySelectorAll('.nav-tab').forEach(x=>x.onclick=()=>switchView(x.dataset.view));document.querySelectorAll('.review-tab').forEach(x=>x.onclick=()=>{A.reviewStatus=x.dataset.status;renderAdminApps()});E('adminLanguageSelect').onchange=()=>saveAdminLanguage(E('adminLanguageSelect').value);E('evolutionMode').onchange=()=>{const schedules={Off:'off',Tuning:'weekly',Thinking:'every_3_days',Aggressive:'daily'};E('evolutionSchedule').value=schedules[E('evolutionMode').value]||'off'};E('refreshMetricsBtn').onclick=()=>loadMetrics().catch(err=>toast(err.message,true));E('metricsHours').onchange=()=>{A.metricUserHash='';loadMetrics('').catch(err=>toast(err.message,true))};E('metricUserSelect').onchange=()=>{A.metricUserHash=E('metricUserSelect').value;loadMetrics(A.metricUserHash).catch(err=>toast(err.message,true))};E('saveConfigBtn').onclick=()=>saveConfig(false).catch(err=>{notice(err.message,true);toast(err.message,true)});E('syncActiveConfigBtn').onclick=()=>syncActiveConfig().catch(err=>toast(err.message,true));E('setDefaultBtn').onclick=()=>saveConfig(true).catch(err=>toast(err.message,true));E('restoreDefaultBtn').onclick=()=>resetConfig('default').catch(err=>toast(err.message,true));E('resetInitialBtn').onclick=()=>{if(confirm('确定重置为程序初始参数吗?'))resetConfig('initial').catch(err=>toast(err.message,true))};E('saveRestartBtn').onclick=()=>restartWithDraft().catch(err=>toast(err.message,true));E('exportConfigBtn').onclick=()=>downloadJson('clouds-coder-startup-config.json',{version:1,values:collectConfig()});E('importConfigBtn').onclick=()=>E('configFileInput').click();E('configFileInput').onchange=()=>{const f=E('configFileInput').files?.[0];if(f)importConfigFile(f).catch(err=>toast(err.message,true));E('configFileInput').value=''};E('refreshEvolutionBtn').onclick=()=>loadEvolution().catch(err=>toast(err.message,true));E('saveEvolutionBtn').onclick=()=>saveEvolution().catch(err=>toast(err.message,true));E('runEvolutionBtn').onclick=()=>runEvolution().catch(err=>toast(err.message,true));E('killEvolutionBtn').onclick=()=>emergencyEvolutionOff().catch(err=>toast(err.message,true));E('closeEvolutionDetailBtn').onclick=()=>E('evolutionDetailCard').classList.add('hidden');E('refreshAppsBtn').onclick=()=>loadApps().catch(err=>toast(err.message,true));E('adminSkillSearch').oninput=renderSkillCatalog;E('createSharedAppBtn').onclick=()=>createSharedApp().catch(err=>toast(err.message,true));E('refreshCollaborationBtn').onclick=()=>loadCollaboration().catch(err=>toast(err.message,true));E('enableLanCollaborationBtn').onclick=()=>configureLanCollaboration(true).catch(err=>toast(err.message,true));E('disableLanCollaborationBtn').onclick=()=>configureLanCollaboration(false).catch(err=>toast(err.message,true));E('createCollabProjectForm').onsubmit=ev=>{ev.preventDefault();createCollabProject().catch(err=>toast(err.message,true))};E('collabProjectSearch').oninput=()=>loadCollaboration().catch(err=>toast(err.message,true));E('collabProjectStatus').onchange=()=>loadCollaboration().catch(err=>toast(err.message,true));E('collabMemberSearch').oninput=()=>{if(A.collabProject)loadCollabProjectDetail().catch(err=>toast(err.message,true))};E('collabMemberStatus').onchange=()=>{if(A.collabProject)loadCollabProjectDetail().catch(err=>toast(err.message,true))};E('logoutBtn').onclick=()=>logoutAdmin();E('setupForm').onsubmit=ev=>{ev.preventDefault();registerAdmin()};E('passwordLoginForm').onsubmit=ev=>{ev.preventDefault();loginWithPassword()};E('tokenLoginForm').onsubmit=ev=>{ev.preventDefault();loginWithToken()};E('retryAuthBtn').onclick=()=>bootstrapAuth();window.addEventListener('focus',()=>loadAdminLanguage(true));document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible')loadAdminLanguage(true)});window.addEventListener('resize',()=>{clearTimeout(A.metricResizeTimer);A.metricResizeTimer=setTimeout(()=>{if(A.metrics&&E('metricsView').classList.contains('active'))renderMetricCharts(A.metrics)},160)})}
100189
+ function bind(){document.querySelectorAll('.nav-tab').forEach(x=>x.onclick=()=>switchView(x.dataset.view));document.querySelectorAll('.review-tab').forEach(x=>x.onclick=()=>{A.reviewStatus=x.dataset.status;renderAdminApps()});E('adminLanguageSelect').onchange=()=>saveAdminLanguage(E('adminLanguageSelect').value);E('evolutionMode').onchange=()=>{const schedules={Off:'off',Tuning:'weekly',Thinking:'every_3_days',Aggressive:'daily'};E('evolutionSchedule').value=schedules[E('evolutionMode').value]||'off'};E('refreshMetricsBtn').onclick=()=>loadMetrics().catch(err=>toast(err.message,true));E('metricsHours').onchange=()=>{A.metricUserHash='';loadMetrics('').catch(err=>toast(err.message,true))};E('metricUserSelect').onchange=()=>{A.metricUserHash=E('metricUserSelect').value;loadMetrics(A.metricUserHash).catch(err=>toast(err.message,true))};E('saveConfigBtn').onclick=()=>saveConfig(false).catch(err=>{notice(err.message,true);toast(err.message,true)});E('syncActiveConfigBtn').onclick=()=>syncActiveConfig().catch(err=>toast(err.message,true));E('setDefaultBtn').onclick=()=>saveConfig(true).catch(err=>toast(err.message,true));E('restoreDefaultBtn').onclick=()=>resetConfig('default').catch(err=>toast(err.message,true));E('resetInitialBtn').onclick=()=>{if(confirm('确定重置为程序初始参数吗?'))resetConfig('initial').catch(err=>toast(err.message,true))};E('saveRestartBtn').onclick=()=>restartWithDraft().catch(err=>toast(err.message,true));E('exportConfigBtn').onclick=()=>downloadJson('clouds-coder-startup-config.json',{version:1,values:collectConfig()});E('importConfigBtn').onclick=()=>E('configFileInput').click();E('configFileInput').onchange=()=>{const f=E('configFileInput').files?.[0];if(f)importConfigFile(f).catch(err=>toast(err.message,true));E('configFileInput').value=''};bindEvolutionForm();E('refreshEvolutionBtn').onclick=()=>loadEvolution().catch(err=>toast(err.message,true));E('saveEvolutionBtn').onclick=()=>saveEvolution().catch(err=>toast(err.message,true));E('runEvolutionBtn').onclick=()=>runEvolution().catch(err=>toast(err.message,true));E('killEvolutionBtn').onclick=()=>emergencyEvolutionOff().catch(err=>toast(err.message,true));E('closeEvolutionDetailBtn').onclick=()=>E('evolutionDetailCard').classList.add('hidden');E('refreshAppsBtn').onclick=()=>loadApps().catch(err=>toast(err.message,true));E('adminSkillSearch').oninput=renderSkillCatalog;E('createSharedAppBtn').onclick=()=>createSharedApp().catch(err=>toast(err.message,true));E('refreshCollaborationBtn').onclick=()=>loadCollaboration().catch(err=>toast(err.message,true));E('enableLanCollaborationBtn').onclick=()=>configureLanCollaboration(true).catch(err=>toast(err.message,true));E('disableLanCollaborationBtn').onclick=()=>configureLanCollaboration(false).catch(err=>toast(err.message,true));E('createCollabProjectForm').onsubmit=ev=>{ev.preventDefault();createCollabProject().catch(err=>toast(err.message,true))};E('collabProjectSearch').oninput=()=>loadCollaboration().catch(err=>toast(err.message,true));E('collabProjectStatus').onchange=()=>loadCollaboration().catch(err=>toast(err.message,true));E('collabMemberSearch').oninput=()=>{if(A.collabProject)loadCollabProjectDetail().catch(err=>toast(err.message,true))};E('collabMemberStatus').onchange=()=>{if(A.collabProject)loadCollabProjectDetail().catch(err=>toast(err.message,true))};E('logoutBtn').onclick=()=>logoutAdmin();E('setupForm').onsubmit=ev=>{ev.preventDefault();registerAdmin()};E('passwordLoginForm').onsubmit=ev=>{ev.preventDefault();loginWithPassword()};E('tokenLoginForm').onsubmit=ev=>{ev.preventDefault();loginWithToken()};E('retryAuthBtn').onclick=()=>bootstrapAuth();window.addEventListener('focus',()=>loadAdminLanguage(true));document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible')loadAdminLanguage(true)});window.addEventListener('resize',()=>{clearTimeout(A.metricResizeTimer);A.metricResizeTimer=setTimeout(()=>{if(A.metrics&&E('metricsView').classList.contains('active'))renderMetricCharts(A.metrics)},160)})}
98744
100190
  window.addEventListener('DOMContentLoaded',async()=>{let cached='zh-CN';try{cached=localStorage.getItem('clouds_coder_admin_language')||cached}catch(_){}applyAdminLanguage(cached);observeAdminI18n();bind();bindProcesses();const loginLanguage=E('adminLoginLanguageSelect');if(loginLanguage)loginLanguage.onchange=()=>saveAdminLanguage(loginLanguage.value);await loadAdminLanguage(true);await bootstrapAuth()});
98745
100191
  """
98746
100192
 
@@ -114768,6 +116214,7 @@ html,body{scrollbar-gutter:stable}
114768
116214
  .open-editors,.side-list,.toolchains,.collaboration-list,.editor-tabs,.artifact-stage,.artifact-markdown,.panel-content,.agent-todo-body,.agent-messages,.agent-composer,.agent-attachments,.agent-tool-output,.agent-diff,.agent-model-menu,.palette-results,.modal,.modal-body,.conflict-preview,.prompt-enhance-body,.application-list,.ide-application-fields,.ide-application-selected,.ide-application-catalog,.xterm-viewport{scrollbar-gutter:stable}
114769
116215
  @media(hover:none),(pointer:coarse){*:active{scrollbar-color:var(--ide-scrollbar-thumb) transparent!important}*:active::-webkit-scrollbar-thumb{background:var(--ide-scrollbar-thumb)!important;background-clip:padding-box!important}}
114770
116216
  @media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
116217
+ .agent-model-menu button{color:#ddd}.agent-model-menu button:hover{color:#fff}.agent-model-config .codicon,.agent-model-config .codicon:before{font-size:18px!important}.agent-model-config{color:#d4d4d4!important}
114771
116218
  """
114772
116219
 
114773
116220
  IDE_JS = r"""
@@ -117484,25 +118931,47 @@ class AppContext:
117484
118931
  if base:
117485
118932
  self.base_url = base
117486
118933
 
117487
- def _liquid_kernel_profile(self, profile_id: str = "") -> dict:
117488
- requested = sanitize_profile_id(str(profile_id or ""))
117489
- if requested and requested in self.global_profiles:
117490
- return dict(self.global_profiles[requested])
117491
- active = dict(self.global_profiles.get(self.global_active_profile_id, {}))
117492
- if active:
117493
- return active
117494
- return {
117495
- "provider": "ollama",
117496
- "model": self.model,
117497
- "base_url": self.base_url,
117498
- "temperature": 0.1,
117499
- }
118934
+ def _evolution_profile_source(self, source: str, owner: str) -> tuple[dict, str]:
118935
+ if source == "global":
118936
+ return copy.deepcopy(self.global_profiles), self.global_active_profile_id
118937
+ if not owner or owner in {".", ".."} or "/" in owner or "\\" in owner:
118938
+ raise LiquidKernelError("invalid_model_owner", "model owner is invalid", 403)
118939
+ if source == "ide":
118940
+ try:
118941
+ with self.ide_auth._connect() as conn:
118942
+ account = conn.execute("SELECT disabled FROM ide_accounts WHERE user_id=?", (owner,)).fetchone()
118943
+ except sqlite3.Error as exc:
118944
+ raise LiquidKernelError("model_owner_unavailable", "IDE model ownership cannot be verified", 503) from exc
118945
+ if account is None or bool(account["disabled"]):
118946
+ raise LiquidKernelError("model_owner_disabled", "IDE model owner was deleted or disabled", 409)
118947
+ path = self.codes_root / owner / "user_prefs.json"
118948
+ if not path.is_file():
118949
+ raise LiquidKernelError("model_profile_missing", "private model configuration no longer exists", 409)
118950
+ raw = self.crypto.read_json(path, {})
118951
+ profiles = raw.get("model_profiles", {}) if isinstance(raw, dict) else {}
118952
+ if not isinstance(profiles, dict):
118953
+ raise LiquidKernelError("model_profile_missing", "private model configuration cannot be read", 409)
118954
+ return profiles, str(raw.get("active_profile_id", ""))
118955
+
118956
+ def _evolution_models(self):
118957
+ from liquid_kernel.models import EvolutionModels
118958
+ return EvolutionModels(
118959
+ self._evolution_profile_source,
118960
+ lambda profile: probe_provider_models(profile, cached_only=True),
118961
+ model_runtime_settings_for,
118962
+ self.liquid_kernel.registry.signing_key,
118963
+ )
118964
+
118965
+ def _liquid_kernel_profile(self, profile_id="") -> dict:
118966
+ reference = profile_id if isinstance(profile_id, dict) else None
118967
+ _, profile = self._evolution_models().resolve(reference, "" if reference else profile_id)
118968
+ return profile
117500
118969
 
117501
- def _liquid_kernel_model_call(self, system: str, prompt: str, profile_id: str, max_tokens: int) -> dict:
118970
+ def _liquid_kernel_model_call(self, system: str, prompt: str, profile_id, max_tokens: int) -> dict:
117502
118971
  profile = self._liquid_kernel_profile(profile_id)
117503
118972
  client = OllamaClient(
117504
- base_url=str(profile.get("base_url", self.base_url) or self.base_url),
117505
- model=str(profile.get("model", self.model) or self.model),
118973
+ base_url=str(profile.get("base_url", "") or ""),
118974
+ model=str(profile["model"]),
117506
118975
  timeout=max(DEFAULT_REQUEST_TIMEOUT, min(MAX_TIMEOUT_SECONDS, 900)),
117507
118976
  provider=str(profile.get("provider", "ollama") or "ollama"),
117508
118977
  endpoint=str(profile.get("endpoint", "") or ""),
@@ -117513,27 +118982,43 @@ class AppContext:
117513
118982
  response_stream=False,
117514
118983
  )
117515
118984
  client.apply_profile(profile)
117516
- client.set_telemetry(self.telemetry.record, context_provider=lambda: {}, name="liquid_kernel_evolution")
117517
- response = client.chat(
117518
- [{"role": "user", "content": str(prompt or "")}],
117519
- system=str(system or ""),
117520
- max_tokens=max(512, min(int(max_tokens or 4096), 32_000)),
117521
- temperature=max(0.0, min(0.3, float(profile.get("temperature", 0.1) or 0.1))),
117522
- think=False,
117523
- response_stream=False,
117524
- )
118985
+ # Custom templates may contain a literal model. Bind it to the selected
118986
+ # model too, without changing the template in its encrypted source.
118987
+ if client.provider == "custom_http" and client.payload_template:
118988
+ template = client.payload_template.strip()
118989
+ if not template.startswith("{"):
118990
+ raise LiquidKernelError("invalid_model_template", "custom model template must be a JSON object")
118991
+ client.payload_template = template[:-1] + ',"model":' + json.dumps(client.model) + ',"max_tokens":' + str(max(512, min(int(max_tokens or 4096), 32_000))) + '}'
118992
+ active_run = self.liquid_kernel.active_run_id
118993
+ try:
118994
+ response = client.chat(
118995
+ [{"role": "user", "content": str(prompt or "")}],
118996
+ system=str(system or ""),
118997
+ max_tokens=max(512, min(int(max_tokens or 4096), 32_000)),
118998
+ temperature=max(0.0, min(0.3, float(profile.get("temperature", 0.1)))),
118999
+ think=False,
119000
+ response_stream=False,
119001
+ cancel_check=lambda: bool(active_run and active_run in self.liquid_kernel.cancelled),
119002
+ )
119003
+ except Exception as exc:
119004
+ if active_run in self.liquid_kernel.cancelled:
119005
+ raise LiquidKernelError("run_cancelled", "evolution run was cancelled", 409) from exc
119006
+ # Provider exceptions may contain credentials, URLs or request data.
119007
+ raise LiquidKernelError("model_call_failed", "Selected model request failed (" + type(exc).__name__ + "); check its service, credentials and model availability", 502) from exc
117525
119008
  text = str(response.get("content", "") or "")
119009
+ secrets = [profile.get("api_key", ""), *list((profile.get("headers") or {}).values())]
119010
+ for secret in secrets:
119011
+ if isinstance(secret, str) and len(secret) >= 4:
119012
+ text = text.replace(secret, "[secret redacted]")
119013
+ text = self._liquid_kernel_redact_text(text)
117526
119014
  parsed = parse_json_object(text, {})
117527
119015
  if not isinstance(parsed, dict) or not parsed:
117528
- raise LiquidKernelError(
117529
- "invalid_model_output",
117530
- "evolution model must return a JSON object",
117531
- details={"output": trim(text, 1200)},
117532
- )
119016
+ raise LiquidKernelError("invalid_model_output", "evolution model must return a JSON object")
117533
119017
  return parsed
117534
119018
 
117535
119019
  def _liquid_kernel_judge_call(self, payload: dict, profile_id: str, max_tokens: int) -> dict:
117536
- profile = str(profile_id or "").strip()
119020
+ from liquid_kernel.control import valid_judge_score
119021
+ profile = profile_id
117537
119022
  order = ["incumbent", "candidate"]
117538
119023
  random.SystemRandom().shuffle(order)
117539
119024
  source_a = str(payload.get(f"{order[0]}_source", "") or "")
@@ -117562,8 +119047,8 @@ class AppContext:
117562
119047
  profile,
117563
119048
  max_tokens,
117564
119049
  )
117565
- score_a = max(0.0, min(100.0, float(result.get("score_a", 50) or 50)))
117566
- score_b = max(0.0, min(100.0, float(result.get("score_b", 50) or 50)))
119050
+ score_a = valid_judge_score(result.get("score_a"))
119051
+ score_b = valid_judge_score(result.get("score_b"))
117567
119052
  mapped = {order[0]: score_a, order[1]: score_b}
117568
119053
  return {
117569
119054
  "incumbent": mapped["incumbent"],
@@ -117989,6 +119474,8 @@ class AppContext:
117989
119474
  experience_provider=self._liquid_kernel_experience,
117990
119475
  model_callback=self._liquid_kernel_model_call,
117991
119476
  judge_callback=self._liquid_kernel_judge_call,
119477
+ model_resolver=lambda config: self._evolution_models().bind_run(config),
119478
+ config_validator=lambda config, **kwargs: self._evolution_models().validate_config(config, **kwargs),
117992
119479
  )
117993
119480
  if self.liquid_kernel_startup_policy == "inject":
117994
119481
  injection = self.liquid_kernel.inject_embedded_kernel()
@@ -123744,6 +125231,7 @@ document.addEventListener('DOMContentLoaded', function(){{
123744
125231
  selection: str,
123745
125232
  *,
123746
125233
  client_ip: str = "",
125234
+ settings: dict | None = None,
123747
125235
  ) -> dict:
123748
125236
  collaboration = str(user_id or "").startswith("collab:")
123749
125237
  source_manager = None
@@ -123762,20 +125250,20 @@ document.addEventListener('DOMContentLoaded', function(){{
123762
125250
  "Stop active Agents in the main Web UI before changing its model."
123763
125251
  )
123764
125252
  if bool(getattr(sess, "running", False)):
123765
- sess._queue_deferred_runtime_update("model_selection", {"selection": picked, "model_override": ""})
125253
+ sess._queue_deferred_runtime_update("model_selection", {"selection": picked, "model_override": "", "settings": normalize_model_runtime_settings(settings)})
123766
125254
  if source_manager is not None:
123767
- source_manager.set_runtime_model(picked, None)
125255
+ source_manager.set_runtime_model(picked, None, settings)
123768
125256
  queued = sess.model_catalog()
123769
125257
  queued["queued"] = True
123770
125258
  queued["note"] = "Model switch queued until the current run finishes."
123771
125259
  return queued
123772
125260
  if source_manager is not None:
123773
- out = source_manager.set_runtime_model(picked, None)
125261
+ out = source_manager.set_runtime_model(picked, None, settings)
123774
125262
  if target_manager is not source_manager:
123775
125263
  self._sync_ordinary_ide_llm_source(user_id, client_ip, force=True)
123776
125264
  out = sess.model_catalog()
123777
125265
  return out
123778
- out = sess.set_runtime_selection(picked)
125266
+ out = sess.set_runtime_selection(picked, settings=settings)
123779
125267
  self.manager_for_user(user_id)._sync_from_session(sess, apply_to_all=False)
123780
125268
  return out
123781
125269
 
@@ -126449,7 +127937,10 @@ document.addEventListener('DOMContentLoaded', function(){{
126449
127937
  mgr = self.manager_for_user(user_id)
126450
127938
  sess = mgr.get(session_id)
126451
127939
  if sess and bool(getattr(sess, "_deferred_runtime_sync_requested", False)):
126452
- mgr._sync_from_session(sess, apply_to_all=False)
127940
+ # Model selection and its per-model runtime controls are user
127941
+ # preferences. Propagate them to every existing session so the
127942
+ # WebUI and IDE observe the same state immediately.
127943
+ mgr._sync_from_session(sess, apply_to_all=True)
126453
127944
  sess._deferred_runtime_sync_requested = False
126454
127945
  except Exception:
126455
127946
  pass
@@ -127261,6 +128752,7 @@ document.addEventListener('DOMContentLoaded', function(){{
127261
128752
  cfg = dict(config or {})
127262
128753
  OllamaClient.clear_global_probe_cache()
127263
128754
  parsed = parse_llm_config_profiles(cfg, self.base_url, self.model)
128755
+ probe_and_merge_model_profiles(parsed.get("profiles", []), force_refresh=True)
127264
128756
  self.default_llm_config = cfg
127265
128757
  revision = self._llm_config_revision(cfg)
127266
128758
  self.global_llm_config_revision = revision
@@ -128031,6 +129523,7 @@ Use this skill when tasks match this flow pattern and reusable execution is need
128031
129523
 
128032
129524
  def model_catalog(self) -> dict:
128033
129525
  opts = []
129526
+ profiles_changed = False
128034
129527
  for pid, profile in self.global_profiles.items():
128035
129528
  model = str(profile.get("model", ""))
128036
129529
  caps = merge_multimodal_capabilities(
@@ -128048,9 +129541,80 @@ Use this skill when tasks match this flow pattern and reusable execution is need
128048
129541
  "thinking_hint": bool(profile.get("thinking_hint", False)),
128049
129542
  "thinking_stream": bool(profile.get("thinking_stream", False)),
128050
129543
  "response_stream": bool(profile.get("response_stream", False)),
129544
+ "effort": str(profile.get("effort", "") or ""),
129545
+ "max_effort": str(profile.get("max_effort", "") or ""),
129546
+ "reasoning_supported": (
129547
+ profile.get("reasoning_supported")
129548
+ if profile.get("reasoning_supported") is not None
129549
+ else model_reasoning_style(
129550
+ str(profile.get("provider", "")), model, caps
129551
+ )
129552
+ != "none"
129553
+ ),
129554
+ "reasoning_style": str(
129555
+ profile.get("reasoning_style")
129556
+ or model_reasoning_style(str(profile.get("provider", "")), model, caps)
129557
+ or ""
129558
+ ),
129559
+ "display_name": str(profile.get("display_name", profile.get("label", pid)) or ""),
129560
+ "title": str(profile.get("title", profile.get("label", pid)) or ""),
128051
129561
  "capabilities": caps,
128052
129562
  }
128053
129563
  )
129564
+ seen = {str(x.get("selection", "")) for x in opts}
129565
+ for pid, profile in self.global_profiles.items():
129566
+ configured = [str(x).strip() for x in profile.get("models", []) if str(x).strip()]
129567
+ records = probe_provider_models(profile, background=True)
129568
+ profiles_changed = merge_probed_models_into_profile(profile, records) or profiles_changed
129569
+ for rec in records:
129570
+ model_id = str(rec.get("id", "") or "").strip()
129571
+ if model_id and model_id not in configured:
129572
+ configured.append(model_id)
129573
+ for model_id in configured:
129574
+ selection = f"{pid}::{model_id}"
129575
+ if not model_id or selection in seen:
129576
+ continue
129577
+ seen.add(selection)
129578
+ rec = next((r for r in records if str(r.get("id", "")) == model_id), {})
129579
+ rcaps = rec.get("capabilities", {}) if isinstance(rec, dict) else {}
129580
+ opts.append({
129581
+ "selection": selection, "profile_id": pid,
129582
+ "provider": profile.get("provider", ""), "model": model_id,
129583
+ "label": f"{profile.get('label', pid)} | {model_id}",
129584
+ "source": "provider-probe" if rec else profile.get("source", ""),
129585
+ "thinking_hint": bool(profile.get("thinking_hint", False)),
129586
+ "thinking_stream": bool(profile.get("thinking_stream", False)),
129587
+ "response_stream": bool(profile.get("response_stream", False)),
129588
+ "effort": str(profile.get("effort", "") or ""),
129589
+ "max_effort": str(profile.get("max_effort", "") or ""),
129590
+ "reasoning_supported": (
129591
+ rcaps.get("reasoning_supported")
129592
+ if isinstance(rcaps, dict) and rcaps.get("reasoning_supported") is not None
129593
+ else profile.get("reasoning_supported")
129594
+ if profile.get("reasoning_supported") is not None
129595
+ else model_reasoning_style(
129596
+ str(profile.get("provider", "")), model_id,
129597
+ rcaps if isinstance(rcaps, dict) else None,
129598
+ )
129599
+ != "none"
129600
+ ),
129601
+ "reasoning_style": str(
129602
+ (rcaps.get("reasoning_style") if isinstance(rcaps, dict) else "")
129603
+ or profile.get("reasoning_style", "")
129604
+ or model_reasoning_style(
129605
+ str(profile.get("provider", "")), model_id,
129606
+ rcaps if isinstance(rcaps, dict) else None,
129607
+ )
129608
+ or ""
129609
+ ),
129610
+ "display_name": str(profile.get("display_name", profile.get("label", pid)) or ""),
129611
+ "title": str(profile.get("title", profile.get("label", pid)) or ""),
129612
+ "capabilities": merge_multimodal_capabilities(infer_model_multimodal_capabilities(str(profile.get("provider", "")), model_id), parse_capability_overrides(profile.get("capabilities", {}))),
129613
+ })
129614
+ for _option in opts:
129615
+ _pid = str(_option.get("profile_id", "") or "")
129616
+ _profile = self.global_profiles.get(_pid, {})
129617
+ apply_model_option_runtime_fields(_option, _profile, str(_option.get("model", "") or ""))
128054
129618
  selected_profile = self.global_profiles.get(self.global_active_profile_id, {})
128055
129619
  selected = f"{self.global_active_profile_id}::{selected_profile.get('model', self.model)}"
128056
129620
  option_map = {str(x.get("selection", "")) for x in opts}
@@ -128067,6 +129631,25 @@ Use this skill when tasks match this flow pattern and reusable execution is need
128067
129631
  "thinking_hint": bool(selected_profile.get("thinking_hint", False)),
128068
129632
  "thinking_stream": bool(selected_profile.get("thinking_stream", False)),
128069
129633
  "response_stream": bool(selected_profile.get("response_stream", False)),
129634
+ "reasoning_supported": (
129635
+ selected_profile.get("reasoning_supported")
129636
+ if selected_profile.get("reasoning_supported") is not None
129637
+ else model_reasoning_style(
129638
+ str(selected_profile.get("provider", "")),
129639
+ str(selected_profile.get("model", self.model)),
129640
+ selected_profile.get("capabilities", {}),
129641
+ )
129642
+ != "none"
129643
+ ),
129644
+ "reasoning_style": str(
129645
+ selected_profile.get("reasoning_style")
129646
+ or model_reasoning_style(
129647
+ str(selected_profile.get("provider", "")),
129648
+ str(selected_profile.get("model", self.model)),
129649
+ selected_profile.get("capabilities", {}),
129650
+ )
129651
+ or ""
129652
+ ),
128070
129653
  "capabilities": merge_multimodal_capabilities(
128071
129654
  infer_model_multimodal_capabilities(
128072
129655
  str(selected_profile.get("provider", "")),
@@ -128076,6 +129659,13 @@ Use this skill when tasks match this flow pattern and reusable execution is need
128076
129659
  ),
128077
129660
  },
128078
129661
  )
129662
+ selected_option = next((item for item in opts if str(item.get("selection", "")) == selected), None)
129663
+ if selected_option is not None:
129664
+ apply_model_option_runtime_fields(
129665
+ selected_option,
129666
+ selected_profile,
129667
+ str(selected_option.get("model", "") or ""),
129668
+ )
128079
129669
  active_caps = merge_multimodal_capabilities(
128080
129670
  infer_model_multimodal_capabilities(
128081
129671
  str(selected_profile.get("provider", "")),
@@ -128083,6 +129673,21 @@ Use this skill when tasks match this flow pattern and reusable execution is need
128083
129673
  ),
128084
129674
  parse_capability_overrides(selected_profile.get("capabilities", {})),
128085
129675
  )
129676
+ if profiles_changed:
129677
+ self.global_profiles_payload = {
129678
+ **(self.global_profiles_payload if isinstance(self.global_profiles_payload, dict) else {}),
129679
+ "profiles": [dict(item) for item in self.global_profiles.values()],
129680
+ }
129681
+ # Keep the discovered directory in the system config so a restart
129682
+ # can render the same provider/model choices before the next probe.
129683
+ persisted = dict(self.default_llm_config or {})
129684
+ persisted["profiles"] = [dict(item) for item in self.global_profiles.values()]
129685
+ persisted["default_profile_id"] = self.global_active_profile_id
129686
+ self.default_llm_config = persisted
129687
+ try:
129688
+ LLM_CONFIG_PATH.write_text(json_dumps(persisted, indent=2), encoding="utf-8")
129689
+ except Exception:
129690
+ pass
128086
129691
  return {
128087
129692
  "provider": selected_profile.get("provider", "ollama"),
128088
129693
  "models": [x["selection"] for x in opts] or [self.model],
@@ -128094,7 +129699,7 @@ Use this skill when tasks match this flow pattern and reusable execution is need
128094
129699
  "active_capabilities": active_caps,
128095
129700
  }
128096
129701
 
128097
- def set_runtime_model(self, model: str, thinking: bool | None = None) -> dict:
129702
+ def set_runtime_model(self, model: str, thinking: bool | None = None, settings: dict | None = None) -> dict:
128098
129703
  raw = str(model or "").strip()
128099
129704
  if not raw:
128100
129705
  raise ValueError("model required")
@@ -128122,6 +129727,11 @@ Use this skill when tasks match this flow pattern and reusable execution is need
128122
129727
  self.global_profiles[pid]["capabilities"] = infer_model_multimodal_capabilities(
128123
129728
  "ollama", selected_model.strip()
128124
129729
  )
129730
+ self.global_profiles[pid] = apply_model_runtime_settings(
129731
+ self.global_profiles.get(pid, {}),
129732
+ str(self.global_profiles[pid].get("model", "") or ""),
129733
+ settings,
129734
+ )
128125
129735
  self.global_active_profile_id = pid
128126
129736
  selected_profile = dict(self.global_profiles.get(pid, {}))
128127
129737
  self._sync_global_ollama_defaults(selected_profile)
@@ -128130,7 +129740,7 @@ Use this skill when tasks match this flow pattern and reusable execution is need
128130
129740
  with self._lock:
128131
129741
  for mgr in self._session_mgrs.values():
128132
129742
  try:
128133
- mgr.set_runtime_model(selection, self.thinking)
129743
+ mgr.set_runtime_model(selection, self.thinking, settings)
128134
129744
  except Exception:
128135
129745
  pass
128136
129746
  return self.model_catalog()
@@ -129393,6 +131003,28 @@ class Handler(BaseHTTPRequestHandler):
129393
131003
  def _user_id(self) -> str:
129394
131004
  return user_id_from_ip(self._client_ip())
129395
131005
 
131006
+ def _evolution_model_context(self) -> dict:
131007
+ # Admin Authorization is distinct from IDE identity. Verify the IDE
131008
+ # cookie (or explicit IDE session header) using the existing auth store.
131009
+ token = str(self.headers.get("X-Evolution-IDE-Token", "") or "").strip()
131010
+ if not token:
131011
+ for item in str(self.headers.get("Cookie", "") or "").split(";"):
131012
+ key, sep, value = item.strip().partition("=")
131013
+ if sep and key == "clouds_ide_session":
131014
+ token = unquote(value.strip())
131015
+ break
131016
+ account = self.app.ide_auth.verify_session(token, self._client_ip()) if token else None
131017
+ if account and (bool(account.get("must_change_password")) or (not bool(getattr(self.app, "ide_password_login_enabled", True)) and account.get("session_kind") != "local_auto")):
131018
+ account = None
131019
+ return {"agent": self._user_id(), "ide": str(account.get("user_id", "")) if account else ""}
131020
+
131021
+ @staticmethod
131022
+ def _evolution_revision(payload: dict) -> int:
131023
+ revision = payload.get("revision")
131024
+ if isinstance(revision, bool) or not isinstance(revision, int) or revision < 1:
131025
+ raise LiquidKernelError("config_revision_required", "load and save the evolution policy before starting; revision is required", 409)
131026
+ return revision
131027
+
129396
131028
  def _session_mgr(self) -> SessionManager:
129397
131029
  return self.app.manager_for_user(self._user_id())
129398
131030
 
@@ -129685,6 +131317,14 @@ class Handler(BaseHTTPRequestHandler):
129685
131317
  if not self._require_admin(query):
129686
131318
  return
129687
131319
  return self._send_json(self.app.admin_config_payload())
131320
+ if path == "/api/admin/evolution/models":
131321
+ if not self._require_admin():
131322
+ return
131323
+ try:
131324
+ return self._send_json(self.app._evolution_models().catalog(
131325
+ self._evolution_model_context(), self.app.liquid_kernel.config()))
131326
+ except (LiquidKernelError, IDEAuthError) as exc:
131327
+ return self._send_json({"error": str(exc), "code": exc.code}, status=exc.status)
129688
131328
  if path == "/api/admin/evolution":
129689
131329
  if not self._require_admin(query):
129690
131330
  return
@@ -129990,10 +131630,33 @@ class Handler(BaseHTTPRequestHandler):
129990
131630
  if path == "/api/ollama/models":
129991
131631
  ollama_url = str((query.get("base_url", [""]) or [""])[0]).strip() or "http://127.0.0.1:11434"
129992
131632
  try:
129993
- models = list_ollama_models(ollama_url, timeout=5)
129994
- return self._send_json({"ok": True, "models": models, "base_url": ollama_url})
131633
+ model_records = probe_ollama_model_records(
131634
+ ollama_url,
131635
+ timeout=1.5,
131636
+ )
131637
+ models = [
131638
+ str(record.get("id", "")).strip()
131639
+ for record in model_records
131640
+ if isinstance(record, dict) and str(record.get("id", "")).strip()
131641
+ ]
131642
+ return self._send_json(
131643
+ {
131644
+ "ok": True,
131645
+ "models": models,
131646
+ "model_records": model_records,
131647
+ "base_url": ollama_url,
131648
+ }
131649
+ )
129995
131650
  except Exception as exc:
129996
- return self._send_json({"ok": False, "models": [], "error": str(exc)[:300], "base_url": ollama_url})
131651
+ return self._send_json(
131652
+ {
131653
+ "ok": False,
131654
+ "models": [],
131655
+ "model_records": [],
131656
+ "error": str(exc)[:300],
131657
+ "base_url": ollama_url,
131658
+ }
131659
+ )
129997
131660
  if path == "/api/openai_compat/models":
129998
131661
  base_url = str((query.get("base_url", [""]) or [""])[0]).strip()
129999
131662
  api_key = str((query.get("api_key", [""]) or [""])[0]).strip()
@@ -130010,7 +131673,60 @@ class Handler(BaseHTTPRequestHandler):
130010
131673
  last_error = ""
130011
131674
  notes: list[str] = []
130012
131675
  probe_headers = openai_compat_probe_headers(provider, api_key)
130013
- for models_url in openai_compat_model_list_urls(normalized_base, provider):
131676
+ model_urls = (
131677
+ [anthropic_model_list_url(normalized_base)]
131678
+ if provider == "anthropic"
131679
+ else openai_compat_model_list_urls(normalized_base, provider)
131680
+ )
131681
+ # Anthropic's Models API is cursor paginated. Fetch the complete
131682
+ # catalog so importing a provider gets every available model.
131683
+ if provider == "anthropic" and model_urls and model_urls[0]:
131684
+ models_url = model_urls[0]
131685
+ all_records: list[dict] = []
131686
+ seen_ids: set[str] = set()
131687
+ after_id = ""
131688
+ for _page in range(20):
131689
+ page_url = f"{models_url}{'&' if '?' in models_url else '?'}limit=1000"
131690
+ if after_id:
131691
+ page_url += "&after_id=" + quote(after_id, safe="")
131692
+ try:
131693
+ req = urllib.request.Request(page_url, method="GET")
131694
+ for hk, hv in probe_headers.items():
131695
+ if str(hk or "").strip() and str(hv or "").strip():
131696
+ req.add_header(str(hk), str(hv))
131697
+ with urlopen(req, timeout=8) as resp:
131698
+ payload = json.loads(resp.read().decode("utf-8", errors="replace"))
131699
+ reachable = True
131700
+ page_records = extract_openai_compat_model_records(payload)
131701
+ for record in page_records:
131702
+ mid = str(record.get("id", "") or "").strip()
131703
+ if mid and mid not in seen_ids:
131704
+ seen_ids.add(mid)
131705
+ all_records.append(record)
131706
+ if not isinstance(payload, dict) or not payload.get("has_more"):
131707
+ break
131708
+ next_after = str(payload.get("last_id", "") or "").strip()
131709
+ if not next_after or next_after == after_id:
131710
+ break
131711
+ after_id = next_after
131712
+ except urllib.error.HTTPError as exc:
131713
+ reachable = int(getattr(exc, "code", 0) or 0) in {200, 401, 403, 404, 405}
131714
+ last_error = f"HTTP {int(getattr(exc, 'code', 0) or 0)}"
131715
+ break
131716
+ except Exception as exc:
131717
+ last_error = trim(str(exc), 300)
131718
+ break
131719
+ if all_records:
131720
+ return self._send_json({
131721
+ "ok": True, "reachable": True, "provider": provider,
131722
+ "models": [str(r.get("id", "")).strip() for r in all_records],
131723
+ "model_records": [
131724
+ {"id": str(r.get("id", "")).strip(), "capabilities": dict(r.get("capabilities", {}))}
131725
+ for r in all_records
131726
+ ],
131727
+ "base_url": normalized_base, "scanned_url": models_url,
131728
+ })
131729
+ for models_url in model_urls:
130014
131730
  try:
130015
131731
  req = urllib.request.Request(models_url, method="GET")
130016
131732
  for hk, hv in probe_headers.items():
@@ -130023,7 +131739,12 @@ class Handler(BaseHTTPRequestHandler):
130023
131739
  payload = json.loads(body_text)
130024
131740
  except Exception:
130025
131741
  payload = {}
130026
- model_ids = extract_openai_compat_model_ids(payload)
131742
+ model_records = extract_openai_compat_model_records(payload)
131743
+ model_ids = [
131744
+ str(record.get("id", "")).strip()
131745
+ for record in model_records
131746
+ if isinstance(record, dict) and str(record.get("id", "")).strip()
131747
+ ]
130027
131748
  if model_ids:
130028
131749
  return self._send_json(
130029
131750
  {
@@ -130031,6 +131752,17 @@ class Handler(BaseHTTPRequestHandler):
130031
131752
  "reachable": True,
130032
131753
  "provider": provider,
130033
131754
  "models": model_ids,
131755
+ "model_records": [
131756
+ {
131757
+ "id": model_id,
131758
+ "capabilities": dict(
131759
+ model_records[index].get("capabilities", {})
131760
+ )
131761
+ if isinstance(model_records[index], dict)
131762
+ else {},
131763
+ }
131764
+ for index, model_id in enumerate(model_ids)
131765
+ ],
130034
131766
  "base_url": normalized_base,
130035
131767
  "scanned_url": models_url,
130036
131768
  }
@@ -130049,7 +131781,12 @@ class Handler(BaseHTTPRequestHandler):
130049
131781
  payload = json.loads(body_text) if body_text else {}
130050
131782
  except Exception:
130051
131783
  payload = {}
130052
- model_ids = extract_openai_compat_model_ids(payload)
131784
+ model_records = extract_openai_compat_model_records(payload)
131785
+ model_ids = [
131786
+ str(record.get("id", "")).strip()
131787
+ for record in model_records
131788
+ if isinstance(record, dict) and str(record.get("id", "")).strip()
131789
+ ]
130053
131790
  if model_ids:
130054
131791
  return self._send_json(
130055
131792
  {
@@ -130057,6 +131794,17 @@ class Handler(BaseHTTPRequestHandler):
130057
131794
  "reachable": True,
130058
131795
  "provider": provider,
130059
131796
  "models": model_ids,
131797
+ "model_records": [
131798
+ {
131799
+ "id": model_id,
131800
+ "capabilities": dict(
131801
+ model_records[index].get("capabilities", {})
131802
+ )
131803
+ if isinstance(model_records[index], dict)
131804
+ else {},
131805
+ }
131806
+ for index, model_id in enumerate(model_ids)
131807
+ ],
130060
131808
  "base_url": normalized_base,
130061
131809
  "scanned_url": models_url,
130062
131810
  }
@@ -130089,6 +131837,7 @@ class Handler(BaseHTTPRequestHandler):
130089
131837
  "reachable": True,
130090
131838
  "provider": provider,
130091
131839
  "models": [],
131840
+ "model_records": [],
130092
131841
  "base_url": normalized_base,
130093
131842
  "note": "endpoint reachable; no standard model list returned",
130094
131843
  "error": last_error,
@@ -130101,6 +131850,7 @@ class Handler(BaseHTTPRequestHandler):
130101
131850
  "reachable": False,
130102
131851
  "provider": provider,
130103
131852
  "models": [],
131853
+ "model_records": [],
130104
131854
  "error": last_error or "unable to reach endpoint",
130105
131855
  "base_url": normalized_base,
130106
131856
  "attempts": notes[-6:],
@@ -130113,6 +131863,7 @@ class Handler(BaseHTTPRequestHandler):
130113
131863
  "reachable": False,
130114
131864
  "provider": provider,
130115
131865
  "models": [],
131866
+ "model_records": [],
130116
131867
  "error": str(exc)[:300],
130117
131868
  "base_url": extract_base_url(base_url),
130118
131869
  }
@@ -130362,6 +132113,68 @@ class Handler(BaseHTTPRequestHandler):
130362
132113
  return self._auth_error(exc)
130363
132114
  except Exception:
130364
132115
  return self._send_json({"error": "invalid authentication request", "code": "invalid_request"}, status=400)
132116
+ if path == "/api/admin/evolution/config":
132117
+ if not self._require_admin():
132118
+ return
132119
+ payload = self._read_json()
132120
+ try:
132121
+ return self._send_json(self.app.liquid_kernel.save_config(
132122
+ payload.get("values", payload), expected_revision=self._evolution_revision(payload),
132123
+ context=self._evolution_model_context(),
132124
+ ))
132125
+ except (LiquidKernelError, IDEAuthError) as exc:
132126
+ return self._send_json({"error": str(exc), "code": exc.code, "details": getattr(exc, "details", {})}, status=exc.status)
132127
+ except (ValueError, TypeError):
132128
+ return self._send_json({"error": "invalid evolution policy values", "code": "invalid_config"}, status=400)
132129
+ except (OSError, sqlite3.Error):
132130
+ return self._send_json({"error": "evolution configuration storage is unavailable", "code": "config_storage_unavailable"}, status=503)
132131
+ if path == "/api/admin/evolution/emergency-off":
132132
+ if not self._require_admin():
132133
+ return
132134
+ try:
132135
+ self._read_json()
132136
+ return self._send_json(self.app.liquid_kernel.emergency_off())
132137
+ except LiquidKernelError as exc:
132138
+ return self._send_json({"error": str(exc), "code": exc.code, "details": exc.details}, status=exc.status)
132139
+ if path == "/api/admin/evolution/runs":
132140
+ if not self._require_admin():
132141
+ return
132142
+ payload = self._read_json()
132143
+ try:
132144
+ return self._send_json(self.app.liquid_kernel.trigger("manual", expected_revision=self._evolution_revision(payload)), status=202)
132145
+ except LiquidKernelError as exc:
132146
+ return self._send_json({"error": str(exc), "code": exc.code, "details": exc.details}, status=exc.status)
132147
+ m_evolution_run_action = re.match(r"^/api/admin/evolution/runs/([^/]+)/(approve|reject|cancel)$", path)
132148
+ if m_evolution_run_action:
132149
+ if not self._require_admin():
132150
+ return
132151
+ payload = self._read_json()
132152
+ try:
132153
+ action = m_evolution_run_action.group(2)
132154
+ if action == "approve":
132155
+ out = self.app.liquid_kernel.approve(m_evolution_run_action.group(1))
132156
+ elif action == "reject":
132157
+ out = self.app.liquid_kernel.reject(m_evolution_run_action.group(1), str(payload.get("reason", "") or ""))
132158
+ else:
132159
+ out = self.app.liquid_kernel.cancel(m_evolution_run_action.group(1))
132160
+ return self._send_json(out)
132161
+ except LiquidKernelError as exc:
132162
+ return self._send_json({"error": str(exc), "code": exc.code, "details": exc.details}, status=exc.status)
132163
+ m_evolution_version_action = re.match(r"^/api/admin/evolution/versions/([^/]+)/(promote|rollback)$", path)
132164
+ if m_evolution_version_action:
132165
+ if not self._require_admin():
132166
+ return
132167
+ payload = self._read_json()
132168
+ try:
132169
+ if m_evolution_version_action.group(2) == "promote":
132170
+ out = self.app.liquid_kernel.registry.promote(m_evolution_version_action.group(1))
132171
+ else:
132172
+ out = self.app.liquid_kernel.registry.rollback(
132173
+ m_evolution_version_action.group(1), reason=str(payload.get("reason", "manual rollback") or "manual rollback")
132174
+ )
132175
+ return self._send_json(out)
132176
+ except LiquidKernelError as exc:
132177
+ return self._send_json({"error": str(exc), "code": exc.code, "details": exc.details}, status=exc.status)
130365
132178
  mgr = self._session_mgr()
130366
132179
  m_process_stop = re.match(r"^/api/processes/([^/]+)/stop$", path)
130367
132180
  if m_process_stop:
@@ -130452,63 +132265,6 @@ class Handler(BaseHTTPRequestHandler):
130452
132265
  expected_revision=str(payload.get("revision", "") or ""),
130453
132266
  )
130454
132267
  return self._send_json(out, status=200 if out.get("ok") else (409 if out.get("conflict") else 400))
130455
- if path == "/api/admin/evolution/config":
130456
- if not self._require_admin():
130457
- return
130458
- payload = self._read_json()
130459
- try:
130460
- return self._send_json(self.app.liquid_kernel.save_config(
130461
- payload.get("values", payload), expected_revision=int(payload.get("revision", 0) or 0)
130462
- ))
130463
- except LiquidKernelError as exc:
130464
- return self._send_json({"error": str(exc), "code": exc.code, "details": exc.details}, status=exc.status)
130465
- if path == "/api/admin/evolution/emergency-off":
130466
- if not self._require_admin():
130467
- return
130468
- try:
130469
- self._read_json()
130470
- return self._send_json(self.app.liquid_kernel.emergency_off())
130471
- except LiquidKernelError as exc:
130472
- return self._send_json({"error": str(exc), "code": exc.code, "details": exc.details}, status=exc.status)
130473
- if path == "/api/admin/evolution/runs":
130474
- if not self._require_admin():
130475
- return
130476
- payload = self._read_json()
130477
- try:
130478
- return self._send_json(self.app.liquid_kernel.trigger(str(payload.get("trigger", "manual") or "manual")), status=202)
130479
- except LiquidKernelError as exc:
130480
- return self._send_json({"error": str(exc), "code": exc.code, "details": exc.details}, status=exc.status)
130481
- m_evolution_run_action = re.match(r"^/api/admin/evolution/runs/([^/]+)/(approve|reject|cancel)$", path)
130482
- if m_evolution_run_action:
130483
- if not self._require_admin():
130484
- return
130485
- payload = self._read_json()
130486
- try:
130487
- action = m_evolution_run_action.group(2)
130488
- if action == "approve":
130489
- out = self.app.liquid_kernel.approve(m_evolution_run_action.group(1))
130490
- elif action == "reject":
130491
- out = self.app.liquid_kernel.reject(m_evolution_run_action.group(1), str(payload.get("reason", "") or ""))
130492
- else:
130493
- out = self.app.liquid_kernel.cancel(m_evolution_run_action.group(1))
130494
- return self._send_json(out)
130495
- except LiquidKernelError as exc:
130496
- return self._send_json({"error": str(exc), "code": exc.code, "details": exc.details}, status=exc.status)
130497
- m_evolution_version_action = re.match(r"^/api/admin/evolution/versions/([^/]+)/(promote|rollback)$", path)
130498
- if m_evolution_version_action:
130499
- if not self._require_admin():
130500
- return
130501
- payload = self._read_json()
130502
- try:
130503
- if m_evolution_version_action.group(2) == "promote":
130504
- out = self.app.liquid_kernel.registry.promote(m_evolution_version_action.group(1))
130505
- else:
130506
- out = self.app.liquid_kernel.registry.rollback(
130507
- m_evolution_version_action.group(1), reason=str(payload.get("reason", "manual rollback") or "manual rollback")
130508
- )
130509
- return self._send_json(out)
130510
- except LiquidKernelError as exc:
130511
- return self._send_json({"error": str(exc), "code": exc.code, "details": exc.details}, status=exc.status)
130512
132268
  if path == "/api/admin/config/sync-active":
130513
132269
  if not self._require_admin():
130514
132270
  return
@@ -130731,7 +132487,7 @@ class Handler(BaseHTTPRequestHandler):
130731
132487
  if not model:
130732
132488
  return self._send_json({"error": "model required"}, status=400)
130733
132489
  try:
130734
- return self._send_json(mgr.set_runtime_model(model, None))
132490
+ return self._send_json(mgr.set_runtime_model(model, None, normalize_model_runtime_settings(payload)))
130735
132491
  except Exception as exc:
130736
132492
  return self._send_json({"error": str(exc)}, status=400)
130737
132493
  if path == "/api/config/language":
@@ -130788,6 +132544,7 @@ class Handler(BaseHTTPRequestHandler):
130788
132544
  if not selection:
130789
132545
  return self._send_json({"error": "selection required"}, status=400)
130790
132546
  model_override = payload.get("model_override")
132547
+ runtime_settings = normalize_model_runtime_settings(payload)
130791
132548
  if bool(getattr(sess, "running", False)):
130792
132549
  try:
130793
132550
  sess._queue_deferred_runtime_update(
@@ -130795,6 +132552,7 @@ class Handler(BaseHTTPRequestHandler):
130795
132552
  {
130796
132553
  "selection": selection,
130797
132554
  "model_override": model_override if isinstance(model_override, str) else "",
132555
+ "settings": runtime_settings,
130798
132556
  },
130799
132557
  )
130800
132558
  except Exception as exc:
@@ -130806,8 +132564,8 @@ class Handler(BaseHTTPRequestHandler):
130806
132564
  )
130807
132565
  return self._send_json(queued)
130808
132566
  try:
130809
- out = sess.set_runtime_selection(selection, model_override if isinstance(model_override, str) else None)
130810
- mgr._sync_from_session(sess, apply_to_all=False)
132567
+ out = sess.set_runtime_selection(selection, model_override if isinstance(model_override, str) else None, settings=runtime_settings)
132568
+ mgr._sync_from_session(sess, apply_to_all=True)
130811
132569
  except Exception as exc:
130812
132570
  return self._send_json({"error": str(exc)}, status=400)
130813
132571
  return self._send_json(out)
@@ -130914,7 +132672,7 @@ class Handler(BaseHTTPRequestHandler):
130914
132672
  meta = sess.add_upload(filename, raw, mime)
130915
132673
  if isinstance(meta.get("model_catalog"), dict) and not bool(meta.get("model_catalog", {}).get("queued")):
130916
132674
  try:
130917
- mgr._sync_from_session(sess, apply_to_all=False)
132675
+ mgr._sync_from_session(sess, apply_to_all=True)
130918
132676
  except Exception:
130919
132677
  pass
130920
132678
  return self._send_json(meta, status=201)
@@ -131708,7 +133466,7 @@ class SkillsHandler(BaseHTTPRequestHandler):
131708
133466
  if not selection:
131709
133467
  return self._send_json({"error": "selection required"}, status=400)
131710
133468
  try:
131711
- return self._send_json(mgr.set_runtime_model(selection, None))
133469
+ return self._send_json(mgr.set_runtime_model(selection, None, normalize_model_runtime_settings(payload)))
131712
133470
  except Exception as exc:
131713
133471
  return self._send_json({"error": str(exc)}, status=400)
131714
133472
  if path == "/api/skillslab/language":
@@ -133859,6 +135617,7 @@ class IdeHandler(BaseHTTPRequestHandler):
133859
135617
  m.group(1),
133860
135618
  str(payload.get("selection", payload.get("model", "")) or ""),
133861
135619
  client_ip=self._client_ip(),
135620
+ settings=normalize_model_runtime_settings(payload),
133862
135621
  )
133863
135622
  )
133864
135623
  except Exception as exc:
@@ -134796,6 +136555,85 @@ class McpServiceHandler(BaseHTTPRequestHandler):
134796
136555
 
134797
136556
  # Bootstrap sequence: load configuration, initialize shared application state,
134798
136557
  # and expose the HTTP service plus background runtime workers.
136558
+ def sqlite_failure_diagnostics(exc: BaseException, database=None) -> dict:
136559
+ """Read-only process/storage probes for server logs, never public responses."""
136560
+ details = {
136561
+ "pid": os.getpid(),
136562
+ "python": sys.version.split()[0],
136563
+ "sqlite": sqlite3.sqlite_version,
136564
+ "script": os.path.abspath(__file__),
136565
+ "sqlite_errorcode": getattr(exc, "sqlite_errorcode", None),
136566
+ "sqlite_errorname": getattr(exc, "sqlite_errorname", ""),
136567
+ "operation": getattr(exc, "sqlite_operation", "query"),
136568
+ }
136569
+ try:
136570
+ import resource
136571
+ soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
136572
+ details["fd_limit_soft"], details["fd_limit_hard"] = soft, hard
136573
+ except (ImportError, OSError, ValueError):
136574
+ pass
136575
+ try:
136576
+ fd = os.open(os.devnull, os.O_RDONLY)
136577
+ except OSError as probe:
136578
+ details["fd_probe_errno"] = probe.errno
136579
+ else:
136580
+ os.close(fd)
136581
+ details["fd_probe_errno"] = 0
136582
+ fd_directory = "/dev/fd" if sys.platform == "darwin" else "/proc/self/fd"
136583
+ if os.name == "posix":
136584
+ try:
136585
+ names = os.listdir(fd_directory)
136586
+ count = 0
136587
+ for name in names:
136588
+ try:
136589
+ os.fstat(int(name))
136590
+ count += 1
136591
+ except (OSError, ValueError):
136592
+ continue
136593
+ details["open_fds"] = count
136594
+ except OSError as probe:
136595
+ details["fd_count_errno"] = probe.errno
136596
+ path = getattr(exc, "sqlite_database_path", None) or database
136597
+ if path:
136598
+ path = os.path.abspath(os.fspath(path))
136599
+ details["database"] = path
136600
+ states = {}
136601
+ for label, target in (
136602
+ ("parent", os.path.dirname(path)),
136603
+ ("database", path),
136604
+ ("wal", path + "-wal"),
136605
+ ("shm", path + "-shm"),
136606
+ ):
136607
+ try:
136608
+ info = os.stat(target)
136609
+ states[label] = {
136610
+ "kind": "directory" if stat.S_ISDIR(info.st_mode) else "file",
136611
+ "mode": oct(stat.S_IMODE(info.st_mode)),
136612
+ "bytes": info.st_size,
136613
+ }
136614
+ except OSError as probe:
136615
+ states[label] = {"errno": probe.errno}
136616
+ details["paths"] = states
136617
+ # No O_CREAT: a missing authentication/collaboration DB must stay missing.
136618
+ try:
136619
+ fd = os.open(path, os.O_RDWR)
136620
+ except OSError as probe:
136621
+ details["database_open_errno"] = probe.errno
136622
+ else:
136623
+ os.close(fd)
136624
+ details["database_open_errno"] = 0
136625
+ errors = {details.get(key) for key in ("fd_probe_errno", "fd_count_errno", "database_open_errno")}
136626
+ if errors.intersection({errno.EMFILE, errno.ENFILE}):
136627
+ details["cause"] = "file_descriptor_exhaustion"
136628
+ elif details.get("database_open_errno") in {errno.EACCES, errno.EPERM, errno.EROFS}:
136629
+ details["cause"] = "storage_access_denied"
136630
+ elif details.get("database_open_errno") in {errno.ENOENT, errno.ENOTDIR}:
136631
+ details["cause"] = "storage_path_unavailable"
136632
+ else:
136633
+ details["cause"] = "undetermined"
136634
+ return details
136635
+
136636
+
134799
136637
  def collaboration_file_watcher_loop(
134800
136638
  app,
134801
136639
  stop_event,
@@ -134861,6 +136699,14 @@ def collaboration_file_watcher_loop(
134861
136699
  "[collaboration] file watcher degraded: "
134862
136700
  f"{trim(str(exc), 240)}; failures={failures}; retry_in={retry_delay:.1f}s"
134863
136701
  )
136702
+ if isinstance(exc, sqlite3.DatabaseError):
136703
+ try:
136704
+ diagnostics = sqlite_failure_diagnostics(
136705
+ exc, getattr(app.collaboration, "db_path", None)
136706
+ )
136707
+ log("[collaboration] storage diagnostics: " + json.dumps(diagnostics, ensure_ascii=True))
136708
+ except Exception as diagnostic_error:
136709
+ log(f"[collaboration] storage diagnostics unavailable: {type(diagnostic_error).__name__}")
134864
136710
 
134865
136711
 
134866
136712
  def collaboration_watcher_health(app) -> dict: