nx_llm_route.nx source
↩ module page · 156 lines · 7393 B
1// nx_llm_route.nx -- MIXED-LLM ROUTING (companionchat CC6, 2026-08-30): a DATA ROW can send a chat turn to a stronger
2// backend FIRST; the local encoder-LLM stays the fallback; and the reply records which one answered (model_route), so a
3// route is never silent and an absent backend is never a mystery. Operator ask: "the companion intelligent, use your mixed llm".
4// CONF llm_routes.conf, rows `tag|trigger|a.b.c.d|port`: the FIRST row whose trigger appears (case-insensitive) in the user
5// turn selects the backend; trigger `*` matches every turn (a default route). Comments `#` and blank lines ignored. The conf
6// is read PER TURN (data, no rebuild) from the daemon cwd, then from the nishihost knowledge tree. No conf, no match, or a
7// malformed row = NO ROUTE, never a guess. The backend must speak the same OpenAI-shape /v1/chat/completions the local
8// worker speaks; nothing here re-implements dispatch -- the orchestrator prepends the routed candidate to its own list.
9// Contract symbol: go_llm_route. Gate: nx_llm_route_gate (fixture confs under /tmp, neg-controls for no-match and no-conf).
10// license_tier: ORIGINAL
11import "nx_syscalls.nx"
12
13const LLR_CONF: *u8 = "llm_routes.conf" as *u8
14const LLR_CONF_ABS: *u8 = "/volume1/homes/elderwesto/nishihost/knowledge/llm_routes.conf" as *u8
15// the orchestrator's candidate arrays hold eight workers; prepending never pushes past that
16const LLR_MAXW: i64 = 8
17const LLR_TAG_CAP: i64 = 48
18const LLR_CH_PIPE: i64 = 124
19const LLR_CH_NL: i64 = 10
20const LLR_CH_CR: i64 = 13
21const LLR_CH_HASH: i64 = 35
22const LLR_CH_STAR: i64 = 42
23const LLR_CH_DOT: i64 = 46
24const LLR_CH_D0: i64 = 48
25const LLR_CH_D9: i64 = 57
26const LLR_PORT_MAX: i64 = 65535
27
28func llr_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
29func llr_cat(dst: *u8, off: i64, s: *u8) -> i64 { var o: i64 = off; var i: i64 = 0; while s[i] != (0 as u8) { dst[o] = s[i]; o = o + 1; i = i + 1 } return o }
30func llr_lower(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c }
31
32// case-insensitive substring: needle is a bounded slice of the conf buffer, hay is the user turn
33func llr_has_ci(hay: *u8, hn: i64, nd: *u8, nn: i64) -> i64 {
34 if nn <= 0 { return 0 }
35 var i: i64 = 0
36 while i + nn <= hn {
37 var j: i64 = 0
38 var same: i64 = 1
39 var scan: i64 = 1
40 while scan == 1 {
41 if j >= nn { scan = 0 } else {
42 if llr_lower(hay[i + j] as i64) != llr_lower(nd[j] as i64) { same = 0; scan = 0 } else { j = j + 1 }
43 }
44 }
45 if same == 1 { return 1 }
46 i = i + 1
47 }
48 return 0
49}
50
51// parse `a.b.c.d` then `|port` starting at s[i0..n): fills w[0..4], returns 1 when every field is a sane number
52func llr_parse_target(s: *u8, i0: i64, n: i64, w: *i64) -> i64 {
53 var i: i64 = i0
54 var f: i64 = 0
55 while f < 5 {
56 var v: i64 = 0
57 var digits: i64 = 0
58 var scan: i64 = 1
59 while scan == 1 {
60 if i >= n { scan = 0 } else {
61 let c: i64 = s[i] as i64
62 if c < LLR_CH_D0 { scan = 0 } else { if c > LLR_CH_D9 { scan = 0 } else { v = v * 10 + (c - LLR_CH_D0); digits = digits + 1; i = i + 1 } }
63 }
64 }
65 if digits == 0 { return 0 }
66 if f < 4 { if v > 255 { return 0 } } else { if v <= 0 { return 0 } if v > LLR_PORT_MAX { return 0 } }
67 w[f] = v
68 f = f + 1
69 if f < 5 {
70 var want: i64 = LLR_CH_DOT
71 if f == 4 { want = LLR_CH_PIPE }
72 if i >= n { return 0 }
73 if (s[i] as i64) != want { return 0 }
74 i = i + 1
75 }
76 }
77 return 1
78}
79
80// THE CORE, conf path explicit so a gate can drive it with fixtures. Returns 1 and fills out_tag/out_w on the first match.
81func go_llm_route_from(path: *u8, umsg: *u8, umlen: i64, out_tag: *u8, out_w: *i64) -> i64 {
82 out_tag[0] = 0 as u8
83 if umlen <= 0 { return 0 }
84 let lp: *i64 = sys_mmap(16) as *i64
85 lp[0] = 0
86 let b: *u8 = sys_read_file(path, lp)
87 if (b as i64) == 0 { return 0 }
88 let n: i64 = lp[0]
89 var i: i64 = 0
90 while i < n {
91 let ls: i64 = i
92 var eol: i64 = i
93 var scan: i64 = 1
94 while scan == 1 { if eol >= n { scan = 0 } else { if b[eol] == (LLR_CH_NL as u8) { scan = 0 } else { eol = eol + 1 } } }
95 var e2: i64 = eol
96 if e2 > ls { if b[e2 - 1] == (LLR_CH_CR as u8) { e2 = e2 - 1 } }
97 var usable: i64 = 1
98 if e2 == ls { usable = 0 } else { if b[ls] == (LLR_CH_HASH as u8) { usable = 0 } }
99 if usable == 1 {
100 // tag | trigger | a.b.c.d | port
101 var p1: i64 = ls
102 var s1: i64 = 1
103 while s1 == 1 { if p1 >= e2 { s1 = 0 } else { if b[p1] == (LLR_CH_PIPE as u8) { s1 = 0 } else { p1 = p1 + 1 } } }
104 if p1 < e2 {
105 var p2: i64 = p1 + 1
106 var s2: i64 = 1
107 while s2 == 1 { if p2 >= e2 { s2 = 0 } else { if b[p2] == (LLR_CH_PIPE as u8) { s2 = 0 } else { p2 = p2 + 1 } } }
108 if p2 < e2 {
109 let tlen: i64 = p2 - (p1 + 1)
110 var hit: i64 = 0
111 if tlen == 1 { if b[p1 + 1] == (LLR_CH_STAR as u8) { hit = 1 } }
112 if hit == 0 { hit = llr_has_ci(umsg, umlen, ((b as i64) + p1 + 1) as *u8, tlen) }
113 if hit == 1 {
114 let w: *i64 = sys_mmap(8 * 8) as *i64
115 if llr_parse_target(b, p2 + 1, e2, w) == 1 {
116 var k: i64 = 0
117 while k < 5 { out_w[k] = w[k]; k = k + 1 }
118 var t: i64 = 0
119 while t < (p1 - ls) { if t < LLR_TAG_CAP - 1 { out_tag[t] = b[ls + t] } t = t + 1 }
120 if t > LLR_TAG_CAP - 1 { t = LLR_TAG_CAP - 1 }
121 out_tag[t] = 0 as u8
122 return 1
123 }
124 }
125 }
126 }
127 }
128 i = eol + 1
129 }
130 return 0
131}
132
133// the daemon-facing wrapper: cwd conf first (the daemon's own data dir), then the nishihost knowledge tree
134func go_llm_route(umsg: *u8, umlen: i64, out_tag: *u8, out_w: *i64) -> i64 {
135 if go_llm_route_from(LLR_CONF, umsg, umlen, out_tag, out_w) == 1 { return 1 }
136 return go_llm_route_from(LLR_CONF_ABS, umsg, umlen, out_tag, out_w)
137}
138
139// put the routed backend FIRST in the orchestrator's candidate arrays; the existing candidates keep their order behind it
140func go_llm_route_prepend(rw: *i64, wa: *i64, wb: *i64, wc: *i64, wd: *i64, wpa: *i64, wcount: i64) -> i64 {
141 var n: i64 = wcount
142 if n > LLR_MAXW - 1 { n = LLR_MAXW - 1 }
143 var k: i64 = n
144 while k > 0 { wa[k] = wa[k - 1]; wb[k] = wb[k - 1]; wc[k] = wc[k - 1]; wd[k] = wd[k - 1]; wpa[k] = wpa[k - 1]; k = k - 1 }
145 wa[0] = rw[0]; wb[0] = rw[1]; wc[0] = rw[2]; wd[0] = rw[3]; wpa[0] = rw[4]
146 return n + 1
147}
148
149// PROVENANCE for the reply: which backend answered. rwin = index of the winning candidate, -1 = none answered.
150func go_llm_route_label(routed: i64, rwin: i64, tag: *u8, dst: *u8, off: i64) -> i64 {
151 if routed == 0 { return llr_cat(dst, off, "local" as *u8) }
152 var o: i64 = llr_cat(dst, off, tag)
153 if rwin == 0 { return o }
154 if rwin > 0 { return llr_cat(dst, o, "-fallback-local" as *u8) }
155 return llr_cat(dst, o, "-unanswered" as *u8)
156}