← Back to C-Kernel-Engine Docs Doxygen Source Documentation
 
Loading...
Searching...
No Matches
ck_cli_v6.6.c
Go to the documentation of this file.
1/*
2 * C-Kernel-Engine v6.6 Native CLI
3 *
4 * Features:
5 * - Model auto-discovery from cache
6 * - Readline support for history/editing
7 * - Chat template support (Qwen, LLaMA, etc.)
8 * - Temperature/top-p sampling
9 * - Streaming output
10 *
11 * Usage:
12 * ck-cli-v6.6 --model <name> # Auto-discover from cache
13 * ck-cli-v6.6 <libmodel.so> <weights.bump> # Direct paths
14 * ck-cli-v6.6 --lib <.so> --weights <.bump> # Named args
15 */
16
17#define _GNU_SOURCE
18#include <stdio.h>
19#include <stdlib.h>
20#include <stdint.h>
21#include <string.h>
22#include <stdbool.h>
23#include <errno.h>
24#include <signal.h>
25#include <dlfcn.h>
26#include <unistd.h>
27#include <time.h>
28#include <math.h>
29#include <dirent.h>
30#include <sys/stat.h>
31
32#ifdef HAVE_READLINE
33#include <readline/readline.h>
34#include <readline/history.h>
35#endif
36
37#include "tokenizer/true_bpe.h"
38#include "ck_features.h"
39
40#define CK_CLI_VERSION "6.6.0"
41#define CK_CLI_DEFAULT_MAX_TOKENS 256
42#define CK_CLI_EOS_MAX 8
43#define CK_CLI_OUTPUT_BUF_SIZE 4096
44#define CK_CLI_MAX_CONTEXT 32768
45#define CK_CLI_HISTORY_FILE ".ck_cli_history"
46
47static volatile sig_atomic_t g_exit_requested = 0;
48static volatile sig_atomic_t g_generation_active = 0;
49
50/* Timing globals */
51static double g_prefill_time_ms = 0.0;
52static double g_decode_time_ms = 0.0;
53static int g_decode_count = 0;
54static int g_prompt_tokens = 0;
55static bool g_use_byte_decoder = true;
56
57static void handle_sigint(int sig) {
58 (void)sig;
60 g_generation_active = 0; /* Stop generation but don't exit */
61 } else {
63 }
64}
65
66/* ============================================================================
67 * Model API Types
68 * ============================================================================ */
69
70typedef int (*init_t)(const char *weights_path);
71typedef int (*embed_t)(const int32_t *tokens, int num_tokens);
72typedef int (*forward_t)(float *logits_out);
73typedef int (*kv_enable_t)(int capacity);
74typedef void (*kv_reset_t)(void);
75typedef int (*decode_t)(int32_t token, float *logits_out);
76typedef int (*sample_argmax_t)(void);
77typedef float *(*get_logits_t)(void);
78typedef int (*get_int_t)(void);
79typedef void *(*get_ptr_t)(void);
80typedef void (*free_t)(void);
81
82typedef struct {
83 void *handle;
84 init_t init;
85 embed_t embed;
86 forward_t forward;
87 kv_enable_t kv_enable;
88 kv_reset_t kv_reset;
89 decode_t decode;
90 sample_argmax_t sample;
91 get_logits_t get_logits;
92 get_int_t get_logits_stride;
93 get_int_t get_context;
94 get_int_t get_vocab_size;
95 get_int_t get_num_merges;
96 get_int_t get_vocab_bytes;
97 get_int_t get_active_tokens;
98 get_ptr_t get_offsets;
99 get_ptr_t get_strings;
100 get_ptr_t get_merges;
101 free_t free_fn;
102} ModelAPI;
103
104/* ============================================================================
105 * Chat Template Types
106 * ============================================================================ */
107
115
116typedef struct {
117 ChatTemplateType type;
118 const char *system_prefix;
119 const char *system_suffix;
120 const char *user_prefix;
121 const char *user_suffix;
122 const char *assistant_prefix;
123 const char *assistant_suffix;
124} ChatTemplate;
125
126static const ChatTemplate g_templates[] = {
128 .type = CHAT_TEMPLATE_NONE,
129 .system_prefix = "", .system_suffix = "\n",
130 .user_prefix = "", .user_suffix = "\n",
131 .assistant_prefix = "", .assistant_suffix = "",
132 },
134 .type = CHAT_TEMPLATE_QWEN,
135 .system_prefix = "<|im_start|>system\n",
136 .system_suffix = "<|im_end|>\n",
137 .user_prefix = "<|im_start|>user\n",
138 .user_suffix = "<|im_end|>\n",
139 .assistant_prefix = "<|im_start|>assistant\n",
140 .assistant_suffix = "<|im_end|>",
141 },
143 .type = CHAT_TEMPLATE_LLAMA,
144 .system_prefix = "[INST] <<SYS>>\n",
145 .system_suffix = "\n<</SYS>>\n\n",
146 .user_prefix = "",
147 .user_suffix = " [/INST]",
148 .assistant_prefix = " ",
149 .assistant_suffix = " </s><s>[INST] ",
150 },
152 .type = CHAT_TEMPLATE_CHATML,
153 .system_prefix = "<|im_start|>system\n",
154 .system_suffix = "<|im_end|>\n",
155 .user_prefix = "<|im_start|>user\n",
156 .user_suffix = "<|im_end|>\n",
157 .assistant_prefix = "<|im_start|>assistant\n",
158 .assistant_suffix = "<|im_end|>",
159 },
161 .type = CHAT_TEMPLATE_MISTRAL,
162 .system_prefix = "",
163 .system_suffix = "\n\n",
164 .user_prefix = "[INST] ",
165 .user_suffix = " [/INST]",
166 .assistant_prefix = "",
167 .assistant_suffix = "</s> ",
168 },
169};
170
171/* ============================================================================
172 * CLI Options
173 * ============================================================================ */
174
175typedef struct {
176 const char *model_name; /* Model name for auto-discovery */
177 const char *lib_path;
178 const char *weights_path;
179 const char *prompt_once;
180 const char *system_prompt;
181 int max_tokens;
182 int context_override;
183 float temperature;
184 float top_p;
185 bool ignore_eos;
186 bool stream;
187 bool timing;
188 bool verbose;
189 bool no_chat_template;
190 ChatTemplateType chat_template;
191 int eos_ids[CK_CLI_EOS_MAX];
192 int eos_count;
193} CLIOptions;
194
195/* ============================================================================
196 * Cache Discovery
197 * ============================================================================ */
198
199static const char *get_cache_dir(void) {
200 static char cache_path[4096];
201 const char *home = getenv("HOME");
202 if (!home) home = "/tmp";
203 snprintf(cache_path, sizeof(cache_path), "%s/.cache/ck-engine-v6.6/models", home);
204 return cache_path;
205}
206
207static bool find_model_in_cache(const char *model_name, char *lib_out, char *weights_out, size_t out_size) {
208 const char *cache_dir = get_cache_dir();
209 DIR *dir = opendir(cache_dir);
210 if (!dir) return false;
211
212 struct dirent *entry;
213 while ((entry = readdir(dir)) != NULL) {
214 if (entry->d_name[0] == '.') continue;
215
216 /* Check if directory name contains model_name */
217 if (strstr(entry->d_name, model_name) != NULL) {
218 char model_dir[4096];
219 snprintf(model_dir, sizeof(model_dir), "%s/%s", cache_dir, entry->d_name);
220
221 /* Check for required files */
222 char so_path[4096], bump_path[4096];
223 snprintf(so_path, sizeof(so_path), "%s/ck-kernel-inference.so", model_dir);
224 snprintf(bump_path, sizeof(bump_path), "%s/weights.bump", model_dir);
225
226 struct stat st;
227 if (stat(so_path, &st) == 0 && stat(bump_path, &st) == 0) {
228 strncpy(lib_out, so_path, out_size - 1);
229 strncpy(weights_out, bump_path, out_size - 1);
230 closedir(dir);
231 return true;
232 }
233 }
234 }
235 closedir(dir);
236 return false;
237}
238
239/* ============================================================================
240 * EOS Token Loading
241 * ============================================================================ */
242
243static bool load_eos_from_vocab_json(const char *weights_path, CLIOptions *opt) {
244 if (!weights_path || !opt) return false;
245
246 /* Construct vocab.json path from weights path */
247 char vocab_path[4096];
248 const char *slash = strrchr(weights_path, '/');
249 if (!slash) return false;
250
251 size_t dir_len = (size_t)(slash - weights_path);
252 if (dir_len + 12 >= sizeof(vocab_path)) return false;
253
254 memcpy(vocab_path, weights_path, dir_len);
255 vocab_path[dir_len] = '\0';
256 strcat(vocab_path, "/vocab.json");
257
258 FILE *f = fopen(vocab_path, "r");
259 if (!f) return false;
260
261 /* Simple JSON parsing for special_tokens */
262 char buf[8192];
263 size_t n = fread(buf, 1, sizeof(buf) - 1, f);
264 fclose(f);
265 buf[n] = '\0';
266
267 /* Look for "special_tokens" section */
268 const char *st = strstr(buf, "\"special_tokens\"");
269 if (!st) return false;
270
271 /* Extract eos token */
272 const char *eos = strstr(st, "\"eos\"");
273 if (eos) {
274 const char *colon = strchr(eos, ':');
275 if (colon) {
276 int eos_id = atoi(colon + 1);
277 if (eos_id > 0) {
278 opt->eos_ids[0] = eos_id;
279 opt->eos_count = 1;
280 }
281 }
282 }
283
284 /* Extract bos token (often used as im_end for chat) */
285 const char *bos = strstr(st, "\"bos\"");
286 if (bos) {
287 const char *colon = strchr(bos, ':');
288 if (colon) {
289 int bos_id = atoi(colon + 1);
290 if (bos_id > 0 && bos_id != opt->eos_ids[0]) {
291 opt->eos_ids[opt->eos_count++] = bos_id;
292 }
293 }
294 }
295
296 return opt->eos_count > 0;
297}
298
299static void list_available_models(void) {
300 const char *cache_dir = get_cache_dir();
301 DIR *dir = opendir(cache_dir);
302 if (!dir) {
303 fprintf(stderr, "No models found in %s\n", cache_dir);
304 return;
305 }
306
307 printf("Available models in %s:\n", cache_dir);
308 struct dirent *entry;
309 int count = 0;
310 while ((entry = readdir(dir)) != NULL) {
311 if (entry->d_name[0] == '.') continue;
312
313 char model_dir[4096];
314 snprintf(model_dir, sizeof(model_dir), "%s/%s", cache_dir, entry->d_name);
315
316 char so_path[4096];
317 snprintf(so_path, sizeof(so_path), "%s/ck-kernel-inference.so", model_dir);
318
319 struct stat st;
320 if (stat(so_path, &st) == 0) {
321 printf(" - %s\n", entry->d_name);
322 count++;
323 }
324 }
325 closedir(dir);
326
327 if (count == 0) {
328 printf(" (none found)\n");
329 }
330}
331
332/* ============================================================================
333 * Sampling
334 * ============================================================================ */
335
336static int sample_top_p(float *logits, int vocab_size, float temperature, float top_p) {
337 if (temperature <= 0.0f || top_p <= 0.0f) {
338 /* Argmax */
339 int best = 0;
340 float best_val = logits[0];
341 for (int i = 1; i < vocab_size; i++) {
342 if (logits[i] > best_val) {
343 best_val = logits[i];
344 best = i;
345 }
346 }
347 return best;
348 }
349
350 /* Apply temperature */
351 float max_logit = logits[0];
352 for (int i = 1; i < vocab_size; i++) {
353 if (logits[i] > max_logit) max_logit = logits[i];
354 }
355
356 float sum = 0.0f;
357 for (int i = 0; i < vocab_size; i++) {
358 logits[i] = expf((logits[i] - max_logit) / temperature);
359 sum += logits[i];
360 }
361
362 /* Normalize to probabilities */
363 for (int i = 0; i < vocab_size; i++) {
364 logits[i] /= sum;
365 }
366
367 /* Sort indices by probability (simple selection for top-p) */
368 /* For efficiency, we'll do nucleus sampling with cumulative sum */
369 float cumsum = 0.0f;
370 float threshold = (float)rand() / (float)RAND_MAX * top_p;
371
372 /* Find nucleus tokens and sample */
373 int *indices = (int *)malloc(vocab_size * sizeof(int));
374 float *probs = (float *)malloc(vocab_size * sizeof(float));
375 for (int i = 0; i < vocab_size; i++) {
376 indices[i] = i;
377 probs[i] = logits[i];
378 }
379
380 /* Simple sort (for small vocab, bubble sort is fine; for large, use qsort) */
381 for (int i = 0; i < vocab_size - 1; i++) {
382 for (int j = i + 1; j < vocab_size; j++) {
383 if (probs[j] > probs[i]) {
384 float tmp_p = probs[i]; probs[i] = probs[j]; probs[j] = tmp_p;
385 int tmp_i = indices[i]; indices[i] = indices[j]; indices[j] = tmp_i;
386 }
387 }
388 cumsum += probs[i];
389 if (cumsum >= top_p) break;
390 }
391
392 /* Sample from nucleus */
393 float r = (float)rand() / (float)RAND_MAX * cumsum;
394 float acc = 0.0f;
395 int result = indices[0];
396 for (int i = 0; cumsum > 0 && i < vocab_size; i++) {
397 acc += probs[i];
398 if (acc >= r) {
399 result = indices[i];
400 break;
401 }
402 if (acc >= cumsum) break;
403 }
404
405 free(indices);
406 free(probs);
407 return result;
408}
409
410/* ============================================================================
411 * Output Helpers
412 * ============================================================================ */
413
414/**
415 * Decode GPT-2 byte-level BPE representation back to actual bytes.
416 *
417 * GPT-2's tokenizer maps certain bytes to Unicode code points:
418 * - Bytes 0x00-0x20 → U+0100-U+0120 (Ā Ć ċ ... Ġ)
419 * - Bytes 0x7F-0xA0 → U+017F-U+01A0
420 * - Printable ASCII (0x21-0x7E) stays as-is
421 *
422 * This function reverses that mapping.
423 *
424 * @param token Input BPE token string (UTF-8)
425 * @param out Output buffer for decoded bytes
426 * @param max Size of output buffer
427 * @return Number of bytes written (not including NUL)
428 */
429static int decode_bpe_token(const char *token, char *out, int max) {
430 if (!token || max <= 0) return 0;
431
432 const unsigned char *src = (const unsigned char *)token;
433 int out_len = 0;
434
435 while (*src && out_len < max - 1) {
436 unsigned int codepoint;
437 int bytes;
438
439 /* Decode UTF-8 to codepoint */
440 if ((src[0] & 0x80) == 0) {
441 /* Single byte ASCII */
442 codepoint = src[0];
443 bytes = 1;
444 } else if ((src[0] & 0xE0) == 0xC0 && (src[1] & 0xC0) == 0x80) {
445 /* Two byte sequence */
446 codepoint = ((src[0] & 0x1F) << 6) | (src[1] & 0x3F);
447 bytes = 2;
448 } else if ((src[0] & 0xF0) == 0xE0 && (src[1] & 0xC0) == 0x80 && (src[2] & 0xC0) == 0x80) {
449 /* Three byte sequence */
450 codepoint = ((src[0] & 0x0F) << 12) | ((src[1] & 0x3F) << 6) | (src[2] & 0x3F);
451 bytes = 3;
452 } else if ((src[0] & 0xF8) == 0xF0 && (src[1] & 0xC0) == 0x80 &&
453 (src[2] & 0xC0) == 0x80 && (src[3] & 0xC0) == 0x80) {
454 /* Four byte sequence */
455 codepoint = ((src[0] & 0x07) << 18) | ((src[1] & 0x3F) << 12) |
456 ((src[2] & 0x3F) << 6) | (src[3] & 0x3F);
457 bytes = 4;
458 } else {
459 /* Invalid UTF-8, copy byte as-is */
460 out[out_len++] = (char)*src;
461 src++;
462 continue;
463 }
464
465 /* Check if this is a GPT-2 byte-encoded character */
466 if (codepoint >= 0x100 && codepoint <= 0x120) {
467 /* Bytes 0x00-0x20: U+0100-U+0120 → byte = codepoint - 0x100 */
468 out[out_len++] = (char)(codepoint - 0x100);
469 } else if (codepoint >= 0x17F && codepoint <= 0x1A0) {
470 /* Bytes 0x7F-0xA0: U+017F-U+01A0 → byte = codepoint - 0x100 */
471 out[out_len++] = (char)(codepoint - 0x100);
472 } else if (codepoint < 0x80) {
473 /* Regular ASCII - copy as-is */
474 out[out_len++] = (char)codepoint;
475 } else if (codepoint == 0x2581) {
476 /* SentencePiece space marker ▁ (U+2581) → space */
477 out[out_len++] = ' ';
478 } else {
479 /* Other UTF-8 characters - copy original bytes */
480 for (int i = 0; i < bytes && out_len < max - 1; i++) {
481 out[out_len++] = (char)src[i];
482 }
483 }
484
485 src += bytes;
486 }
487
488 out[out_len] = '\0';
489 return out_len;
490}
491
492static void output_flush(char *buf, size_t *len) {
493 if (*len == 0) return;
494 fwrite(buf, 1, *len, stdout);
495 *len = 0;
496}
497
498static void output_append(char *buf, size_t *len, const char *text) {
499 if (!text || !*text) return;
500 size_t n = strlen(text);
501 if (*len + n >= CK_CLI_OUTPUT_BUF_SIZE) {
502 output_flush(buf, len);
503 }
504 if (n >= CK_CLI_OUTPUT_BUF_SIZE) {
505 fwrite(text, 1, n, stdout);
506 return;
507 }
508 memcpy(buf + *len, text, n);
509 *len += n;
510}
511
512static bool token_has_gpt2_bytes(const char *token) {
513 if (!token) return false;
514 const unsigned char *p = (const unsigned char *)token;
515 while (*p) {
516 if ((p[0] & 0x80) == 0) {
517 p++;
518 continue;
519 }
520 if ((p[0] & 0xE0) == 0xC0 && (p[1] & 0xC0) == 0x80) {
521 unsigned int cp = ((p[0] & 0x1F) << 6) | (p[1] & 0x3F);
522 if ((cp >= 0x100 && cp <= 0x120) || (cp >= 0x17F && cp <= 0x1A0)) {
523 return true;
524 }
525 p += 2;
526 continue;
527 }
528 if ((p[0] & 0xF0) == 0xE0 && (p[1] & 0xC0) == 0x80 && (p[2] & 0xC0) == 0x80) {
529 p += 3;
530 continue;
531 }
532 if ((p[0] & 0xF8) == 0xF0 && (p[1] & 0xC0) == 0x80 &&
533 (p[2] & 0xC0) == 0x80 && (p[3] & 0xC0) == 0x80) {
534 p += 4;
535 continue;
536 }
537 p++;
538 }
539 return false;
540}
541
542static bool detect_gpt2_byte_fallback(CKTrueBPE *tokenizer, int vocab_size) {
543 if (!tokenizer || vocab_size <= 0) return false;
544 int limit = vocab_size < 2048 ? vocab_size : 2048;
545 for (int i = 0; i < limit; i++) {
546 const char *tok = ck_true_bpe_id_to_token(tokenizer, i);
547 if (tok && token_has_gpt2_bytes(tok)) {
548 return true;
549 }
550 }
551 return false;
552}
553
554static void output_token(char *buf, size_t *len, const char *token) {
555 if (!token || !*token) return;
556
557 if (!g_use_byte_decoder) {
558 output_append(buf, len, token);
559 return;
560 }
561
562 /* Decode BPE byte-level encoding to actual bytes */
563 char decoded[1024];
564 int n = decode_bpe_token(token, decoded, sizeof(decoded));
565 if (n > 0) {
566 output_append(buf, len, decoded);
567 }
568}
569
570/* ============================================================================
571 * Model Loading
572 * ============================================================================ */
573
574static bool resolve_symbol(void *handle, const char *name, void **out_ptr, bool required) {
575 void *sym = dlsym(handle, name);
576 if (!sym && required) {
577 fprintf(stderr, "Error: missing symbol %s\n", name);
578 return false;
579 }
580 if (out_ptr) *out_ptr = sym;
581 return true;
582}
583
584static bool load_model_api(const char *lib_path, ModelAPI *api) {
585 if (!lib_path || !api) return false;
586 memset(api, 0, sizeof(*api));
587 api->handle = dlopen(lib_path, RTLD_NOW);
588 if (!api->handle) {
589 fprintf(stderr, "Error: dlopen failed: %s\n", dlerror());
590 return false;
591 }
592
593 if (!resolve_symbol(api->handle, "ck_model_init", (void **)&api->init, true)) return false;
594 if (!resolve_symbol(api->handle, "ck_model_embed_tokens", (void **)&api->embed, true)) return false;
595 if (!resolve_symbol(api->handle, "ck_model_forward", (void **)&api->forward, true)) return false;
596 if (!resolve_symbol(api->handle, "ck_model_decode", (void **)&api->decode, true)) return false;
597 resolve_symbol(api->handle, "ck_model_sample_argmax", (void **)&api->sample, false); /* Optional - we can sample from logits */
598 resolve_symbol(api->handle, "ck_model_get_logits", (void **)&api->get_logits, false);
599 resolve_symbol(api->handle, "ck_model_get_logits_stride", (void **)&api->get_logits_stride, false);
600 resolve_symbol(api->handle, "ck_model_kv_cache_enable", (void **)&api->kv_enable, false);
601 resolve_symbol(api->handle, "ck_model_kv_cache_reset", (void **)&api->kv_reset, false);
602 resolve_symbol(api->handle, "ck_model_get_context_window", (void **)&api->get_context, false);
603 resolve_symbol(api->handle, "ck_model_get_vocab_size", (void **)&api->get_vocab_size, false);
604 resolve_symbol(api->handle, "ck_model_get_num_merges", (void **)&api->get_num_merges, false);
605 resolve_symbol(api->handle, "ck_model_get_vocab_strings_size", (void **)&api->get_vocab_bytes, false);
606 resolve_symbol(api->handle, "ck_model_get_active_tokens", (void **)&api->get_active_tokens, false);
607 resolve_symbol(api->handle, "ck_model_get_vocab_offsets", (void **)&api->get_offsets, false);
608 resolve_symbol(api->handle, "ck_model_get_vocab_strings", (void **)&api->get_strings, false);
609 resolve_symbol(api->handle, "ck_model_get_vocab_merges", (void **)&api->get_merges, false);
610 resolve_symbol(api->handle, "ck_model_free", (void **)&api->free_fn, false);
611
612 if (!api->get_vocab_size || !api->get_offsets || !api->get_strings) {
613 fprintf(stderr, "Error: vocab accessors missing from model\n");
614 return false;
615 }
616 return true;
617}
618
619/* ============================================================================
620 * Chat Template Application
621 * ============================================================================ */
622
623static ChatTemplateType detect_chat_template(const char *model_name) {
624 if (!model_name) return CHAT_TEMPLATE_CHATML;
625
626 /* Lowercase comparison */
627 char lower[256];
628 strncpy(lower, model_name, sizeof(lower) - 1);
629 for (char *p = lower; *p; p++) *p = (*p >= 'A' && *p <= 'Z') ? *p + 32 : *p;
630
631 if (strstr(lower, "qwen")) return CHAT_TEMPLATE_QWEN;
632 if (strstr(lower, "llama")) return CHAT_TEMPLATE_LLAMA;
633 if (strstr(lower, "mistral")) return CHAT_TEMPLATE_MISTRAL;
634
635 return CHAT_TEMPLATE_CHATML; /* Default */
636}
637
638static char *apply_chat_template(const ChatTemplate *tmpl, const char *system, const char *user) {
639 size_t needed = 0;
640 if (system && *system) {
641 needed += strlen(tmpl->system_prefix) + strlen(system) + strlen(tmpl->system_suffix);
642 }
643 needed += strlen(tmpl->user_prefix) + strlen(user) + strlen(tmpl->user_suffix);
644 needed += strlen(tmpl->assistant_prefix);
645 needed += 1; /* null terminator */
646
647 char *result = (char *)malloc(needed);
648 if (!result) return NULL;
649
650 result[0] = '\0';
651 if (system && *system) {
652 strcat(result, tmpl->system_prefix);
653 strcat(result, system);
654 strcat(result, tmpl->system_suffix);
655 }
656 strcat(result, tmpl->user_prefix);
657 strcat(result, user);
658 strcat(result, tmpl->user_suffix);
659 strcat(result, tmpl->assistant_prefix);
660
661 return result;
662}
663
664/* ============================================================================
665 * EOS Token Handling
666 * ============================================================================ */
667
668static bool is_eos_token(const CLIOptions *opt, int token) {
669 if (!opt || opt->ignore_eos) return false;
670 for (int i = 0; i < opt->eos_count; i++) {
671 if (opt->eos_ids[i] == token) return true;
672 }
673 return false;
674}
675
676/**
677 * Text-based EOS pattern detection with pending output buffering.
678 *
679 * When special tokens like <|im_end|> are tokenized as regular text
680 * (e.g., !, im, _end, !), we need to detect the pattern in the output
681 * and avoid outputting the partial pattern tokens.
682 *
683 * This is a workaround for tokenizers that don't properly encode special tokens.
684 */
685#define EOS_PATTERN_BUF_SIZE 64
686#define EOS_PENDING_MAX 8
687
688typedef struct {
689 char pattern_buf[EOS_PATTERN_BUF_SIZE]; /* Accumulated text for pattern matching */
690 int pattern_len;
691 char *pending[EOS_PENDING_MAX]; /* Pending token texts (not yet output) */
692 int pending_count;
693 const char *target_pattern; /* Pattern to detect */
694 const char *partial_prefix; /* Prefix that might start the pattern */
695} EOSPatternState;
696
697static EOSPatternState g_eos_state = {0};
698
699static void eos_pattern_reset(void) {
700 g_eos_state.pattern_len = 0;
701 g_eos_state.pattern_buf[0] = '\0';
702 for (int i = 0; i < g_eos_state.pending_count; i++) {
703 free(g_eos_state.pending[i]);
704 g_eos_state.pending[i] = NULL;
705 }
706 g_eos_state.pending_count = 0;
707 g_eos_state.target_pattern = NULL;
708 g_eos_state.partial_prefix = NULL;
709}
710
713 switch (tmpl) {
716 g_eos_state.target_pattern = "im_end";
717 g_eos_state.partial_prefix = "im";
718 break;
721 g_eos_state.target_pattern = "</s>";
722 g_eos_state.partial_prefix = "</";
723 break;
724 default:
725 break;
726 }
727}
728
729/**
730 * Check if token might be start of EOS pattern.
731 */
732static bool eos_is_potential_prefix(const char *token) {
733 if (!token || !g_eos_state.partial_prefix) return false;
734
735 /* Check if current accumulated buffer + token could start the pattern */
736 size_t tlen = strlen(token);
737 size_t plen = g_eos_state.pattern_len;
738 size_t target_len = g_eos_state.target_pattern ? strlen(g_eos_state.target_pattern) : 0;
739
740 /* If buffer + token contains partial match of target, it's a potential prefix */
741 if (target_len == 0) return false;
742
743 /* Build temp buffer */
744 char temp[EOS_PATTERN_BUF_SIZE];
745 if (plen + tlen >= EOS_PATTERN_BUF_SIZE) return false;
746 memcpy(temp, g_eos_state.pattern_buf, plen);
747 memcpy(temp + plen, token, tlen);
748 temp[plen + tlen] = '\0';
749
750 /* Check if temp is a prefix of target or contains start of target */
751 const char *target = g_eos_state.target_pattern;
752 size_t temp_len = plen + tlen;
753
754 /* Look for any suffix of temp that is a prefix of target */
755 for (size_t i = 0; i < temp_len; i++) {
756 size_t remaining = temp_len - i;
757 if (remaining > target_len) remaining = target_len;
758 if (strncmp(temp + i, target, remaining) == 0) {
759 return true;
760 }
761 }
762
763 return false;
764}
765
766/**
767 * Process a token for EOS pattern detection.
768 *
769 * @param token_text The token text to process
770 * @param out_buf Output buffer for safe-to-output text
771 * @param out_len Current length of output buffer
772 * @param tmpl Chat template type
773 * @return true if EOS pattern detected, false otherwise
774 */
775static bool eos_pattern_process(const char *token_text, char *out_buf, size_t *out_len,
776 void (*output_fn)(char*, size_t*, const char*),
777 ChatTemplateType tmpl) {
778 if (!token_text || !g_eos_state.target_pattern) {
779 /* No pattern to match - output directly */
780 if (token_text && output_fn) output_fn(out_buf, out_len, token_text);
781 return false;
782 }
783
784 /* Append to pattern buffer */
785 size_t tlen = strlen(token_text);
786 if (g_eos_state.pattern_len + (int)tlen < EOS_PATTERN_BUF_SIZE - 1) {
787 memcpy(g_eos_state.pattern_buf + g_eos_state.pattern_len, token_text, tlen);
788 g_eos_state.pattern_len += (int)tlen;
789 g_eos_state.pattern_buf[g_eos_state.pattern_len] = '\0';
790 }
791
792 /* Check if pattern is complete */
793 if (strstr(g_eos_state.pattern_buf, g_eos_state.target_pattern)) {
794 /* EOS detected - don't output pending tokens */
796 return true;
797 }
798
799 /* Check if this could still be part of the pattern */
800 if (eos_is_potential_prefix(token_text)) {
801 /* Hold this token - might be part of EOS */
802 if (g_eos_state.pending_count < EOS_PENDING_MAX) {
803 g_eos_state.pending[g_eos_state.pending_count] = strdup(token_text);
804 g_eos_state.pending_count++;
805 }
806 return false;
807 }
808
809 /* Not part of pattern - flush pending tokens and this one */
810 for (int i = 0; i < g_eos_state.pending_count; i++) {
811 if (output_fn) output_fn(out_buf, out_len, g_eos_state.pending[i]);
812 free(g_eos_state.pending[i]);
813 g_eos_state.pending[i] = NULL;
814 }
815 g_eos_state.pending_count = 0;
816 g_eos_state.pattern_len = 0;
817 g_eos_state.pattern_buf[0] = '\0';
818
819 if (output_fn) output_fn(out_buf, out_len, token_text);
820 return false;
821}
822
823static bool parse_eos_ids(const char *arg, CLIOptions *opt) {
824 if (!arg || !opt) return false;
825 opt->eos_count = 0;
826 const char *p = arg;
827 while (*p && opt->eos_count < CK_CLI_EOS_MAX) {
828 char *end = NULL;
829 long v = strtol(p, &end, 10);
830 if (end == p) break;
831 opt->eos_ids[opt->eos_count++] = (int)v;
832 p = end;
833 if (*p == ',') p++;
834 }
835 return opt->eos_count > 0;
836}
837
838/* ============================================================================
839 * Prompt Execution
840 * ============================================================================ */
841
842static int run_prompt(ModelAPI *api, CKTrueBPE *tokenizer, CLIOptions *opt, const char *input) {
843 if (!api || !tokenizer || !opt || !input) return -1;
844 if (g_exit_requested) return -1;
845
846 int ctx = opt->context_override;
847 if (ctx <= 0 && api->get_context) ctx = api->get_context();
848 if (ctx <= 0) ctx = 4096;
849 if (ctx > CK_CLI_MAX_CONTEXT) ctx = CK_CLI_MAX_CONTEXT;
850
851 int max_tokens = opt->max_tokens > 0 ? opt->max_tokens : CK_CLI_DEFAULT_MAX_TOKENS;
852
853 /* Apply chat template if enabled */
854 const ChatTemplate *tmpl = &g_templates[opt->no_chat_template ? CHAT_TEMPLATE_NONE : opt->chat_template];
855 char *formatted = apply_chat_template(tmpl, opt->system_prompt, input);
856 if (!formatted) {
857 fprintf(stderr, "Error: failed to format prompt\n");
858 return -1;
859 }
860
861 if (opt->verbose) {
862 printf("[DEBUG] Formatted prompt:\n%s\n", formatted);
863 }
864
865 int32_t *ids = (int32_t *)malloc((size_t)ctx * sizeof(int32_t));
866 if (!ids) {
867 fprintf(stderr, "Error: failed to allocate token buffer\n");
868 free(formatted);
869 return -1;
870 }
871
872 int n = ck_true_bpe_encode(tokenizer, formatted, -1, ids, ctx);
873 free(formatted);
874
875 if (n <= 0) {
876 fprintf(stderr, "[Tokenizer] failed to encode prompt\n");
877 free(ids);
878 return -1;
879 }
880 if (n > ctx - max_tokens) {
881 n = ctx - max_tokens;
882 if (opt->verbose) {
883 printf("[DEBUG] Truncated prompt to %d tokens\n", n);
884 }
885 }
886
887 g_prefill_time_ms = 0.0;
888 g_decode_time_ms = 0.0;
889 g_decode_count = 0;
890 g_prompt_tokens = n;
891
892 if (api->kv_reset) api->kv_reset();
893
894 if (api->embed(ids, n) != 0) {
895 fprintf(stderr, "[Model] embed failed\n");
896 free(ids);
897 return -1;
898 }
899
900 struct timespec t0, t1;
901 clock_gettime(CLOCK_MONOTONIC, &t0);
902 if (api->forward(NULL) != 0) {
903 fprintf(stderr, "[Model] forward failed\n");
904 free(ids);
905 return -1;
906 }
907 clock_gettime(CLOCK_MONOTONIC, &t1);
908 g_prefill_time_ms = (t1.tv_sec - t0.tv_sec) * 1000.0 +
909 (t1.tv_nsec - t0.tv_nsec) / 1000000.0;
910
911 /* Get vocab size for sampling */
912 int vocab_size = api->get_vocab_size ? api->get_vocab_size() : 0;
913
914 /* Helper: sample next token from logits */
915 #define SAMPLE_NEXT_TOKEN() do { \
916 if (api->get_logits && vocab_size > 0) { \
917 float *logits = api->get_logits(); \
918 if (logits) { \
919 int stride = api->get_logits_stride ? api->get_logits_stride() : vocab_size; \
920 int active = api->get_active_tokens ? api->get_active_tokens() : 1; \
921 float *last_logits = logits; \
922 if (stride > 0) { \
923 if (active < 1) active = 1; \
924 last_logits = logits + (size_t)(active - 1) * (size_t)stride; \
925 } \
926 float *logits_copy = (float *)malloc(vocab_size * sizeof(float)); \
927 memcpy(logits_copy, last_logits, vocab_size * sizeof(float)); \
928 next_token = sample_top_p(logits_copy, vocab_size, opt->temperature, opt->top_p); \
929 free(logits_copy); \
930 } else if (api->sample) { \
931 next_token = api->sample(); \
932 } else { \
933 next_token = -1; \
934 } \
935 } else if (api->sample) { \
936 next_token = api->sample(); \
937 } else { \
938 next_token = -1; \
939 } \
940 } while(0)
941
942 /* Sample first token */
943 int next_token;
945
946 char out_buf[CK_CLI_OUTPUT_BUF_SIZE];
947 size_t out_len = 0;
948
949 /* Initialize EOS pattern detection for this prompt */
950 eos_pattern_init(opt->chat_template);
951
953
954 for (int generated = 0; generated < max_tokens && !g_exit_requested && g_generation_active; generated++) {
955 if (next_token < 0) break;
956
957 if (opt->verbose) {
958 const char *tok_str = ck_true_bpe_id_to_token(tokenizer, next_token);
959 fprintf(stderr, "[DEBUG] Token %d: %d (%s)\n", generated, next_token, tok_str ? tok_str : "NULL");
960 }
961
962 if (is_eos_token(opt, next_token)) {
963 if (opt->verbose) {
964 fprintf(stderr, "[DEBUG] EOS detected (token ID), stopping\n");
965 }
966 break;
967 }
968
969 const char *word = ck_true_bpe_id_to_token(tokenizer, next_token);
970
971 /* Process token through EOS pattern detection (buffers potential EOS tokens) */
972 if (!opt->ignore_eos &&
973 eos_pattern_process(word, out_buf, &out_len, output_token, opt->chat_template)) {
974 if (opt->verbose) {
975 fprintf(stderr, "[DEBUG] EOS detected (text pattern), stopping\n");
976 }
977 break;
978 }
979
980 if (opt->stream) {
981 output_flush(out_buf, &out_len);
982 fflush(stdout);
983 } else if (out_len > (CK_CLI_OUTPUT_BUF_SIZE / 2)) {
984 output_flush(out_buf, &out_len);
985 fflush(stdout);
986 }
987
988 if (generated + 1 >= max_tokens) break;
989
990 clock_gettime(CLOCK_MONOTONIC, &t0);
991 if (api->decode(next_token, NULL) != 0) {
992 fprintf(stderr, "\n[Model] decode failed\n");
993 break;
994 }
995 clock_gettime(CLOCK_MONOTONIC, &t1);
996 g_decode_time_ms += (t1.tv_sec - t0.tv_sec) * 1000.0 +
997 (t1.tv_nsec - t0.tv_nsec) / 1000000.0;
999
1000 /* Sample next token */
1002 }
1003
1004 #undef SAMPLE_NEXT_TOKEN
1006 output_flush(out_buf, &out_len);
1007 printf("\n");
1008
1009 if (opt->timing) {
1010 double total_ms = g_prefill_time_ms + g_decode_time_ms;
1011 double prefill_rate = g_prompt_tokens / (g_prefill_time_ms / 1000.0);
1012 double decode_rate = g_decode_count > 0 ? g_decode_count / (g_decode_time_ms / 1000.0) : 0.0;
1013 double avg_decode = g_decode_count > 0 ? g_decode_time_ms / g_decode_count : 0.0;
1014
1015 printf("\033[90m"); /* Gray text */
1016 printf("prompt: %3d tok / %7.1f ms (%5.1f tok/s) | ", g_prompt_tokens, g_prefill_time_ms, prefill_rate);
1017 printf("decode: %3d tok / %7.1f ms (%5.1f tok/s, %5.1f ms/tok)\033[0m\n",
1018 g_decode_count, g_decode_time_ms, decode_rate, avg_decode);
1019 }
1020 fflush(stdout);
1021
1022 free(ids);
1023 return 0;
1024}
1025
1026/* ============================================================================
1027 * Help & Argument Parsing
1028 * ============================================================================ */
1029
1030static void print_banner(void) {
1031 printf("\n");
1032 printf(" \033[1;36mC-Kernel-Engine v%s\033[0m\n", CK_CLI_VERSION);
1033 printf(" Native inference CLI with true-BPE tokenization\n");
1034 printf("\n");
1035}
1036
1037static void print_help(const char *prog) {
1038 print_banner();
1039 fprintf(stderr, "Usage:\n");
1040 fprintf(stderr, " %s --model <name> Auto-discover model from cache\n", prog);
1041 fprintf(stderr, " %s <libmodel.so> <weights.bump> Direct paths\n", prog);
1042 fprintf(stderr, " %s --lib <.so> --weights <.bump> Named arguments\n", prog);
1043 fprintf(stderr, "\nOptions:\n");
1044 fprintf(stderr, " --model, -m NAME Model name (searches in cache)\n");
1045 fprintf(stderr, " --lib PATH Path to compiled model .so\n");
1046 fprintf(stderr, " --weights PATH Path to weights .bump file\n");
1047 fprintf(stderr, " --prompt, -p TEXT Run single prompt (non-interactive)\n");
1048 fprintf(stderr, " --system, -S TEXT System prompt\n");
1049 fprintf(stderr, " --max-tokens, -n N Max tokens to generate (default: %d)\n", CK_CLI_DEFAULT_MAX_TOKENS);
1050 fprintf(stderr, " --context, -c N Override context/KV cache size\n");
1051 fprintf(stderr, " --temperature, -T F Sampling temperature (default: 0.0 = greedy)\n");
1052 fprintf(stderr, " --top-p F Nucleus sampling top-p (default: 0.9)\n");
1053 fprintf(stderr, " --stream, -s Stream tokens as generated\n");
1054 fprintf(stderr, " --timing, -t Show timing breakdown\n");
1055 fprintf(stderr, " --no-chat-template Disable chat template formatting\n");
1056 fprintf(stderr, " --eos IDS Comma-separated EOS token IDs\n");
1057 fprintf(stderr, " --ignore-eos Do not stop on EOS tokens\n");
1058 fprintf(stderr, " --list List available models\n");
1059 fprintf(stderr, " --verbose, -v Verbose output\n");
1060 fprintf(stderr, " --help, -h Show this help\n");
1061 fprintf(stderr, "\nREPL Commands:\n");
1062 fprintf(stderr, " /exit, /quit Exit the REPL\n");
1063 fprintf(stderr, " /reset Reset KV cache\n");
1064 fprintf(stderr, " /timing Toggle timing display\n");
1065 fprintf(stderr, " /temp <value> Set temperature\n");
1066 fprintf(stderr, " /system <text> Set system prompt\n");
1067 fprintf(stderr, " /help Show help\n");
1068}
1069
1070static bool parse_args(int argc, char **argv, CLIOptions *opt) {
1071 if (!opt) return false;
1072 memset(opt, 0, sizeof(*opt));
1073 opt->max_tokens = CK_CLI_DEFAULT_MAX_TOKENS;
1074 opt->temperature = 0.0f; /* Greedy by default */
1075 opt->top_p = 0.9f;
1076 opt->stream = true; /* Stream by default */
1077 opt->timing = true; /* Show timing by default */
1078 /* Default EOS tokens for Qwen/ChatML */
1079 opt->eos_ids[0] = 151643; /* <|im_end|> */
1080 opt->eos_ids[1] = 151645; /* <|endoftext|> */
1081 opt->eos_ids[2] = 151644; /* <|im_sep|> */
1082 opt->eos_count = 3;
1083
1084 for (int i = 1; i < argc; i++) {
1085 const char *arg = argv[i];
1086
1087 if (!strcmp(arg, "--help") || !strcmp(arg, "-h")) {
1088 print_help(argv[0]);
1089 return false;
1090 } else if (!strcmp(arg, "--list")) {
1092 return false;
1093 } else if ((!strcmp(arg, "--model") || !strcmp(arg, "-m")) && i + 1 < argc) {
1094 opt->model_name = argv[++i];
1095 } else if (!strcmp(arg, "--lib") && i + 1 < argc) {
1096 opt->lib_path = argv[++i];
1097 } else if (!strcmp(arg, "--weights") && i + 1 < argc) {
1098 opt->weights_path = argv[++i];
1099 } else if ((!strcmp(arg, "--prompt") || !strcmp(arg, "-p")) && i + 1 < argc) {
1100 opt->prompt_once = argv[++i];
1101 } else if ((!strcmp(arg, "--system") || !strcmp(arg, "-S")) && i + 1 < argc) {
1102 opt->system_prompt = argv[++i];
1103 } else if ((!strcmp(arg, "--max-tokens") || !strcmp(arg, "-n")) && i + 1 < argc) {
1104 opt->max_tokens = atoi(argv[++i]);
1105 } else if ((!strcmp(arg, "--context") || !strcmp(arg, "-c")) && i + 1 < argc) {
1106 opt->context_override = atoi(argv[++i]);
1107 } else if ((!strcmp(arg, "--temperature") || !strcmp(arg, "-T")) && i + 1 < argc) {
1108 opt->temperature = (float)atof(argv[++i]);
1109 } else if (!strcmp(arg, "--top-p") && i + 1 < argc) {
1110 opt->top_p = (float)atof(argv[++i]);
1111 } else if (!strcmp(arg, "--stream") || !strcmp(arg, "-s")) {
1112 opt->stream = true;
1113 } else if (!strcmp(arg, "--no-stream")) {
1114 opt->stream = false;
1115 } else if (!strcmp(arg, "--timing") || !strcmp(arg, "-t")) {
1116 opt->timing = true;
1117 } else if (!strcmp(arg, "--no-timing")) {
1118 opt->timing = false;
1119 } else if (!strcmp(arg, "--no-chat-template")) {
1120 opt->no_chat_template = true;
1121 } else if (!strcmp(arg, "--eos") && i + 1 < argc) {
1122 parse_eos_ids(argv[++i], opt);
1123 } else if (!strcmp(arg, "--ignore-eos")) {
1124 opt->ignore_eos = true;
1125 } else if (!strcmp(arg, "--verbose") || !strcmp(arg, "-v")) {
1126 opt->verbose = true;
1127 } else if (arg[0] != '-') {
1128 if (!opt->lib_path) opt->lib_path = arg;
1129 else if (!opt->weights_path) opt->weights_path = arg;
1130 else {
1131 fprintf(stderr, "Unknown argument: %s\n", arg);
1132 return false;
1133 }
1134 } else {
1135 fprintf(stderr, "Unknown option: %s\n", arg);
1136 return false;
1137 }
1138 }
1139
1140 /* Auto-discover model if --model specified */
1141 if (opt->model_name && (!opt->lib_path || !opt->weights_path)) {
1142 static char lib_buf[4096], weights_buf[4096];
1143 if (find_model_in_cache(opt->model_name, lib_buf, weights_buf, sizeof(lib_buf))) {
1144 opt->lib_path = lib_buf;
1145 opt->weights_path = weights_buf;
1146 } else {
1147 fprintf(stderr, "Error: model '%s' not found in cache\n", opt->model_name);
1148 fprintf(stderr, "Run with --list to see available models\n");
1149 return false;
1150 }
1151 }
1152
1153 if (!opt->lib_path || !opt->weights_path) {
1154 print_help(argv[0]);
1155 return false;
1156 }
1157
1158 /* Auto-detect chat template from model name/path */
1159 const char *name_for_template = opt->model_name ? opt->model_name : opt->lib_path;
1160 opt->chat_template = detect_chat_template(name_for_template);
1161
1162 /* Load EOS tokens from vocab.json if available */
1163 if (load_eos_from_vocab_json(opt->weights_path, opt)) {
1164 if (opt->verbose) {
1165 printf("[DEBUG] Loaded %d EOS tokens: ", opt->eos_count);
1166 for (int i = 0; i < opt->eos_count; i++) {
1167 printf("%d ", opt->eos_ids[i]);
1168 }
1169 printf("\n");
1170 }
1171 }
1172
1173 return true;
1174}
1175
1176/* ============================================================================
1177 * REPL Command Processing
1178 * ============================================================================ */
1179
1180static bool process_repl_command(const char *line, CLIOptions *opt, ModelAPI *api) {
1181 if (!line || line[0] != '/') return false;
1182
1183 if (!strncmp(line, "/exit", 5) || !strncmp(line, "/quit", 5)) {
1184 g_exit_requested = 1;
1185 return true;
1186 }
1187 if (!strncmp(line, "/help", 5)) {
1188 printf("REPL Commands:\n");
1189 printf(" /exit, /quit Exit\n");
1190 printf(" /reset Reset KV cache\n");
1191 printf(" /timing Toggle timing display\n");
1192 printf(" /temp <value> Set temperature (0 = greedy)\n");
1193 printf(" /top-p <value> Set top-p\n");
1194 printf(" /system <text> Set system prompt\n");
1195 printf(" /clear Clear system prompt\n");
1196 printf(" /verbose Toggle verbose mode\n");
1197 return true;
1198 }
1199 if (!strncmp(line, "/reset", 6)) {
1200 if (api->kv_reset) {
1201 api->kv_reset();
1202 printf("[KV cache reset]\n");
1203 }
1204 return true;
1205 }
1206 if (!strncmp(line, "/timing", 7)) {
1207 opt->timing = !opt->timing;
1208 printf("[Timing %s]\n", opt->timing ? "enabled" : "disabled");
1209 return true;
1210 }
1211 if (!strncmp(line, "/verbose", 8)) {
1212 opt->verbose = !opt->verbose;
1213 printf("[Verbose %s]\n", opt->verbose ? "enabled" : "disabled");
1214 return true;
1215 }
1216 if (!strncmp(line, "/temp ", 6)) {
1217 opt->temperature = (float)atof(line + 6);
1218 printf("[Temperature set to %.2f]\n", opt->temperature);
1219 return true;
1220 }
1221 if (!strncmp(line, "/top-p ", 7)) {
1222 opt->top_p = (float)atof(line + 7);
1223 printf("[Top-p set to %.2f]\n", opt->top_p);
1224 return true;
1225 }
1226 if (!strncmp(line, "/system ", 8)) {
1227 opt->system_prompt = strdup(line + 8);
1228 printf("[System prompt set]\n");
1229 return true;
1230 }
1231 if (!strncmp(line, "/clear", 6)) {
1232 opt->system_prompt = NULL;
1233 printf("[System prompt cleared]\n");
1234 return true;
1235 }
1236
1237 printf("Unknown command: %s\n", line);
1238 return true;
1239}
1240
1241/* ============================================================================
1242 * Main
1243 * ============================================================================ */
1244
1245int main(int argc, char **argv) {
1246 signal(SIGINT, handle_sigint);
1247 srand((unsigned int)time(NULL));
1248
1249 CLIOptions opt;
1250 if (!parse_args(argc, argv, &opt)) {
1251 return 1;
1252 }
1253
1254 print_banner();
1255 printf("Loading: %s\n", opt.lib_path);
1256
1257 ModelAPI api;
1258 if (!load_model_api(opt.lib_path, &api)) {
1259 return 1;
1260 }
1261
1262 printf("Initializing model...\n");
1263 if (api.init(opt.weights_path) != 0) {
1264 fprintf(stderr, "Error: ck_model_init failed\n");
1265 return 1;
1266 }
1267
1268 int ctx = opt.context_override;
1269 if (ctx <= 0 && api.get_context) ctx = api.get_context();
1270 if (api.kv_enable && ctx > 0) {
1271 api.kv_enable(ctx);
1272 }
1273
1274 CKTrueBPE *tokenizer = ck_true_bpe_create();
1275 if (!tokenizer) {
1276 fprintf(stderr, "[Tokenizer] failed to create\n");
1277 return 1;
1278 }
1279
1280 int vocab_size = api.get_vocab_size ? api.get_vocab_size() : 0;
1281 int vocab_bytes = api.get_vocab_bytes ? api.get_vocab_bytes() : 0;
1282 int num_merges = api.get_num_merges ? api.get_num_merges() : 0;
1283 const int32_t *offsets = (const int32_t *)api.get_offsets();
1284 const char *strings = (const char *)api.get_strings();
1285 const int32_t *merges = api.get_merges ? (const int32_t *)api.get_merges() : NULL;
1286
1287 if (vocab_size <= 0 || vocab_bytes <= 0 || !offsets || !strings) {
1288 fprintf(stderr, "[Tokenizer] missing vocab data in model\n");
1289 ck_true_bpe_free(tokenizer);
1290 return 1;
1291 }
1292
1294 fprintf(stderr, "[Tokenizer] failed to load vocab\n");
1295 ck_true_bpe_free(tokenizer);
1296 return 1;
1297 }
1298
1299 /* Register special tokens for pre-BPE matching.
1300 * This is done in the CLI (orchestrator), NOT the generated model code.
1301 * The generated model code stays "dumb" - just inference.
1302 * Model-specific token handling is the CLI's responsibility.
1303 */
1304 {
1305 /* Common special tokens across model families */
1306 static const char *special_tokens[] = {
1307 /* Qwen/ChatML */
1308 "<|im_start|>", "<|im_end|>", "<|endoftext|>",
1309 /* Llama 3 */
1310 "<|eot_id|>", "<|begin_of_text|>", "<|end_of_text|>",
1311 "<|start_header_id|>", "<|end_header_id|>",
1312 /* Generic */
1313 "</s>", "<s>", "<pad>", "<unk>",
1314 NULL
1315 };
1316 int registered = 0;
1317 for (int i = 0; special_tokens[i] != NULL; i++) {
1318 int32_t id = ck_true_bpe_lookup(tokenizer, special_tokens[i]);
1319 /* Verify it's actually this token (not unk) via round-trip */
1320 const char *check = ck_true_bpe_id_to_token(tokenizer, id);
1321 if (check && strcmp(check, special_tokens[i]) == 0) {
1322 ck_true_bpe_add_special_token(tokenizer, special_tokens[i], id);
1323 registered++;
1324 if (opt.verbose) {
1325 printf("[Tokenizer] Registered special: %s -> %d\n", special_tokens[i], id);
1326 }
1327 }
1328 }
1329 if (opt.verbose) {
1330 printf("[Tokenizer] Registered %d special tokens for pre-BPE matching\n", registered);
1331 }
1332 }
1333
1335 if (opt.verbose) {
1336 printf("[Tokenizer] Byte-level decoder: %s\n", g_use_byte_decoder ? "ON" : "OFF");
1337 }
1338
1339 printf("Ready! Vocab: %d, Context: %d, Template: %s\n",
1340 vocab_size, ctx,
1341 opt.no_chat_template ? "none" :
1342 opt.chat_template == CHAT_TEMPLATE_QWEN ? "qwen" :
1343 opt.chat_template == CHAT_TEMPLATE_LLAMA ? "llama" :
1344 opt.chat_template == CHAT_TEMPLATE_MISTRAL ? "mistral" : "chatml");
1345
1346 /* Print CPU capability info */
1348 printf("[Hardware] %s | Vector: %d-bit | FMA: %s | AI Accel: %s | Kernel: %s\n",
1349 cap.name, cap.width, cap.has_fma ? "Yes" : "No",
1350 cap.has_ai_accel ? "Yes" : "No", cap.best_kernel);
1351
1352 printf("Type /help for commands, Ctrl+C to stop generation\n\n");
1353
1354 setvbuf(stdout, NULL, _IOFBF, 1 << 20);
1355
1356 if (opt.prompt_once) {
1357 run_prompt(&api, tokenizer, &opt, opt.prompt_once);
1358 } else {
1359 /* REPL */
1360#ifdef HAVE_READLINE
1361 char *home = getenv("HOME");
1362 char history_path[4096];
1363 if (home) {
1364 snprintf(history_path, sizeof(history_path), "%s/%s", home, CK_CLI_HISTORY_FILE);
1365 read_history(history_path);
1366 }
1367#endif
1368
1369 while (!g_exit_requested) {
1370#ifdef HAVE_READLINE
1371 char *line = readline("\033[1;32mYou:\033[0m ");
1372 if (!line) break;
1373 if (*line) add_history(line);
1374#else
1375 printf("\033[1;32mYou:\033[0m ");
1376 fflush(stdout);
1377 char line_buf[4096];
1378 if (!fgets(line_buf, sizeof(line_buf), stdin)) {
1379 if (feof(stdin) || g_exit_requested) break;
1380 if (errno == EINTR) break;
1381 continue;
1382 }
1383 /* Remove trailing newline */
1384 size_t len = strlen(line_buf);
1385 if (len > 0 && line_buf[len-1] == '\n') line_buf[len-1] = '\0';
1386 char *line = line_buf;
1387#endif
1388
1389 if (line[0] == '\0') {
1390#ifdef HAVE_READLINE
1391 free(line);
1392#endif
1393 continue;
1394 }
1395
1396 if (line[0] == '/') {
1397 process_repl_command(line, &opt, &api);
1398#ifdef HAVE_READLINE
1399 free(line);
1400#endif
1401 continue;
1402 }
1403
1404 printf("\033[1;34mAssistant:\033[0m ");
1405 fflush(stdout);
1406 run_prompt(&api, tokenizer, &opt, line);
1407
1408#ifdef HAVE_READLINE
1409 free(line);
1410#endif
1411 }
1412
1413#ifdef HAVE_READLINE
1414 if (home) {
1415 write_history(history_path);
1416 }
1417#endif
1418 }
1419
1420 ck_true_bpe_free(tokenizer);
1421 if (api.free_fn) api.free_fn();
1422 if (api.handle) dlclose(api.handle);
1423
1424 printf("\nGoodbye!\n");
1425 return 0;
1426}
ChatTemplateType
#define EOS_PENDING_MAX
#define EOS_PATTERN_BUF_SIZE
void(* kv_reset_t)(void)
Definition ck_cli_v6.6.c:74
static bool parse_eos_ids(const char *arg, CLIOptions *opt)
int(* init_t)(const char *weights_path)
Definition ck_cli_v6.6.c:70
float *(* get_logits_t)(void)
Definition ck_cli_v6.6.c:77
static bool detect_gpt2_byte_fallback(CKTrueBPE *tokenizer, int vocab_size)
static bool resolve_symbol(void *handle, const char *name, void **out_ptr, bool required)
static double g_decode_time_ms
Definition ck_cli_v6.6.c:52
static void handle_sigint(int sig)
Definition ck_cli_v6.6.c:57
int(* embed_t)(const int32_t *tokens, int num_tokens)
Definition ck_cli_v6.6.c:71
int(* kv_enable_t)(int capacity)
Definition ck_cli_v6.6.c:73
static int sample_top_p(float *logits, int vocab_size, float temperature, float top_p)
static bool load_eos_from_vocab_json(const char *weights_path, CLIOptions *opt)
ChatTemplateType
@ CHAT_TEMPLATE_LLAMA
@ CHAT_TEMPLATE_MISTRAL
@ CHAT_TEMPLATE_QWEN
@ CHAT_TEMPLATE_CHATML
@ CHAT_TEMPLATE_NONE
static double g_prefill_time_ms
Definition ck_cli_v6.6.c:51
static bool find_model_in_cache(const char *model_name, char *lib_out, char *weights_out, size_t out_size)
int main(int argc, char **argv)
static void print_help(const char *prog)
#define CK_CLI_EOS_MAX
Definition ck_cli_v6.6.c:42
static ChatTemplateType detect_chat_template(const char *model_name)
static char * apply_chat_template(const ChatTemplate *tmpl, const char *system, const char *user)
void(* free_t)(void)
Definition ck_cli_v6.6.c:80
#define CK_CLI_HISTORY_FILE
Definition ck_cli_v6.6.c:45
static int g_decode_count
Definition ck_cli_v6.6.c:53
static bool process_repl_command(const char *line, CLIOptions *opt, ModelAPI *api)
static bool is_eos_token(const CLIOptions *opt, int token)
static bool eos_is_potential_prefix(const char *token)
int(* forward_t)(float *logits_out)
Definition ck_cli_v6.6.c:72
#define EOS_PENDING_MAX
static void eos_pattern_init(ChatTemplateType tmpl)
static void output_append(char *buf, size_t *len, const char *text)
static void list_available_models(void)
static volatile sig_atomic_t g_generation_active
Definition ck_cli_v6.6.c:48
#define CK_CLI_MAX_CONTEXT
Definition ck_cli_v6.6.c:44
static int decode_bpe_token(const char *token, char *out, int max)
int(* get_int_t)(void)
Definition ck_cli_v6.6.c:78
static bool g_use_byte_decoder
Definition ck_cli_v6.6.c:55
int(* decode_t)(int32_t token, float *logits_out)
Definition ck_cli_v6.6.c:75
#define EOS_PATTERN_BUF_SIZE
static void print_banner(void)
static bool parse_args(int argc, char **argv, CLIOptions *opt)
static int run_prompt(ModelAPI *api, CKTrueBPE *tokenizer, CLIOptions *opt, const char *input)
static void eos_pattern_reset(void)
static volatile sig_atomic_t g_exit_requested
Definition ck_cli_v6.6.c:47
static EOSPatternState g_eos_state
#define CK_CLI_OUTPUT_BUF_SIZE
Definition ck_cli_v6.6.c:43
#define CK_CLI_DEFAULT_MAX_TOKENS
Definition ck_cli_v6.6.c:41
static void output_flush(char *buf, size_t *len)
static const ChatTemplate g_templates[]
static bool load_model_api(const char *lib_path, ModelAPI *api)
#define SAMPLE_NEXT_TOKEN()
static void output_token(char *buf, size_t *len, const char *token)
static bool eos_pattern_process(const char *token_text, char *out_buf, size_t *out_len, void(*output_fn)(char *, size_t *, const char *), ChatTemplateType tmpl)
static const char * get_cache_dir(void)
#define CK_CLI_VERSION
Definition ck_cli_v6.6.c:40
static bool token_has_gpt2_bytes(const char *token)
void *(* get_ptr_t)(void)
Definition ck_cli_v6.6.c:79
int(* sample_argmax_t)(void)
Definition ck_cli_v6.6.c:76
static int g_prompt_tokens
Definition ck_cli_v6.6.c:54
CPU feature detection and dispatch macros.
static ck_capability_t ck_get_capabilities(void)
Get current platform capabilities.
CPU capability information structure.
const int32_t * ids
Definition tokenizer.h:444
const char * text
Definition tokenizer.h:564
const char * token
Definition tokenizer.h:307
int32_t int32_t int32_t eos
Definition tokenizer.h:232
int32_t int32_t bos
Definition tokenizer.h:231
const int32_t int int * out_len
Definition tokenizer.h:446
int ck_true_bpe_encode(CKTrueBPE *bpe, const char *text, int text_len, int32_t *ids, int max_ids)
Definition true_bpe.c:1395
void ck_true_bpe_free(CKTrueBPE *bpe)
Definition true_bpe.c:406
CKTrueBPE * ck_true_bpe_create(void)
Definition true_bpe.c:342
int ck_true_bpe_add_special_token(CKTrueBPE *bpe, const char *token, int32_t id)
Definition true_bpe.c:566
const char * ck_true_bpe_id_to_token(const CKTrueBPE *bpe, int32_t id)
Definition true_bpe.c:651
int ck_true_bpe_load_binary(CKTrueBPE *bpe, int vocab_size, const int32_t *offsets, const char *strings, int num_merges, const int32_t *merges)
Definition true_bpe.c:607
int32_t ck_true_bpe_lookup(const CKTrueBPE *bpe, const char *token)
Definition true_bpe.c:645
int const int32_t const char int num_merges
Definition true_bpe.h:196
int const int32_t const char * strings
Definition true_bpe.h:195
int const int32_t const char int const int32_t * merges
Definition true_bpe.h:197
int vocab_size
Definition true_bpe.h:193
int const int32_t * offsets
Definition true_bpe.h:194
uint32_t end
Definition utf8.c:215