1 module test_bindings; 2 3 import llama; 4 5 // --------------------------------------------------------------------------- 6 // Compile-time constant checks 7 // --------------------------------------------------------------------------- 8 9 @("LLAMA constants match header values") 10 unittest 11 { 12 static assert(LLAMA_DEFAULT_SEED == 0xFFFF_FFFFu); 13 static assert(LLAMA_TOKEN_NULL == -1); 14 static assert(LLAMA_SESSION_VERSION == 9); 15 } 16 17 @("GGML constants match header values") 18 unittest 19 { 20 static assert(GGML_MAX_DIMS == 4); 21 static assert(GGML_MAX_SRC == 10); 22 static assert(GGML_MAX_NAME == 64); 23 static assert(GGML_ROPE_TYPE_NEOX == 2); 24 static assert(GGML_ROPE_TYPE_MROPE == 8); 25 static assert(GGML_ROPE_TYPE_VISION == 24); 26 } 27 28 // --------------------------------------------------------------------------- 29 // Symbol reachability — core API 30 // --------------------------------------------------------------------------- 31 32 @("llama_* core symbols are reachable") 33 unittest 34 { 35 auto _0 = &llama_backend_init; 36 auto _1 = &llama_backend_free; 37 auto _2 = &llama_model_load_from_file; 38 auto _3 = &llama_model_free; 39 auto _4 = &llama_init_from_model; 40 auto _5 = &llama_free; 41 auto _6 = &llama_decode; 42 auto _7 = &llama_encode; 43 auto _8 = &llama_get_logits_ith; 44 auto _9 = &llama_tokenize; 45 auto _10 = &llama_detokenize; 46 auto _11 = &llama_token_to_piece; 47 auto _12 = &llama_model_get_vocab; 48 auto _13 = &llama_vocab_bos; 49 auto _14 = &llama_vocab_eos; 50 auto _15 = &llama_vocab_is_eog; 51 auto _16 = &llama_sampler_chain_init; 52 auto _17 = &llama_sampler_chain_add; 53 auto _18 = &llama_sampler_init_greedy; 54 auto _19 = &llama_sampler_sample; 55 auto _20 = &llama_sampler_free; 56 auto _21 = &llama_batch_get_one; 57 auto _22 = &llama_perf_context_print; 58 auto _23 = &llama_perf_sampler_print; 59 auto _24 = &llama_model_n_embd; 60 auto _25 = &llama_model_n_layer; 61 auto _26 = &llama_model_has_encoder; 62 auto _27 = &llama_model_has_decoder; 63 } 64 65 @("llama_* extended API symbols are reachable") 66 unittest 67 { 68 // Context / memory / state 69 auto _0 = &llama_get_memory; 70 auto _1 = &llama_memory_seq_rm; 71 auto _2 = &llama_memory_seq_cp; 72 auto _3 = &llama_memory_seq_keep; 73 auto _4 = &llama_memory_seq_add; 74 auto _5 = &llama_memory_seq_div; 75 auto _6 = &llama_memory_seq_pos_min; 76 auto _7 = &llama_memory_seq_pos_max; 77 auto _8 = &llama_memory_can_shift; 78 auto _9 = &llama_state_get_size; 79 auto _10 = &llama_state_get_data; 80 auto _11 = &llama_state_set_data; 81 auto _12 = &llama_state_load_file; 82 auto _13 = &llama_state_save_file; 83 // Embeddings 84 auto _14 = &llama_get_embeddings; 85 auto _15 = &llama_get_embeddings_ith; 86 auto _16 = &llama_get_embeddings_seq; 87 // Chat 88 auto _17 = &llama_chat_apply_template; 89 auto _18 = &llama_chat_builtin_templates; 90 // Adapter / LoRA 91 auto _19 = &llama_adapter_lora_init; 92 auto _20 = &llama_adapter_lora_free; 93 auto _21 = &llama_set_adapters_lora; 94 // Model metadata 95 auto _22 = &llama_model_n_ctx_train; 96 auto _23 = &llama_model_n_params; 97 auto _24 = &llama_model_size; 98 auto _25 = &llama_model_desc; 99 auto _26 = &llama_model_chat_template; 100 auto _27 = &llama_model_is_recurrent; 101 auto _28 = &llama_model_meta_count; 102 auto _29 = &llama_model_meta_key_by_index; 103 auto _30 = &llama_model_meta_val_str; 104 auto _31 = &llama_model_meta_val_str_by_index; 105 // Vocab extras 106 auto _32 = &llama_vocab_nl; 107 auto _33 = &llama_vocab_pad; 108 auto _34 = &llama_vocab_sep; 109 auto _35 = &llama_vocab_fim_pre; 110 auto _36 = &llama_vocab_fim_suf; 111 auto _37 = &llama_vocab_fim_mid; 112 auto _38 = &llama_vocab_fim_pad; 113 auto _39 = &llama_vocab_fim_rep; 114 auto _40 = &llama_vocab_fim_sep; 115 auto _41 = &llama_vocab_get_text; 116 auto _42 = &llama_vocab_get_score; 117 auto _43 = &llama_vocab_get_attr; 118 auto _44 = &llama_vocab_is_control; 119 // Samplers 120 auto _45 = &llama_sampler_init_penalties; 121 auto _46 = &llama_sampler_init_typical; 122 auto _47 = &llama_sampler_init_temp_ext; 123 auto _48 = &llama_sampler_init_top_n_sigma; 124 auto _49 = &llama_sampler_init_xtc; 125 auto _50 = &llama_sampler_init_mirostat_v2; 126 auto _51 = &llama_sampler_init_grammar; 127 auto _52 = &llama_sampler_init_dry; 128 auto _53 = &llama_sampler_init_logit_bias; 129 } 130 131 @("ggml_* symbols are reachable") 132 unittest 133 { 134 auto _0 = &ggml_time_us; 135 auto _1 = &ggml_backend_load_all; 136 } 137 138 // --------------------------------------------------------------------------- 139 // SamplerChain: chain construction (no model required) 140 // --------------------------------------------------------------------------- 141 142 @("SamplerChain: basic creation — greedy and noPerf variants") 143 unittest 144 { 145 { 146 auto smpl = SamplerChain.create(); 147 smpl.greedy(); 148 } 149 { 150 auto smpl = SamplerChain.create(/*noPerf=*/true); 151 smpl.greedy(); 152 assert(smpl.ptr !is null); 153 } 154 } 155 156 @("SamplerChain: temp + topK + topP + dist") 157 unittest 158 { 159 auto smpl = SamplerChain.create(); 160 smpl.temp(0.8f).topK(40).topP(0.95f).dist(); 161 } 162 163 @("SamplerChain: temp + minP + dist with explicit seed") 164 unittest 165 { 166 auto smpl = SamplerChain.create(); 167 smpl.temp(1.0f).minP(0.05f).dist(42u); 168 } 169 170 @("SamplerChain: penalties — normal and disabled (zero lastN)") 171 unittest 172 { 173 { 174 auto smpl = SamplerChain.create(); 175 smpl.penalties(64, 1.1f, 0.0f, 0.0f).dist(); 176 } 177 { 178 auto smpl = SamplerChain.create(); 179 smpl.penalties(0).dist(); // penalty_last_n=0 disables the sampler 180 } 181 } 182 183 @("SamplerChain: typical") 184 unittest 185 { 186 auto smpl = SamplerChain.create(); 187 smpl.temp(1.0f).typical(0.95f).dist(); 188 } 189 190 @("SamplerChain: tempExt (dynamic temperature)") 191 unittest 192 { 193 auto smpl = SamplerChain.create(); 194 smpl.tempExt(0.8f, 0.5f, 1.5f).dist(); 195 } 196 197 @("SamplerChain: topNSigma") 198 unittest 199 { 200 auto smpl = SamplerChain.create(); 201 smpl.topNSigma(2.0f).dist(); 202 } 203 204 @("SamplerChain: xtc") 205 unittest 206 { 207 auto smpl = SamplerChain.create(); 208 smpl.xtc(0.1f, 0.1f, 1, LLAMA_DEFAULT_SEED).dist(); 209 } 210 211 @("SamplerChain: mirostatV2") 212 unittest 213 { 214 auto smpl = SamplerChain.create(); 215 smpl.mirostatV2(5.0f, 0.1f, LLAMA_DEFAULT_SEED); 216 } 217 218 @("SamplerChain: logitBias — empty and with explicit entries") 219 @trusted unittest 220 { 221 { 222 auto smpl = SamplerChain.create(); 223 smpl.logitBias(32_000, []).dist(); 224 } 225 { 226 auto smpl = SamplerChain.create(); 227 llama_logit_bias[2] biases; 228 biases[0].token = 1; biases[0].bias = -100.0f; 229 biases[1].token = 2; biases[1].bias = 10.0f; 230 smpl.logitBias(32_000, biases[]).dist(); 231 } 232 } 233 234 @("SamplerChain: combined chain — penalties + typical + tempExt") 235 unittest 236 { 237 auto smpl = SamplerChain.create(); 238 smpl.penalties(64, 1.1f).typical(0.9f).tempExt(0.8f, 0.0f, 1.0f).dist(); 239 } 240 241 // --------------------------------------------------------------------------- 242 // Batch helpers 243 // --------------------------------------------------------------------------- 244 245 @("batchGetOne: multi-token and single-token slices") 246 unittest 247 { 248 { 249 llama_token[4] toks = [1, 2, 3, 4]; 250 llama_batch b = batchGetOne(toks[]); 251 assert(b.n_tokens == 4); 252 assert(b.token !is null); 253 } 254 { 255 llama_token[1] tok = [42]; 256 llama_batch b = batchGetOne(tok[]); 257 assert(b.n_tokens == 1); 258 } 259 } 260 261 @("allocBatch: token batch and embedding batch") 262 unittest 263 { 264 { 265 auto ob = allocBatch(512); 266 assert(ob.get().token !is null); 267 } 268 { 269 auto ob = allocBatch(64, 128); 270 assert(ob.get().embd !is null); 271 } 272 } 273 274 @("batchClear: resets n_tokens to zero") 275 unittest 276 { 277 auto ob = allocBatch(8); 278 batchAdd(ob.get(), 1, 0, 0, true); 279 batchAdd(ob.get(), 2, 1, 0, false); 280 assert(ob.get().n_tokens == 2); 281 batchClear(ob.get()); 282 assert(ob.get().n_tokens == 0); 283 } 284 285 @("batchAdd: populates token, pos, seq_id, logits fields") 286 unittest 287 { 288 auto ob = allocBatch(4); 289 batchAdd(ob.get(), 42, 7, 3, true); 290 ref llama_batch b = ob.get(); 291 assert(b.n_tokens == 1); 292 assert(b.token[0] == 42); 293 assert(b.pos[0] == 7); 294 assert(b.n_seq_id[0] == 1); 295 assert(b.seq_id[0][0] == 3); 296 assert(b.logits[0] == true); 297 } 298 299 // --------------------------------------------------------------------------- 300 // Backend 301 // --------------------------------------------------------------------------- 302 303 @("loadAllBackends: idempotent — safe to call multiple times") 304 unittest 305 { 306 loadAllBackends(); 307 loadAllBackends(); 308 } 309 310 // --------------------------------------------------------------------------- 311 // LlamaModel 312 // --------------------------------------------------------------------------- 313 314 @("LlamaModel: loadFromFile with nonexistent path returns falsy") 315 unittest 316 { 317 auto model = LlamaModel.loadFromFile("/nonexistent/model.gguf"); 318 assert(!model); 319 } 320 321 @("LlamaModel: new properties compile") 322 unittest 323 { 324 static assert(__traits(hasMember, LlamaModel, "isRecurrent")); 325 static assert(__traits(hasMember, LlamaModel, "nCtxTrain")); 326 static assert(__traits(hasMember, LlamaModel, "nParams")); 327 static assert(__traits(hasMember, LlamaModel, "size")); 328 static assert(__traits(hasMember, LlamaModel, "desc")); 329 static assert(__traits(hasMember, LlamaModel, "chatTemplate")); 330 static assert(__traits(hasMember, LlamaModel, "metaCount")); 331 static assert(__traits(hasMember, LlamaModel, "metaKeyAt")); 332 static assert(__traits(hasMember, LlamaModel, "metaValAt")); 333 static assert(__traits(hasMember, LlamaModel, "metaVal")); 334 } 335 336 // --------------------------------------------------------------------------- 337 // LlamaContext 338 // --------------------------------------------------------------------------- 339 340 @("LlamaContext: new properties compile") 341 unittest 342 { 343 static assert(__traits(hasMember, LlamaContext, "poolingType")); 344 static assert(__traits(hasMember, LlamaContext, "memory")); 345 static assert(__traits(hasMember, LlamaContext, "memoryClear")); 346 static assert(__traits(hasMember, LlamaContext, "getEmbeddings")); 347 static assert(__traits(hasMember, LlamaContext, "getEmbeddingsIth")); 348 static assert(__traits(hasMember, LlamaContext, "getEmbeddingsSeq")); 349 static assert(__traits(hasMember, LlamaContext, "stateGetSize")); 350 static assert(__traits(hasMember, LlamaContext, "stateGetData")); 351 static assert(__traits(hasMember, LlamaContext, "stateSetData")); 352 static assert(__traits(hasMember, LlamaContext, "stateSaveFile")); 353 static assert(__traits(hasMember, LlamaContext, "stateLoadFile")); 354 static assert(__traits(hasMember, LlamaContext, "stateSeqGetSize")); 355 static assert(__traits(hasMember, LlamaContext, "stateSeqGetData")); 356 static assert(__traits(hasMember, LlamaContext, "stateSeqSetData")); 357 } 358 359 // --------------------------------------------------------------------------- 360 // Owned mixin — structural checks 361 // --------------------------------------------------------------------------- 362 363 @("Owned mixin: injected members present on wrapper structs") 364 unittest 365 { 366 import llama.owned; 367 // ptr() property 368 static assert(__traits(hasMember, LlamaModel, "ptr")); 369 static assert(__traits(hasMember, LlamaContext, "ptr")); 370 static assert(__traits(hasMember, SamplerChain, "ptr")); 371 // bool conversion via opCast 372 static assert(__traits(compiles, { auto m = LlamaModel.loadFromFile("/x"); bool b = cast(bool) m; })); 373 // not copyable 374 static assert(!__traits(compiles, { auto m = LlamaModel.loadFromFile("/x"); auto m2 = m; })); 375 } 376 377 // --------------------------------------------------------------------------- 378 // Vocab helpers 379 // --------------------------------------------------------------------------- 380 381 @("tokenize: empty string returns null without touching C") 382 unittest 383 { 384 const(llama_vocab)* nullVocab = null; 385 assert(tokenize(nullVocab, "") is null); 386 } 387 388 @("detokenize: empty token slice returns empty string without touching C") 389 unittest 390 { 391 const(llama_vocab)* nullVocab = null; 392 assert(detokenize(nullVocab, []) == ""); 393 } 394 395 @("vocab: new helper function symbols compile") 396 unittest 397 { 398 static assert(__traits(compiles, &nlToken)); 399 static assert(__traits(compiles, &padToken)); 400 static assert(__traits(compiles, &sepToken)); 401 static assert(__traits(compiles, &fimPreToken)); 402 static assert(__traits(compiles, &fimSufToken)); 403 static assert(__traits(compiles, &fimMidToken)); 404 static assert(__traits(compiles, &fimPadToken)); 405 static assert(__traits(compiles, &fimRepToken)); 406 static assert(__traits(compiles, &fimSepToken)); 407 static assert(__traits(compiles, &vocabType)); 408 static assert(__traits(compiles, &tokenText)); 409 static assert(__traits(compiles, &tokenScore)); 410 static assert(__traits(compiles, &tokenAttr)); 411 static assert(__traits(compiles, &isControl)); 412 } 413 414 // --------------------------------------------------------------------------- 415 // chat.d 416 // --------------------------------------------------------------------------- 417 418 @("chat: llama_chat_message struct layout") 419 @trusted unittest 420 { 421 import llama.chat; 422 static assert(is(llama_chat_message == struct)); 423 llama_chat_message msg; 424 msg.role = "user"; 425 msg.content = "Hello"; 426 assert(msg.role !is null && msg.content !is null); 427 } 428 429 @("chat: builtinTemplates returns a non-empty list of named templates") 430 unittest 431 { 432 import llama.chat : builtinTemplates; 433 auto names = builtinTemplates(); 434 assert(names.length > 0); 435 foreach (n; names) 436 assert(n.length > 0); 437 } 438 439 @("chat: chatApplyTemplate with a known built-in template name") 440 @trusted unittest 441 { 442 import llama.chat : builtinTemplates, chatApplyTemplate; 443 import std.string : toStringz; 444 auto names = builtinTemplates(); 445 assert(names.length > 0); 446 447 llama_chat_message[1] msgs; 448 msgs[0].role = "user"; 449 msgs[0].content = "Hello"; 450 451 char[1024] buf; 452 int n = chatApplyTemplate(names[0].toStringz, msgs[], false, buf[]); 453 assert(n > 0); 454 } 455 456 @("chat: applyTemplate returns a non-empty D string") 457 @trusted unittest 458 { 459 import llama.chat : builtinTemplates, applyTemplate; 460 import std.string : toStringz; 461 auto names = builtinTemplates(); 462 assert(names.length > 0); 463 464 llama_chat_message[2] msgs; 465 msgs[0].role = "user"; msgs[0].content = "What is 2+2?"; 466 msgs[1].role = "assistant"; msgs[1].content = "4"; 467 468 string result = applyTemplate(names[0].toStringz, msgs[], true); 469 assert(result.length > 0); 470 } 471 472 // --------------------------------------------------------------------------- 473 // adapter.d 474 // --------------------------------------------------------------------------- 475 476 @("adapter: types and functions compile") 477 unittest 478 { 479 import llama.adapter : LlamaAdapterLora, loadAdapterLora, setAdaptersLora; 480 static assert(is(LlamaAdapterLora == struct)); 481 static assert(__traits(compiles, &loadAdapterLora)); 482 static assert(__traits(compiles, &setAdaptersLora)); 483 } 484 485 @("adapter: loadAdapterLora with nonexistent path returns falsy") 486 unittest 487 { 488 import llama.adapter : loadAdapterLora; 489 loadAllBackends(); 490 auto model = LlamaModel.loadFromFile("/nonexistent/model.gguf"); 491 if (!model) return; // no model in CI — skip the rest 492 auto adapter = loadAdapterLora(model, "/nonexistent/lora.gguf"); 493 assert(!adapter); 494 } 495 496 // --------------------------------------------------------------------------- 497 // mtmd — C binding symbol reachability 498 // --------------------------------------------------------------------------- 499 500 @("mtmd C symbols are reachable") 501 unittest 502 { 503 import llama.mtmd; 504 auto _0 = &mtmd_default_marker; 505 auto _1 = &mtmd_context_params_default; 506 auto _2 = &mtmd_init_from_file; 507 auto _3 = &mtmd_free; 508 auto _4 = &mtmd_bitmap_init; 509 auto _5 = &mtmd_bitmap_init_from_audio; 510 auto _6 = &mtmd_bitmap_free; 511 auto _7 = &mtmd_input_chunks_init; 512 auto _8 = &mtmd_input_chunks_size; 513 auto _9 = &mtmd_input_chunks_free; 514 auto _10 = &mtmd_tokenize; 515 auto _11 = &mtmd_encode_chunk; 516 auto _12 = &mtmd_get_output_embd; 517 auto _13 = &mtmd_helper_bitmap_init_from_file; 518 auto _14 = &mtmd_helper_bitmap_init_from_buf; 519 auto _15 = &mtmd_helper_get_n_tokens; 520 auto _16 = &mtmd_helper_get_n_pos; 521 auto _17 = &mtmd_helper_eval_chunks; 522 } 523 524 @("mtmd_context_params_default: n_threads is non-negative") 525 unittest 526 { 527 import llama.mtmd : mtmd_context_params_default; 528 auto p = mtmd_context_params_default(); 529 assert(p.n_threads >= 0); 530 } 531 532 // --------------------------------------------------------------------------- 533 // MtmdBitmap wrapper 534 // --------------------------------------------------------------------------- 535 536 @("MtmdBitmap: fromRGB creates bitmap with correct dimensions and data length") 537 unittest 538 { 539 import llama.mtmd : MtmdBitmap; 540 ubyte[12] rgb; // 2×2 RGB 541 auto bmp = MtmdBitmap.fromRGB(2, 2, rgb[]); 542 assert(cast(bool) bmp); 543 assert(bmp.nx == 2 && bmp.ny == 2); 544 assert(!bmp.isAudio); 545 assert(bmp.data.length == 12); 546 assert(bmp.ptr !is null); 547 } 548 549 @("MtmdBitmap: fromAudio creates audio bitmap") 550 unittest 551 { 552 import llama.mtmd : MtmdBitmap; 553 float[16_000] pcm; 554 auto bmp = MtmdBitmap.fromAudio(pcm[]); 555 assert(cast(bool) bmp && bmp.isAudio); 556 } 557 558 // --------------------------------------------------------------------------- 559 // InputChunks wrapper + range 560 // --------------------------------------------------------------------------- 561 562 @("InputChunks: empty list — length, empty, nTokens, nPos, iteration") 563 unittest 564 { 565 import llama.mtmd : InputChunks; 566 auto chunks = InputChunks.create(); 567 assert(chunks.length == 0); 568 assert(chunks.empty); 569 assert(chunks.nTokens == 0); 570 assert(chunks.nPos == 0); 571 int count; 572 foreach (chunk; chunks) count++; 573 assert(count == 0); 574 } 575 576 // --------------------------------------------------------------------------- 577 // MtmdContext wrapper 578 // --------------------------------------------------------------------------- 579 580 @("MtmdContext: initFromFile with nonexistent path returns falsy") 581 unittest 582 { 583 import llama.mtmd : MtmdContext; 584 auto ctx = MtmdContext.initFromFile("/nonexistent/mmproj.gguf", null); 585 assert(!ctx); 586 } 587 588 @("MtmdContext: default marker is a non-empty C string") 589 unittest 590 { 591 import llama.mtmd : mtmd_default_marker; 592 import core.stdc.string : strlen; 593 const(char)* m = mtmd_default_marker(); 594 assert(m !is null && strlen(m) > 0); 595 }