cortico 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/package.json +43 -0
  4. package/src/boot.ts +46 -0
  5. package/src/bot.ts +1342 -0
  6. package/src/config-file.ts +41 -0
  7. package/src/core/README.md +92 -0
  8. package/src/core/billing.ts +26 -0
  9. package/src/core/blobs.ts +106 -0
  10. package/src/core/bus.ts +311 -0
  11. package/src/core/config-schema.ts +201 -0
  12. package/src/core/config.ts +262 -0
  13. package/src/core/core.ts +699 -0
  14. package/src/core/cost.ts +244 -0
  15. package/src/core/event-store.ts +304 -0
  16. package/src/core/fork.ts +113 -0
  17. package/src/core/generation.ts +118 -0
  18. package/src/core/instance-lock.ts +215 -0
  19. package/src/core/ipc-logger.ts +105 -0
  20. package/src/core/language.ts +62 -0
  21. package/src/core/log-context.ts +38 -0
  22. package/src/core/loop.ts +1858 -0
  23. package/src/core/markers.ts +48 -0
  24. package/src/core/prefix.ts +121 -0
  25. package/src/core/run.ts +95 -0
  26. package/src/core/secrets.ts +18 -0
  27. package/src/core/session.ts +56 -0
  28. package/src/core/sessions.ts +261 -0
  29. package/src/core/state.ts +104 -0
  30. package/src/core/template.ts +72 -0
  31. package/src/core/timers.ts +129 -0
  32. package/src/core/tool-log.ts +135 -0
  33. package/src/core/transcript.ts +92 -0
  34. package/src/core/truncate.ts +121 -0
  35. package/src/core/types.ts +1140 -0
  36. package/src/core/usage-log.ts +79 -0
  37. package/src/core/util.ts +386 -0
  38. package/src/deploy-listing.ts +160 -0
  39. package/src/deploy.ts +162 -0
  40. package/src/extensions/README.md +72 -0
  41. package/src/extensions/dry-mount.ts +390 -0
  42. package/src/extensions/manifest.ts +153 -0
  43. package/src/extensions/runtime.ts +42 -0
  44. package/src/extensions.ts +519 -0
  45. package/src/launcher.ts +285 -0
  46. package/src/paths.ts +155 -0
  47. package/src/protocol/open-responses/LICENSE +201 -0
  48. package/src/protocol/open-responses/README.md +14 -0
  49. package/src/protocol/open-responses/context-helpers.ts +27 -0
  50. package/src/protocol/open-responses/context-log.ts +59 -0
  51. package/src/protocol/open-responses/context.ts +89 -0
  52. package/src/protocol/open-responses/generated.ts +115 -0
  53. package/src/protocol/open-responses/index.ts +33 -0
  54. package/src/protocol/open-responses/openapi.json +4230 -0
  55. package/src/protocol/open-responses/stream.ts +146 -0
  56. package/src/protocol/open-responses/tokens.ts +31 -0
  57. package/src/providers/README.md +86 -0
  58. package/src/providers/base.ts +111 -0
  59. package/src/providers/configuration.ts +97 -0
  60. package/src/providers/console/config.ts +21 -0
  61. package/src/providers/console/settings.ts +443 -0
  62. package/src/providers/console/strings.ts +74 -0
  63. package/src/providers/console/types.ts +11 -0
  64. package/src/providers/llamacpp/archive.ts +148 -0
  65. package/src/providers/llamacpp/catalog.ts +247 -0
  66. package/src/providers/llamacpp/config.ts +36 -0
  67. package/src/providers/llamacpp/console/client.ts +6 -0
  68. package/src/providers/llamacpp/console/models-panel.ts +120 -0
  69. package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
  70. package/src/providers/llamacpp/console/server.ts +153 -0
  71. package/src/providers/llamacpp/index.ts +95 -0
  72. package/src/providers/llamacpp/native.ts +51 -0
  73. package/src/providers/llamacpp/options.ts +117 -0
  74. package/src/providers/llamacpp/runtime-store.ts +139 -0
  75. package/src/providers/llamacpp/runtime.ts +214 -0
  76. package/src/providers/llamacpp/server.ts +286 -0
  77. package/src/providers/llamacpp/strings.ts +230 -0
  78. package/src/providers/openai-responses-compat/config.ts +20 -0
  79. package/src/providers/openai-responses-compat/index.ts +89 -0
  80. package/src/providers/openai-responses-compat/native.ts +127 -0
  81. package/src/providers/openai-responses-compat/strings.ts +19 -0
  82. package/src/providers/pricebook.ts +75 -0
  83. package/src/providers/registry.ts +137 -0
  84. package/src/providers/strings.ts +71 -0
  85. package/src/providers/transport/chat.ts +156 -0
  86. package/src/providers/transport/errors.ts +48 -0
  87. package/src/providers/transport/history.ts +68 -0
  88. package/src/providers/transport/native-input.ts +83 -0
  89. package/src/providers/transport/native-types.ts +13 -0
  90. package/src/providers/transport/response-assembly.ts +191 -0
  91. package/src/providers/transport/response-http.ts +196 -0
  92. package/src/providers/transport/response-meters.ts +27 -0
  93. package/src/providers/transport/responses-input.ts +56 -0
  94. package/src/web/README.md +84 -0
  95. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
  96. package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
  97. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
  98. package/src/web/client/console-pages/builtins.ts +10 -0
  99. package/src/web/client/console-pages/context.ts +135 -0
  100. package/src/web/client/console-pages/host.ts +633 -0
  101. package/src/web/client/console-pages/loader.ts +132 -0
  102. package/src/web/client/console-pages/strings.ts +90 -0
  103. package/src/web/client/console-pages/tools/strings.ts +49 -0
  104. package/src/web/client/console-pages/tools/view.ts +215 -0
  105. package/src/web/client/core/api.ts +200 -0
  106. package/src/web/client/core/language.ts +56 -0
  107. package/src/web/client/core/lifecycle.ts +132 -0
  108. package/src/web/client/core/router.ts +199 -0
  109. package/src/web/client/core/stream.ts +157 -0
  110. package/src/web/client/core/websocket.ts +82 -0
  111. package/src/web/client/features/appearance/index.ts +333 -0
  112. package/src/web/client/features/appearance/strings.ts +87 -0
  113. package/src/web/client/features/config/index.ts +11 -0
  114. package/src/web/client/features/config/strings.ts +41 -0
  115. package/src/web/client/features/config/view.ts +428 -0
  116. package/src/web/client/features/core/events.ts +197 -0
  117. package/src/web/client/features/core/index.ts +254 -0
  118. package/src/web/client/features/core/run.ts +56 -0
  119. package/src/web/client/features/core/runlog.ts +214 -0
  120. package/src/web/client/features/core/sessions.ts +53 -0
  121. package/src/web/client/features/core/strings.ts +157 -0
  122. package/src/web/client/features/extensions/index.ts +422 -0
  123. package/src/web/client/features/extensions/strings.ts +164 -0
  124. package/src/web/client/features/feature.ts +93 -0
  125. package/src/web/client/features/live/context.ts +280 -0
  126. package/src/web/client/features/live/fork.ts +156 -0
  127. package/src/web/client/features/live/index.ts +392 -0
  128. package/src/web/client/features/live/onboarding.ts +113 -0
  129. package/src/web/client/features/live/protocol.ts +122 -0
  130. package/src/web/client/features/live/sessions.ts +71 -0
  131. package/src/web/client/features/live/status.ts +68 -0
  132. package/src/web/client/features/live/strings.ts +252 -0
  133. package/src/web/client/features/live/timeline.ts +590 -0
  134. package/src/web/client/features/prompts/editor.ts +292 -0
  135. package/src/web/client/features/prompts/index.ts +346 -0
  136. package/src/web/client/features/prompts/strings.ts +136 -0
  137. package/src/web/client/features/prompts/view.ts +256 -0
  138. package/src/web/client/features/providers/index.ts +100 -0
  139. package/src/web/client/features/providers/strings.ts +23 -0
  140. package/src/web/client/features/settings/general.ts +28 -0
  141. package/src/web/client/features/settings/index.ts +82 -0
  142. package/src/web/client/features/settings/strings.ts +33 -0
  143. package/src/web/client/features/storage/index.ts +1 -0
  144. package/src/web/client/features/storage/strings.ts +45 -0
  145. package/src/web/client/features/storage/view.ts +154 -0
  146. package/src/web/client/features/usage/chart.ts +486 -0
  147. package/src/web/client/features/usage/index.ts +407 -0
  148. package/src/web/client/features/usage/labels.ts +46 -0
  149. package/src/web/client/features/usage/range.ts +36 -0
  150. package/src/web/client/features/usage/state.ts +37 -0
  151. package/src/web/client/features/usage/strings.ts +191 -0
  152. package/src/web/client/features/usage/tooltip.ts +86 -0
  153. package/src/web/client/features/usage/types.ts +96 -0
  154. package/src/web/client/features/worlds/index.ts +424 -0
  155. package/src/web/client/features/worlds/strings.ts +121 -0
  156. package/src/web/client/main.ts +273 -0
  157. package/src/web/client/shell/avatar.ts +233 -0
  158. package/src/web/client/shell/index.ts +549 -0
  159. package/src/web/client/shell/strings.ts +89 -0
  160. package/src/web/client/theme/handoff.ts +43 -0
  161. package/src/web/client/theme/palette.ts +109 -0
  162. package/src/web/client/theme/registry.ts +130 -0
  163. package/src/web/client/theme/storage.ts +72 -0
  164. package/src/web/client/theme/strings.ts +123 -0
  165. package/src/web/client/theme/studio.ts +366 -0
  166. package/src/web/client/ui/actions.ts +109 -0
  167. package/src/web/client/ui/data.ts +185 -0
  168. package/src/web/client/ui/dom.ts +23 -0
  169. package/src/web/client/ui/fields.ts +191 -0
  170. package/src/web/client/ui/format.ts +75 -0
  171. package/src/web/client/ui/icons.ts +183 -0
  172. package/src/web/client/ui/images.ts +91 -0
  173. package/src/web/client/ui/index.ts +89 -0
  174. package/src/web/client/ui/lamp.ts +125 -0
  175. package/src/web/client/ui/log.ts +98 -0
  176. package/src/web/client/ui/overlay.ts +184 -0
  177. package/src/web/client/ui/page.ts +8 -0
  178. package/src/web/client/ui/prompt-input.tsx +311 -0
  179. package/src/web/client/ui/sheet.ts +131 -0
  180. package/src/web/client/ui/strings.ts +71 -0
  181. package/src/web/console-pages.ts +367 -0
  182. package/src/web/files.ts +107 -0
  183. package/src/web/path-picker.ts +296 -0
  184. package/src/web/public/index.html +17 -0
  185. package/src/web/public/styles.css +1715 -0
  186. package/src/web/server.ts +1883 -0
  187. package/src/web/shared/client-panel.ts +603 -0
  188. package/src/web/shared/console-protocol.ts +560 -0
  189. package/src/web/shared/css.d.ts +1 -0
  190. package/src/web/shared/path-picker.ts +23 -0
  191. package/src/web/shared/theme.ts +256 -0
  192. package/src/web/theme-store.ts +37 -0
  193. package/src/world.ts +449 -0
  194. package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
  195. package/src/worlds/bilibili/README.md +176 -0
  196. package/src/worlds/bilibili/audience-admission.ts +995 -0
  197. package/src/worlds/bilibili/client.ts +528 -0
  198. package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
  199. package/src/worlds/bilibili/config.ts +185 -0
  200. package/src/worlds/bilibili/console/client.ts +202 -0
  201. package/src/worlds/bilibili/definition.ts +35 -0
  202. package/src/worlds/bilibili/gift-frame.ts +131 -0
  203. package/src/worlds/bilibili/normalize.ts +452 -0
  204. package/src/worlds/bilibili/overlay/announcement.ts +76 -0
  205. package/src/worlds/bilibili/overlay/assets.ts +82 -0
  206. package/src/worlds/bilibili/overlay/model.ts +494 -0
  207. package/src/worlds/bilibili/overlay/project.ts +202 -0
  208. package/src/worlds/bilibili/overlay/server.ts +286 -0
  209. package/src/worlds/bilibili/overlay/types.ts +217 -0
  210. package/src/worlds/bilibili/overlay/web/app.js +690 -0
  211. package/src/worlds/bilibili/overlay/web/editor.css +297 -0
  212. package/src/worlds/bilibili/overlay/web/editor.html +102 -0
  213. package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
  214. package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
  215. package/src/worlds/bilibili/overlay/web/styles.css +258 -0
  216. package/src/worlds/bilibili/protobuf.ts +123 -0
  217. package/src/worlds/bilibili/wire.ts +94 -0
  218. package/src/worlds/bilibili/world.ts +1389 -0
  219. package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
  220. package/src/worlds/console-fixture/console/client.ts +72 -0
  221. package/src/worlds/console-fixture/world.ts +73 -0
  222. package/src/worlds/index.ts +15 -0
  223. package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
  224. package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
  225. package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
  226. package/src/worlds/minecraft/README.md +917 -0
  227. package/src/worlds/minecraft/blueprint-plan.ts +915 -0
  228. package/src/worlds/minecraft/blueprint-registry.ts +566 -0
  229. package/src/worlds/minecraft/blueprint-repair.ts +236 -0
  230. package/src/worlds/minecraft/blueprint-resource.ts +272 -0
  231. package/src/worlds/minecraft/blueprint.ts +492 -0
  232. package/src/worlds/minecraft/body-lease.ts +340 -0
  233. package/src/worlds/minecraft/bridge.ts +749 -0
  234. package/src/worlds/minecraft/check.ts +651 -0
  235. package/src/worlds/minecraft/chests.ts +292 -0
  236. package/src/worlds/minecraft/client-launch.ts +272 -0
  237. package/src/worlds/minecraft/client-options.ts +85 -0
  238. package/src/worlds/minecraft/client-skins.ts +164 -0
  239. package/src/worlds/minecraft/client-window.ps1 +91 -0
  240. package/src/worlds/minecraft/client.ts +423 -0
  241. package/src/worlds/minecraft/combat-context.ts +109 -0
  242. package/src/worlds/minecraft/combat.ts +1552 -0
  243. package/src/worlds/minecraft/config.ts +499 -0
  244. package/src/worlds/minecraft/console/access.ts +218 -0
  245. package/src/worlds/minecraft/console/client.ts +244 -0
  246. package/src/worlds/minecraft/console/log.ts +110 -0
  247. package/src/worlds/minecraft/console/mount.ts +420 -0
  248. package/src/worlds/minecraft/console/skin.ts +257 -0
  249. package/src/worlds/minecraft/console/style.css +154 -0
  250. package/src/worlds/minecraft/console/world.ts +449 -0
  251. package/src/worlds/minecraft/deaths.ts +135 -0
  252. package/src/worlds/minecraft/definition.ts +18 -0
  253. package/src/worlds/minecraft/engine-child.ts +274 -0
  254. package/src/worlds/minecraft/engine-ipc.ts +98 -0
  255. package/src/worlds/minecraft/entity-facts.ts +132 -0
  256. package/src/worlds/minecraft/escape.ts +407 -0
  257. package/src/worlds/minecraft/executor.ts +14494 -0
  258. package/src/worlds/minecraft/explored.ts +156 -0
  259. package/src/worlds/minecraft/geometry.ts +205 -0
  260. package/src/worlds/minecraft/goal-plan.ts +647 -0
  261. package/src/worlds/minecraft/item-break.ts +126 -0
  262. package/src/worlds/minecraft/item-facts.ts +146 -0
  263. package/src/worlds/minecraft/item-pick.ts +67 -0
  264. package/src/worlds/minecraft/level-dat.ts +176 -0
  265. package/src/worlds/minecraft/log.ts +132 -0
  266. package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
  267. package/src/worlds/minecraft/names.ts +377 -0
  268. package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
  269. package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
  270. package/src/worlds/minecraft/piglin.ts +49 -0
  271. package/src/worlds/minecraft/policy.ts +547 -0
  272. package/src/worlds/minecraft/precheck.ts +655 -0
  273. package/src/worlds/minecraft/proxy.ts +445 -0
  274. package/src/worlds/minecraft/ranged.ts +635 -0
  275. package/src/worlds/minecraft/readouts.ts +102 -0
  276. package/src/worlds/minecraft/round.ts +26 -0
  277. package/src/worlds/minecraft/search-observation.ts +92 -0
  278. package/src/worlds/minecraft/server-config.ts +467 -0
  279. package/src/worlds/minecraft/server.ts +658 -0
  280. package/src/worlds/minecraft/show.ts +84 -0
  281. package/src/worlds/minecraft/skills.ts +1763 -0
  282. package/src/worlds/minecraft/terrain.ts +1735 -0
  283. package/src/worlds/minecraft/window.ts +96 -0
  284. package/src/worlds/minecraft/works.ts +179 -0
  285. package/src/worlds/minecraft/world.ts +6438 -0
  286. package/src/worlds/qq/ENV_PROMPT.md +5 -0
  287. package/src/worlds/qq/config.ts +108 -0
  288. package/src/worlds/qq/console/client.ts +126 -0
  289. package/src/worlds/qq/console/events.ts +155 -0
  290. package/src/worlds/qq/console/gate.ts +232 -0
  291. package/src/worlds/qq/console/roster.ts +203 -0
  292. package/src/worlds/qq/console/style.css +65 -0
  293. package/src/worlds/qq/conversation.ts +28 -0
  294. package/src/worlds/qq/definition.ts +62 -0
  295. package/src/worlds/qq/driver.ts +375 -0
  296. package/src/worlds/qq/history-tools.ts +209 -0
  297. package/src/worlds/qq/normalize.ts +323 -0
  298. package/src/worlds/qq/qface-map.ts +282 -0
  299. package/src/worlds/qq/vision-prompt.ts +27 -0
  300. package/src/worlds/qq/vision.ts +642 -0
  301. package/src/worlds/qq/vlm.ts +142 -0
  302. package/src/worlds/qq/world.ts +1503 -0
  303. package/src/worlds/terminal/ENV_PROMPT.md +11 -0
  304. package/src/worlds/terminal/config.ts +9 -0
  305. package/src/worlds/terminal/definition.ts +14 -0
  306. package/src/worlds/terminal/world.ts +700 -0
  307. package/src/worlds/websearch/ENV_PROMPT.md +1 -0
  308. package/src/worlds/websearch/brave-client.ts +261 -0
  309. package/src/worlds/websearch/config.ts +75 -0
  310. package/src/worlds/websearch/definition.ts +20 -0
  311. package/src/worlds/websearch/world.ts +171 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Per-endpoint controller of the managed side: which build the endpoint wants, whether it is
3
+ * installed, and the one llama-server process that serves the endpoint.
4
+ *
5
+ * Launch configuration is latched on start: editing it while the server runs marks the state
6
+ * `configurationPending`, and the next start applies it.
7
+ */
8
+ import { join } from 'node:path';
9
+ import type { LLMProviderEntry, Logger } from '../../core/types.ts';
10
+ import type { Language } from '../../core/language.ts';
11
+ import { backendChoices, llamacppOptions, releasePlan, type LaunchOptions, type ReleasePlan } from './options.ts';
12
+ import { RuntimeStore, type InstallState } from './runtime-store.ts';
13
+ import { LlamaServerManager, serverExecutable, smartAppControlState, type ServerState } from './server.ts';
14
+ import { text } from './strings.ts';
15
+
16
+ export interface RuntimeRoots {
17
+ runtimes: string;
18
+ models: string;
19
+ }
20
+
21
+ export interface LlamaRuntimeOptions {
22
+ name: string;
23
+ entry: () => LLMProviderEntry;
24
+ secret: (name: string) => string;
25
+ log: Logger;
26
+ roots: RuntimeRoots;
27
+ store?: RuntimeStore;
28
+ fetchImpl?: typeof fetch;
29
+ commandOverride?: { command: string; args: string[] };
30
+ platform?: NodeJS.Platform;
31
+ arch?: string;
32
+ }
33
+
34
+ export interface RuntimeState {
35
+ managed: boolean;
36
+ release: string | null;
37
+ backend: string | null;
38
+ backendChoices: string[];
39
+ /** Directory the server runs from: the install, or the operator's own runtime directory. */
40
+ runtimeDir: string | null;
41
+ own: boolean;
42
+ /** False when the upstream ships no build for this backend on this platform. */
43
+ supported: boolean;
44
+ install: InstallState;
45
+ server: (ServerState & { configurationPending: boolean }) | null;
46
+ launch: LaunchOptions | null;
47
+ autoStart: boolean;
48
+ /** Windows Smart App Control: 0 off, 1 enforced, 2 evaluation; null elsewhere. */
49
+ smartAppControl: 0 | 1 | 2 | null;
50
+ cacheDir: string;
51
+ localModelsDir: string;
52
+ }
53
+
54
+ interface Target {
55
+ release: string;
56
+ backend: string;
57
+ own: boolean;
58
+ plan: ReleasePlan | null;
59
+ dir: string | null;
60
+ serverExe: string;
61
+ }
62
+
63
+ export class LlamaRuntime {
64
+ private readonly store: RuntimeStore;
65
+ private current: { key: string; manager: LlamaServerManager } | null = null;
66
+ private transition: Promise<unknown> = Promise.resolve();
67
+ private sac: Promise<0 | 1 | 2 | null> | null = null;
68
+ private readonly platform: NodeJS.Platform;
69
+ private readonly arch: string;
70
+
71
+ constructor(private readonly opts: LlamaRuntimeOptions) {
72
+ this.platform = opts.platform ?? process.platform;
73
+ this.arch = opts.arch ?? process.arch;
74
+ this.store = opts.store ?? new RuntimeStore(opts.roots.runtimes, opts.log, opts.fetchImpl);
75
+ }
76
+
77
+ managed(): boolean {
78
+ return Boolean(llamacppOptions(this.opts.entry()).runtime);
79
+ }
80
+
81
+ modelDirs(): { cacheDir: string; localModelsDir: string } {
82
+ const base = join(this.opts.roots.models, 'llamacpp');
83
+ return { cacheDir: join(base, 'cache'), localModelsDir: join(base, 'local') };
84
+ }
85
+
86
+ private target(): Target | null {
87
+ const runtime = llamacppOptions(this.opts.entry()).runtime;
88
+ if (!runtime) return null;
89
+ const own = Boolean(runtime.runtimeDir);
90
+ const plan = own ? null : releasePlan(runtime.release, runtime.backend, this.platform, this.arch);
91
+ return {
92
+ release: runtime.release,
93
+ backend: runtime.backend,
94
+ own,
95
+ plan,
96
+ dir: own ? runtime.runtimeDir! : plan ? this.store.dir(runtime.release, plan) : null,
97
+ serverExe: plan?.serverExe ?? serverExecutable(this.platform),
98
+ };
99
+ }
100
+
101
+ installState(language: Language = 'zh'): InstallState {
102
+ const target = this.target();
103
+ if (!target?.dir) return { phase: 'absent', file: null, done: 0, total: null, detail: null };
104
+ if (target.own) return { phase: 'installed', file: null, done: 0, total: null, detail: null };
105
+ return this.store.state(target.dir, language);
106
+ }
107
+
108
+ async install(language: Language = 'zh'): Promise<void> {
109
+ const target = this.target();
110
+ if (!target) throw new Error(text(language).notManaged);
111
+ if (target.own) return;
112
+ if (!target.plan) throw new Error(text(language).runtimeUnsupported);
113
+ await this.store.install(target.release, target.plan, language);
114
+ }
115
+
116
+ async state(language: Language = 'zh'): Promise<RuntimeState> {
117
+ const options = llamacppOptions(this.opts.entry());
118
+ const target = this.target();
119
+ const dirs = this.modelDirs();
120
+ const base = {
121
+ backendChoices: backendChoices(this.platform, this.arch),
122
+ autoStart: options.autoStart === true,
123
+ smartAppControl: await this.smartAppControl(),
124
+ ...dirs,
125
+ };
126
+ if (!target) {
127
+ return {
128
+ ...base,
129
+ managed: false,
130
+ release: null,
131
+ backend: null,
132
+ runtimeDir: null,
133
+ own: false,
134
+ supported: true,
135
+ install: this.installState(language),
136
+ server: null,
137
+ launch: null,
138
+ };
139
+ }
140
+ const launch = this.launch(language);
141
+ const manager = this.current?.manager ?? launch?.manager();
142
+ return {
143
+ ...base,
144
+ managed: true,
145
+ release: target.release,
146
+ backend: target.backend,
147
+ runtimeDir: target.dir,
148
+ own: target.own,
149
+ supported: target.own || target.plan !== null,
150
+ install: this.installState(language),
151
+ server: manager
152
+ ? { ...(await manager.state(language)), configurationPending: this.current !== null && this.current.key !== launch?.key }
153
+ : null,
154
+ launch: options.launch ?? null,
155
+ };
156
+ }
157
+
158
+ private smartAppControl(): Promise<0 | 1 | 2 | null> {
159
+ if (!this.sac) this.sac = smartAppControlState(this.platform);
160
+ return this.sac;
161
+ }
162
+
163
+ private launch(language: Language): { key: string; manager: () => LlamaServerManager } | null {
164
+ const entry = this.opts.entry();
165
+ const options = llamacppOptions(entry);
166
+ const target = this.target();
167
+ if (!target?.dir || !options.launch) return null;
168
+ const launch = options.launch;
169
+ const dirs = this.modelDirs();
170
+ const apiKey = entry.secret ? this.opts.secret(entry.secret) : undefined;
171
+ return {
172
+ key: JSON.stringify([entry.baseUrl, target.dir, launch, entry.secret ?? null]),
173
+ manager: () =>
174
+ new LlamaServerManager({
175
+ name: this.opts.name,
176
+ baseUrl: entry.baseUrl,
177
+ runtimeDir: target.dir!,
178
+ serverExe: target.serverExe,
179
+ ...dirs,
180
+ apiKey,
181
+ launch,
182
+ log: this.opts.log,
183
+ commandOverride: this.opts.commandOverride,
184
+ fetchImpl: this.opts.fetchImpl,
185
+ }),
186
+ };
187
+ }
188
+
189
+ private schedule<T>(operation: () => Promise<T>): Promise<T> {
190
+ const next = this.transition.then(operation, operation);
191
+ this.transition = next;
192
+ return next;
193
+ }
194
+
195
+ start(language: Language = 'zh'): Promise<ServerState | undefined> {
196
+ return this.schedule(async () => {
197
+ const S = text(language);
198
+ const target = this.target();
199
+ if (!target) throw new Error(S.notManaged);
200
+ if (!target.own && this.installState(language).phase !== 'installed') throw new Error(S.runtimeNotInstalled);
201
+ const launch = this.launch(language);
202
+ if (!launch) throw new Error(S.notManaged);
203
+ if (this.current?.key !== launch.key) {
204
+ await this.current?.manager.stop();
205
+ this.current = { key: launch.key, manager: launch.manager() };
206
+ }
207
+ return this.current.manager.start(language);
208
+ });
209
+ }
210
+
211
+ stop(language: Language = 'zh'): Promise<ServerState | undefined> {
212
+ return this.schedule(async () => this.current?.manager.stop(language));
213
+ }
214
+ }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * One llama-server process in router mode: no model on the command line, models come from
3
+ * `LLAMA_CACHE` and `--models-dir`, and the router loads them on first request.
4
+ *
5
+ * A server someone else started at the same endpoint is left alone: `start()` probes first and
6
+ * only spawns when nothing answers.
7
+ */
8
+ import { execFile, spawn, type ChildProcess } from 'node:child_process';
9
+ import { existsSync, mkdirSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import type { Logger } from '../../core/types.ts';
12
+ import type { Language } from '../../core/language.ts';
13
+ import type { LaunchOptions } from './options.ts';
14
+ import { text, type Text } from './strings.ts';
15
+
16
+ export type ServerPhase = 'stopped' | 'starting' | 'running' | 'error';
17
+
18
+ export interface ServerState {
19
+ phase: ServerPhase;
20
+ baseUrl: string;
21
+ detail: string | null;
22
+ pid: number | null;
23
+ /** Whether the endpoint answers `/health` now, whoever started it. */
24
+ reachable: boolean;
25
+ }
26
+
27
+ export interface LlamaServerOptions {
28
+ name: string;
29
+ baseUrl: string;
30
+ /** Directory holding the server executable and its shared libraries. */
31
+ runtimeDir: string;
32
+ serverExe: string;
33
+ /** `LLAMA_CACHE` for the process: where `-hf` style pulls land. */
34
+ cacheDir: string;
35
+ /** `--models-dir`: GGUF files the operator put there by hand. */
36
+ localModelsDir: string;
37
+ apiKey?: string;
38
+ launch: LaunchOptions;
39
+ log: Logger;
40
+ /** Test injection: replaces the spawned command. */
41
+ commandOverride?: { command: string; args: string[] };
42
+ healthIntervalMs?: number;
43
+ healthTimeoutMs?: number;
44
+ fetchImpl?: typeof fetch;
45
+ }
46
+
47
+ type Detail = (S: Text) => string;
48
+
49
+ export function serverExecutable(platform: NodeJS.Platform = process.platform): string {
50
+ return platform === 'win32' ? 'llama-server.exe' : 'llama-server';
51
+ }
52
+
53
+ /** baseUrl → host / port / origin; null when it names no port. */
54
+ export function parseEndpoint(baseUrl: string): { host: string; port: number; origin: string } | null {
55
+ try {
56
+ const url = new URL(baseUrl);
57
+ const port = Number(url.port);
58
+ if (!Number.isFinite(port) || port <= 0) return null;
59
+ return { host: url.hostname, port, origin: url.origin };
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /** Command line of the router; exported so the launch contract is testable without a spawn. */
66
+ export function launchArgs(options: Pick<LlamaServerOptions, 'launch' | 'localModelsDir'>, host: string, port: number): string[] {
67
+ const { launch } = options;
68
+ return [
69
+ '--host', host,
70
+ '--port', String(port),
71
+ '--models-dir', options.localModelsDir,
72
+ '-c', String(launch.contextSize),
73
+ '-ngl', String(launch.nGpuLayers),
74
+ '--parallel', String(launch.parallel),
75
+ '--jinja',
76
+ '--reasoning-format', 'deepseek',
77
+ ...launch.extraArgs.split(/\s+/).filter((arg) => arg.length > 0),
78
+ ];
79
+ }
80
+
81
+ /** Library lookup for the spawned process: the archives put everything next to the executable. */
82
+ export function launchEnv(runtimeDir: string, cacheDir: string, apiKey: string | undefined, base: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): NodeJS.ProcessEnv {
83
+ const env: NodeJS.ProcessEnv = { ...base, LLAMA_CACHE: cacheDir };
84
+ if (apiKey) env.LLAMA_API_KEY = apiKey;
85
+ if (platform === 'win32') env.PATH = `${runtimeDir};${base.PATH ?? ''}`;
86
+ else if (platform === 'darwin') env.DYLD_LIBRARY_PATH = [runtimeDir, base.DYLD_LIBRARY_PATH].filter(Boolean).join(':');
87
+ else env.LD_LIBRARY_PATH = [runtimeDir, base.LD_LIBRARY_PATH].filter(Boolean).join(':');
88
+ return env;
89
+ }
90
+
91
+ /**
92
+ * Windows UNKNOWN errors include the executable path and a conditional application-control check.
93
+ * The error code alone does not identify an application-control block.
94
+ */
95
+ function spawnFailDetail(error: unknown, exe: string): Detail {
96
+ const e = error as NodeJS.ErrnoException;
97
+ if (process.platform === 'win32' && e?.code === 'UNKNOWN') return (S) => S.spawnBlocked(exe);
98
+ return (S) => S.spawnFailed(e?.message ?? String(error));
99
+ }
100
+
101
+ /** Smart App Control: 0 off, 1 enforced, 2 evaluation; null where the value cannot be read. */
102
+ export async function smartAppControlState(platform: NodeJS.Platform = process.platform): Promise<0 | 1 | 2 | null> {
103
+ if (platform !== 'win32') return null;
104
+ const output = await new Promise<string>((resolve) => {
105
+ execFile(
106
+ 'reg',
107
+ ['query', 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\CI\\Policy', '/v', 'VerifiedAndReputablePolicyState'],
108
+ { windowsHide: true },
109
+ (error, stdout) => resolve(error ? '' : String(stdout)),
110
+ );
111
+ });
112
+ return parseRegDword(output);
113
+ }
114
+
115
+ export function parseRegDword(output: string): 0 | 1 | 2 | null {
116
+ const match = /REG_DWORD\s+0x([0-9a-f]+)/i.exec(output);
117
+ if (!match) return null;
118
+ const value = Number.parseInt(match[1], 16);
119
+ return value === 0 || value === 1 || value === 2 ? value : null;
120
+ }
121
+
122
+ export class LlamaServerManager {
123
+ private phase: ServerPhase = 'stopped';
124
+ private detail: Detail | null = null;
125
+ private proc: ChildProcess | null = null;
126
+ private healthTimer: ReturnType<typeof setInterval> | null = null;
127
+ private stderrTail = '';
128
+ private readonly fetchImpl: typeof fetch;
129
+
130
+ constructor(private readonly opts: LlamaServerOptions) {
131
+ this.fetchImpl = opts.fetchImpl ?? fetch;
132
+ }
133
+
134
+ async state(language: Language = 'zh'): Promise<ServerState> {
135
+ return {
136
+ phase: this.phase,
137
+ baseUrl: this.opts.baseUrl,
138
+ detail: this.detail ? this.detail(text(language)) : null,
139
+ pid: this.proc?.pid ?? null,
140
+ reachable: await this.probe(),
141
+ };
142
+ }
143
+
144
+ async start(language: Language = 'zh'): Promise<ServerState> {
145
+ if (this.phase === 'starting' || this.phase === 'running') return this.state(language);
146
+ if (await this.probe()) {
147
+ this.detail = (S) => S.externalServer;
148
+ return this.state(language);
149
+ }
150
+ const launch = this.resolveLaunch();
151
+ if ('error' in launch) {
152
+ this.phase = 'error';
153
+ this.detail = launch.error;
154
+ return this.state(language);
155
+ }
156
+ mkdirSync(this.opts.cacheDir, { recursive: true });
157
+ mkdirSync(this.opts.localModelsDir, { recursive: true });
158
+ this.stderrTail = '';
159
+ let proc: ChildProcess;
160
+ try {
161
+ proc = spawn(launch.command, launch.args, {
162
+ cwd: launch.cwd,
163
+ env: launch.env,
164
+ stdio: ['ignore', 'ignore', 'pipe'],
165
+ windowsHide: true,
166
+ });
167
+ } catch (error) {
168
+ this.fail(spawnFailDetail(error, launch.command));
169
+ return this.state(language);
170
+ }
171
+ this.proc = proc;
172
+ this.phase = 'starting';
173
+ this.detail = (S) => S.starting;
174
+ proc.stderr?.on('data', (chunk: Buffer) => {
175
+ this.stderrTail = (this.stderrTail + chunk.toString()).slice(-2000);
176
+ });
177
+ proc.on('error', (error) => {
178
+ if (this.proc !== proc) return;
179
+ this.proc = null;
180
+ this.fail(spawnFailDetail(error, launch.command));
181
+ });
182
+ proc.on('exit', (code) => {
183
+ if (this.proc !== proc) return;
184
+ this.proc = null;
185
+ if (this.phase === 'stopped') return;
186
+ const tail = this.stderrTail.slice(-400);
187
+ this.fail((S) => S.exited(code, tail));
188
+ });
189
+ this.opts.log.info(`llama-server 启动中 endpoint=${this.opts.name} pid=${proc.pid} ${this.opts.baseUrl}`);
190
+ this.beginHealthPolling();
191
+ return this.state(language);
192
+ }
193
+
194
+ async stop(language: Language = 'zh'): Promise<ServerState> {
195
+ this.clearHealthTimer();
196
+ this.phase = 'stopped';
197
+ this.detail = null;
198
+ const proc = this.proc;
199
+ this.proc = null;
200
+ if (proc && proc.exitCode === null) {
201
+ proc.kill();
202
+ await new Promise<void>((resolve) => {
203
+ const force = setTimeout(() => {
204
+ try {
205
+ proc.kill('SIGKILL');
206
+ } catch {
207
+ /* already gone */
208
+ }
209
+ resolve();
210
+ }, 3_000);
211
+ proc.once('exit', () => {
212
+ clearTimeout(force);
213
+ resolve();
214
+ });
215
+ });
216
+ this.opts.log.info(`llama-server 已停止 endpoint=${this.opts.name}`);
217
+ }
218
+ return this.state(language);
219
+ }
220
+
221
+ async probe(): Promise<boolean> {
222
+ const endpoint = parseEndpoint(this.opts.baseUrl);
223
+ if (!endpoint) return false;
224
+ try {
225
+ const res = await this.fetchImpl(`${endpoint.origin}/health`, { signal: AbortSignal.timeout(2000) });
226
+ return res.ok;
227
+ } catch {
228
+ return false;
229
+ }
230
+ }
231
+
232
+ private resolveLaunch():
233
+ | { command: string; args: string[]; cwd: string | undefined; env: NodeJS.ProcessEnv }
234
+ | { error: Detail } {
235
+ const endpoint = parseEndpoint(this.opts.baseUrl);
236
+ if (!endpoint) return { error: (S) => S.badBaseUrl(this.opts.baseUrl) };
237
+ const env = launchEnv(this.opts.runtimeDir, this.opts.cacheDir, this.opts.apiKey);
238
+ if (this.opts.commandOverride) {
239
+ return { command: this.opts.commandOverride.command, args: this.opts.commandOverride.args, cwd: undefined, env };
240
+ }
241
+ const exe = join(this.opts.runtimeDir, this.opts.serverExe);
242
+ if (!existsSync(exe)) return { error: (S) => S.missingBinary(exe) };
243
+ return { command: exe, args: launchArgs(this.opts, endpoint.host, endpoint.port), cwd: this.opts.runtimeDir, env };
244
+ }
245
+
246
+ private beginHealthPolling(): void {
247
+ this.clearHealthTimer();
248
+ const interval = this.opts.healthIntervalMs ?? 1_000;
249
+ const deadline = Date.now() + (this.opts.healthTimeoutMs ?? 120_000);
250
+ this.healthTimer = setInterval(async () => {
251
+ if (this.phase !== 'starting') {
252
+ this.clearHealthTimer();
253
+ return;
254
+ }
255
+ if (await this.probe()) {
256
+ this.phase = 'running';
257
+ this.detail = null;
258
+ this.clearHealthTimer();
259
+ this.opts.log.info(`llama-server 就绪 ${this.opts.baseUrl}`);
260
+ return;
261
+ }
262
+ if (Date.now() > deadline) {
263
+ const orphan = this.proc;
264
+ this.proc = null;
265
+ this.fail((S) => S.healthTimeout);
266
+ try {
267
+ orphan?.kill();
268
+ } catch {
269
+ /* already gone */
270
+ }
271
+ }
272
+ }, interval);
273
+ }
274
+
275
+ private clearHealthTimer(): void {
276
+ if (this.healthTimer) clearInterval(this.healthTimer);
277
+ this.healthTimer = null;
278
+ }
279
+
280
+ private fail(detail: Detail): void {
281
+ this.clearHealthTimer();
282
+ this.phase = 'error';
283
+ this.detail = detail;
284
+ this.opts.log.warn(`llama-server 异常: ${detail(text('zh'))}`);
285
+ }
286
+ }