react-native-nobodywho 0.1.1 → 0.2.1

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.
package/Nobodywho.podspec CHANGED
@@ -43,10 +43,10 @@ Pod::Spec.new do |s|
43
43
  s.summary = package["description"]
44
44
  s.homepage = package["homepage"]
45
45
  s.license = package["license"]
46
- s.authors = package["author"]
46
+ s.authors = package["author"] || { "NobodyWho" => "info@nobodywho.ooo" }
47
47
 
48
48
  s.platforms = { :ios => min_ios_version_supported }
49
- s.source = { :git => "https://github.com/nobodywho-ooo/nobodywho", :tag => "#{s.version}" }
49
+ s.source = { :git => "https://github.com/nobodywho-ooo/nobodywho.git", :tag => "#{s.version}" }
50
50
 
51
51
  s.source_files = "ios/**/*.{h,m,mm,swift}", "ios/generated/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "generated/cpp/**/*.{hpp,cpp,c,h}"
52
52
  s.vendored_frameworks = framework_name
package/README.md CHANGED
@@ -30,13 +30,31 @@ This library uses the **GGUF format** — a binary format optimized for fast loa
30
30
  - iOS: iPhone 11 or newer with at least 4 GB of RAM.
31
31
  - Android: Snapdragon 855 / Adreno 640 / 6 GB RAM or better.
32
32
 
33
+ ## Model Loading
34
+
35
+ Models can be loaded from a local file path or downloaded automatically from HuggingFace:
36
+
37
+ ```typescript
38
+ import { Model } from "react-native-nobodywho";
39
+
40
+ // Download from HuggingFace (cached automatically)
41
+ const model = await Model.load({
42
+ modelPath: "hf://NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf",
43
+ });
44
+
45
+ // Or load from a local file
46
+ const model = await Model.load({ modelPath: "/path/to/model.gguf" });
47
+ ```
48
+
49
+ Downloaded models are cached on disk and reused on subsequent loads.
50
+
33
51
  ## Chat
34
52
 
35
53
  ```typescript
36
54
  import { Chat } from "react-native-nobodywho";
37
55
 
38
56
  const chat = await Chat.fromPath({
39
- modelPath: "/path/to/model.gguf",
57
+ modelPath: "hf://NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf",
40
58
  systemPrompt: "You are a helpful assistant.",
41
59
  });
42
60
 
@@ -249,7 +249,7 @@ void uniffi_nobodywho_uniffi_fn_init_callback_vtable_rusttoolcallback(
249
249
  float uniffi_nobodywho_uniffi_fn_func_cosine_similarity(
250
250
  RustBuffer a, RustBuffer b, RustCallStatus *uniffi_out_err);
251
251
  /*handle*/ uint64_t uniffi_nobodywho_uniffi_fn_func_load_model(
252
- RustBuffer model_path, int8_t use_gpu, RustBuffer image_model_path);
252
+ RustBuffer model_path, int8_t use_gpu, RustBuffer projection_model_path);
253
253
  /*handle*/ uint64_t uniffi_nobodywho_uniffi_fn_func_sampler_preset_default(
254
254
  RustCallStatus *uniffi_out_err);
255
255
  /*handle*/ uint64_t uniffi_nobodywho_uniffi_fn_func_sampler_preset_dry(
@@ -76,7 +76,7 @@ interface NativeModuleInterface {
76
76
  ubrn_uniffi_nobodywho_uniffi_fn_method_samplerconfig_to_json(ptr: bigint, uniffi_out_err: UniffiRustCallStatus): Uint8Array;
77
77
  ubrn_uniffi_nobodywho_uniffi_fn_init_callback_vtable_rusttoolcallback(vtable: UniffiVTableCallbackInterfaceRustToolCallback): void;
78
78
  ubrn_uniffi_nobodywho_uniffi_fn_func_cosine_similarity(a: Uint8Array, b: Uint8Array, uniffi_out_err: UniffiRustCallStatus): number;
79
- ubrn_uniffi_nobodywho_uniffi_fn_func_load_model(modelPath: Uint8Array, useGpu: number, imageModelPath: Uint8Array): bigint;
79
+ ubrn_uniffi_nobodywho_uniffi_fn_func_load_model(modelPath: Uint8Array, useGpu: number, projectionModelPath: Uint8Array): bigint;
80
80
  ubrn_uniffi_nobodywho_uniffi_fn_func_sampler_preset_default(uniffi_out_err: UniffiRustCallStatus): bigint;
81
81
  ubrn_uniffi_nobodywho_uniffi_fn_func_sampler_preset_dry(uniffi_out_err: UniffiRustCallStatus): bigint;
82
82
  ubrn_uniffi_nobodywho_uniffi_fn_func_sampler_preset_grammar(grammar: Uint8Array, uniffi_out_err: UniffiRustCallStatus): bigint;
@@ -95,18 +95,22 @@ export function cosineSimilarity(a: Array</*f32*/number>, b: Array</*f32*/number
95
95
  ));
96
96
  }
97
97
  /**
98
- * Load a GGUF model from disk.
98
+ * Load a GGUF model from a local path or remote URL.
99
+ *
100
+ * Accepts local filesystem paths, `hf://owner/repo/file.gguf` for HuggingFace downloads,
101
+ * or `https://` URLs. Downloaded models are cached automatically.
102
+ *
99
103
  * This is a free function instead of an async constructor because
100
104
  * uniffi-bindgen-react-native generates invalid JS (`async static` instead
101
105
  * of `static async`) for async constructors.
102
106
  */
103
- export async function loadModel(modelPath: string, useGpu: boolean, imageModelPath: string | undefined, asyncOpts_?: { signal: AbortSignal }): Promise<ModelInterface> /*throws*/ {
107
+ export async function loadModel(modelPath: string, useGpu: boolean, projectionModelPath: string | undefined, asyncOpts_?: { signal: AbortSignal }): Promise<ModelInterface> /*throws*/ {
104
108
  const __stack = uniffiIsDebug ? new Error().stack : undefined;
105
109
  try {
106
110
  return await uniffiRustCallAsync(
107
111
  /*rustCaller:*/ uniffiCaller,
108
112
  /*rustFutureFunc:*/ () => {
109
- return nativeModule().ubrn_uniffi_nobodywho_uniffi_fn_func_load_model(FfiConverterString.lower(modelPath),FfiConverterBool.lower(useGpu),FfiConverterOptionalString.lower(imageModelPath)
113
+ return nativeModule().ubrn_uniffi_nobodywho_uniffi_fn_func_load_model(FfiConverterString.lower(modelPath),FfiConverterBool.lower(useGpu),FfiConverterOptionalString.lower(projectionModelPath)
110
114
  );
111
115
  },
112
116
  /*pollFunc:*/ nativeModule().ubrn_ffi_nobodywho_uniffi_rust_future_poll_u64,
@@ -693,108 +697,76 @@ const FfiConverterTypeMessage = (() => {
693
697
 
694
698
 
695
699
 
696
- // Error type: NobodyWhoError
697
700
 
698
- // Enum: NobodyWhoError
701
+ // Flat error type: NobodyWhoError
699
702
  export enum NobodyWhoError_Tags {
700
703
  Error = "Error"
701
704
  }
702
705
  export const NobodyWhoError = (() => {
703
-
704
-
705
- type Error__interface = {
706
- tag: NobodyWhoError_Tags.Error;
707
- inner: Readonly<{message: string}>
708
- };
709
-
710
-
711
- class Error_ extends UniffiError implements Error__interface {
706
+ class Error extends UniffiError {
712
707
  /**
713
708
  * @private
714
- * This field is private and should not be used, use `tag` instead.
709
+ * This field is private and should not be used.
715
710
  */
716
- readonly [uniffiTypeNameSymbol] = "NobodyWhoError";
717
- readonly tag = NobodyWhoError_Tags.Error;
718
- readonly inner: Readonly<{message: string}>;
719
- constructor(inner: { message: string }) {
720
- super("NobodyWhoError", "Error");
721
- this.inner = Object.freeze(inner);
722
- }
723
-
724
- static new(inner: { message: string }): Error_ {
725
- return new Error_(inner);
726
- }
711
+ readonly [uniffiTypeNameSymbol]: string = "NobodyWhoError";
712
+ /**
713
+ * @private
714
+ * This field is private and should not be used.
715
+ */
716
+ readonly [variantOrdinalSymbol] = 1;
727
717
 
728
- static instanceOf(obj: any): obj is Error_ {
729
- return obj.tag === NobodyWhoError_Tags.Error;
730
- }
731
-
718
+ readonly tag = NobodyWhoError_Tags.Error;
732
719
 
733
-
734
- static hasInner(obj: any): obj is Error_ {
735
- return Error_.instanceOf(obj);
720
+ constructor(message: string) {
721
+ super("NobodyWhoError", "Error", message);
736
722
  }
737
723
 
738
- static getInner(obj: Error_): Readonly<{message: string}> {
739
- return obj.inner;
724
+ static instanceOf(e: any): e is Error {
725
+ return (
726
+ instanceOf(e) && (e as any)[variantOrdinalSymbol] === 1
727
+ );
740
728
  }
741
-
742
729
  }
743
730
 
744
- function instanceOf(obj: any): obj is NobodyWhoError {
745
- return obj[uniffiTypeNameSymbol] === "NobodyWhoError";
731
+ // Utility function which does not rely on instanceof.
732
+ function instanceOf(e: any): e is NobodyWhoError {
733
+ return (e as any)[uniffiTypeNameSymbol] === "NobodyWhoError";
746
734
  }
747
-
748
- return Object.freeze({
735
+ return {
736
+ Error,
749
737
  instanceOf,
750
- Error: Error_
751
- });
752
-
738
+ };
753
739
  })();
754
740
 
741
+ // Union type for NobodyWhoError error type.
755
742
 
756
743
 
757
744
  export type NobodyWhoError = InstanceType<
758
745
  typeof NobodyWhoError[keyof Omit<typeof NobodyWhoError, 'instanceOf'>]
759
746
  >;
760
747
 
761
- // FfiConverter for enum NobodyWhoError
762
748
  const FfiConverterTypeNobodyWhoError = (() => {
763
- const ordinalConverter = FfiConverterInt32;
749
+ const intConverter = FfiConverterInt32;
764
750
  type TypeName = NobodyWhoError;
765
- class FFIConverter extends AbstractFfiConverterByteArray<TypeName> {
751
+ class FfiConverter extends AbstractFfiConverterByteArray<TypeName> {
766
752
  read(from: RustBuffer): TypeName {
767
- switch (ordinalConverter.read(from)) {
768
- case 1: return new NobodyWhoError.Error({message: FfiConverterString.read(from) });
753
+ switch (intConverter.read(from)) {
754
+ case 1: return new NobodyWhoError.Error(FfiConverterString.read(from)
755
+ );
756
+
769
757
  default: throw new UniffiInternalError.UnexpectedEnumCase();
770
758
  }
771
759
  }
772
760
  write(value: TypeName, into: RustBuffer): void {
773
- switch (value.tag) {
774
- case NobodyWhoError_Tags.Error: {
775
- ordinalConverter.write(1, into);
776
- const inner = value.inner;
777
- FfiConverterString.write(inner.message, into);
778
- return;
779
- }
780
- default:
781
- // Throwing from here means that NobodyWhoError_Tags hasn't matched an ordinal.
782
- throw new UniffiInternalError.UnexpectedEnumCase();
783
- }
761
+ const obj = value as any;
762
+ const index = obj[variantOrdinalSymbol] as number;
763
+ intConverter.write(index, into);
784
764
  }
785
765
  allocationSize(value: TypeName): number {
786
- switch (value.tag) {
787
- case NobodyWhoError_Tags.Error: {
788
- const inner = value.inner;
789
- let size = ordinalConverter.allocationSize(1);
790
- size += FfiConverterString.allocationSize(inner.message);
791
- return size;
792
- }
793
- default: throw new UniffiInternalError.UnexpectedEnumCase();
794
- }
766
+ return intConverter.allocationSize(0);
795
767
  }
796
768
  }
797
- return new FFIConverter();
769
+ return new FfiConverter();
798
770
  })();
799
771
 
800
772
 
@@ -2872,7 +2844,7 @@ function uniffiEnsureInitialized() {
2872
2844
  if (nativeModule().ubrn_uniffi_nobodywho_uniffi_checksum_func_cosine_similarity() !== 63439) {
2873
2845
  throw new UniffiInternalError.ApiChecksumMismatch("uniffi_nobodywho_uniffi_checksum_func_cosine_similarity");
2874
2846
  }
2875
- if (nativeModule().ubrn_uniffi_nobodywho_uniffi_checksum_func_load_model() !== 19243) {
2847
+ if (nativeModule().ubrn_uniffi_nobodywho_uniffi_checksum_func_load_model() !== 57455) {
2876
2848
  throw new UniffiInternalError.ApiChecksumMismatch("uniffi_nobodywho_uniffi_checksum_func_load_model");
2877
2849
  }
2878
2850
  if (nativeModule().ubrn_uniffi_nobodywho_uniffi_checksum_func_sampler_preset_default() !== 10834) {
package/ios/Nobodywho.h CHANGED
@@ -4,7 +4,7 @@
4
4
  #endif
5
5
 
6
6
  #ifdef RCT_NEW_ARCH_ENABLED
7
- #import "RNNativeModuleSpec.h"
7
+ #import "NobodywhoSpec.h"
8
8
 
9
9
  @interface Nobodywho : NSObject <NativeNobodywhoSpec>
10
10
  #else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nobodywho",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Run LLMs locally with offline inference — React Native bindings",
5
5
  "main": "src/wrapper.ts",
6
6
  "types": "src/wrapper.ts",
package/src/chat.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import {
2
2
  RustChat,
3
- loadModel,
4
- type ModelInterface,
5
- type SamplerConfigInterface,
6
- type Message,
3
+ SamplerConfig,
7
4
  } from "../generated/ts/nobodywho";
5
+ import { Model } from "./model";
6
+ import { type ChatMessage, fromInternal, toInternal } from "./message";
8
7
  import { TokenStream } from "./streaming";
9
8
  import type { Prompt } from "./prompt";
10
9
  import type { Tool } from "./tool";
@@ -17,7 +16,7 @@ import type { Tool } from "./tool";
17
16
  *
18
17
  * @example
19
18
  * ```typescript
20
- * const model = await loadModel("model.gguf", true);
19
+ * const model = await Model.load({ modelPath: "model.gguf" });
21
20
  * const chat = new Chat({
22
21
  * model,
23
22
  * systemPrompt: "You are a helpful assistant.",
@@ -32,15 +31,15 @@ export class Chat {
32
31
  private readonly _inner: RustChat;
33
32
 
34
33
  constructor(opts: {
35
- model: ModelInterface;
34
+ model: Model;
36
35
  systemPrompt?: string;
37
36
  contextSize?: number;
38
37
  templateVariables?: Map<string, boolean>;
39
38
  tools?: Tool[];
40
- sampler?: SamplerConfigInterface;
39
+ sampler?: SamplerConfig;
41
40
  }) {
42
41
  this._inner = new RustChat(
43
- opts.model,
42
+ opts.model._inner,
44
43
  opts.systemPrompt ?? undefined,
45
44
  opts.contextSize ?? 4096,
46
45
  opts.templateVariables ?? undefined,
@@ -64,18 +63,18 @@ export class Chat {
64
63
  static async fromPath(opts: {
65
64
  modelPath: string;
66
65
  useGpu?: boolean;
67
- imageModelPath?: string;
66
+ projectionModelPath?: string;
68
67
  systemPrompt?: string;
69
68
  contextSize?: number;
70
69
  templateVariables?: Map<string, boolean>;
71
70
  tools?: Tool[];
72
- sampler?: SamplerConfigInterface;
71
+ sampler?: SamplerConfig;
73
72
  }): Promise<Chat> {
74
- const model = await loadModel(
75
- opts.modelPath,
76
- opts.useGpu ?? true,
77
- opts.imageModelPath,
78
- );
73
+ const model = await Model.load({
74
+ modelPath: opts.modelPath,
75
+ useGpu: opts.useGpu,
76
+ projectionModelPath: opts.projectionModelPath,
77
+ });
79
78
  return new Chat({ model, ...opts });
80
79
  }
81
80
 
@@ -109,13 +108,14 @@ export class Chat {
109
108
  }
110
109
 
111
110
  /** Get the current chat history as a list of messages. */
112
- async getChatHistory(): Promise<Message[]> {
113
- return this._inner.getChatHistory();
111
+ async getChatHistory(): Promise<ChatMessage[]> {
112
+ const internal = await this._inner.getChatHistory();
113
+ return internal.map(fromInternal);
114
114
  }
115
115
 
116
116
  /** Set the chat history from a list of messages. */
117
- async setChatHistory(messages: Message[]): Promise<void> {
118
- return this._inner.setChatHistory(messages);
117
+ async setChatHistory(messages: ChatMessage[]): Promise<void> {
118
+ return this._inner.setChatHistory(messages.map(toInternal));
119
119
  }
120
120
 
121
121
  /** Get the current system prompt. */
@@ -144,7 +144,7 @@ export class Chat {
144
144
  }
145
145
 
146
146
  /** Set the sampler configuration. */
147
- async setSamplerConfig(sampler: SamplerConfigInterface): Promise<void> {
147
+ async setSamplerConfig(sampler: SamplerConfig): Promise<void> {
148
148
  return this._inner.setSamplerConfig(sampler);
149
149
  }
150
150
 
@@ -152,4 +152,12 @@ export class Chat {
152
152
  async getSamplerConfigJson(): Promise<string> {
153
153
  return this._inner.getSamplerConfigJson();
154
154
  }
155
+
156
+ /**
157
+ * Immediately free the underlying Rust resources (model context, KV cache, etc.).
158
+ * After calling this, the Chat instance is no longer usable.
159
+ */
160
+ destroy(): void {
161
+ this._inner.uniffiDestroy();
162
+ }
155
163
  }
@@ -0,0 +1,67 @@
1
+ import { CrossEncoder as RustCrossEncoder } from "../generated/ts/nobodywho";
2
+ import { Model } from "./model";
3
+
4
+ /**
5
+ * A cross-encoder for ranking documents by relevance to a query.
6
+ *
7
+ * Wraps the internal CrossEncoder with an ergonomic API including
8
+ * a `fromPath` factory for one-step model loading.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const crossEncoder = await CrossEncoder.fromPath({
13
+ * modelPath: "crossencoder.gguf",
14
+ * });
15
+ * const scores = await crossEncoder.rank("query", ["doc1", "doc2"]);
16
+ * const sorted = await crossEncoder.rankAndSort("query", ["doc1", "doc2"]);
17
+ * ```
18
+ */
19
+ export class CrossEncoder {
20
+ /** @internal */
21
+ private readonly _inner: RustCrossEncoder;
22
+
23
+ constructor(opts: { model: Model; contextSize?: number }) {
24
+ this._inner = new RustCrossEncoder(
25
+ opts.model._inner,
26
+ opts.contextSize ?? undefined,
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Create a cross-encoder directly from a model path.
32
+ * Loads the model and creates the cross-encoder in one step.
33
+ */
34
+ static async fromPath(opts: {
35
+ modelPath: string;
36
+ useGpu?: boolean;
37
+ contextSize?: number;
38
+ }): Promise<CrossEncoder> {
39
+ const model = await Model.load({ modelPath: opts.modelPath, useGpu: opts.useGpu });
40
+ return new CrossEncoder({ model, ...opts });
41
+ }
42
+
43
+ /** Rank documents by relevance to a query. Returns similarity scores. */
44
+ async rank(query: string, documents: string[]): Promise<number[]> {
45
+ return this._inner.rank(query, documents);
46
+ }
47
+
48
+ /**
49
+ * Rank documents and return them sorted by descending relevance.
50
+ * Returns an array of [document, score] pairs.
51
+ */
52
+ async rankAndSort(
53
+ query: string,
54
+ documents: string[],
55
+ ): Promise<[string, number][]> {
56
+ const json = await this._inner.rankAndSortJson(query, documents);
57
+ return JSON.parse(json);
58
+ }
59
+
60
+ /**
61
+ * Immediately free the underlying Rust resources.
62
+ * After calling this, the CrossEncoder instance is no longer usable.
63
+ */
64
+ destroy(): void {
65
+ this._inner.uniffiDestroy();
66
+ }
67
+ }
package/src/encoder.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { Encoder as RustEncoder } from "../generated/ts/nobodywho";
2
+ import { Model } from "./model";
3
+
4
+ /**
5
+ * An embedding encoder for generating text embeddings.
6
+ *
7
+ * Wraps the internal Encoder with an ergonomic API including
8
+ * a `fromPath` factory for one-step model loading.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const encoder = await Encoder.fromPath({
13
+ * modelPath: "embeddings.gguf",
14
+ * });
15
+ * const embedding = await encoder.encode("Hello world");
16
+ * ```
17
+ */
18
+ export class Encoder {
19
+ /** @internal */
20
+ private readonly _inner: RustEncoder;
21
+
22
+ constructor(opts: { model: Model; contextSize?: number }) {
23
+ this._inner = new RustEncoder(opts.model._inner, opts.contextSize ?? undefined);
24
+ }
25
+
26
+ /**
27
+ * Create an encoder directly from a model path.
28
+ * Loads the model and creates the encoder in one step.
29
+ */
30
+ static async fromPath(opts: {
31
+ modelPath: string;
32
+ useGpu?: boolean;
33
+ contextSize?: number;
34
+ }): Promise<Encoder> {
35
+ const model = await Model.load({ modelPath: opts.modelPath, useGpu: opts.useGpu });
36
+ return new Encoder({ model, ...opts });
37
+ }
38
+
39
+ /** Encode text into an embedding vector. */
40
+ async encode(text: string): Promise<number[]> {
41
+ return this._inner.encode(text);
42
+ }
43
+
44
+ /**
45
+ * Immediately free the underlying Rust resources.
46
+ * After calling this, the Encoder instance is no longer usable.
47
+ */
48
+ destroy(): void {
49
+ this._inner.uniffiDestroy();
50
+ }
51
+ }
package/src/message.ts ADDED
@@ -0,0 +1,75 @@
1
+ import {
2
+ Message as InternalMessage,
3
+ Message_Tags,
4
+ Role,
5
+ type Asset,
6
+ type ToolCall,
7
+ } from "../generated/ts/nobodywho";
8
+
9
+ /**
10
+ * A chat message. The `role` field combined with the presence of
11
+ * `toolCalls` or `name` determines the message type:
12
+ *
13
+ * - **User/Assistant/System message:** `{ role, content, assets? }`
14
+ * - **Tool call request:** `{ role: Role.Assistant, content, toolCalls }`
15
+ * - **Tool response:** `{ role: Role.Tool, name, content }`
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const history = await chat.getChatHistory();
20
+ * for (const msg of history) {
21
+ * if ("toolCalls" in msg) {
22
+ * console.log("Tool calls:", msg.toolCalls);
23
+ * } else if ("name" in msg) {
24
+ * console.log("Tool response:", msg.name, msg.content);
25
+ * } else {
26
+ * console.log(msg.role, msg.content);
27
+ * }
28
+ * }
29
+ * ```
30
+ */
31
+ export type ChatMessage =
32
+ | { role: Role.User | Role.Assistant | Role.System; content: string; assets?: Asset[] }
33
+ | { role: Role.Assistant; content: string; toolCalls: ToolCall[] }
34
+ | { role: Role.Tool; name: string; content: string };
35
+
36
+ /** @internal Convert internal Message to ChatMessage */
37
+ export function fromInternal(msg: InternalMessage): ChatMessage {
38
+ if (msg.tag === Message_Tags.Message) {
39
+ const { role, content, assets } = msg.inner;
40
+ return {
41
+ role: role as Role.User | Role.Assistant | Role.System,
42
+ content,
43
+ ...(assets.length > 0 ? { assets } : {}),
44
+ };
45
+ } else if (msg.tag === Message_Tags.ToolCalls) {
46
+ const { role, content, toolCalls } = msg.inner;
47
+ return { role: role as Role.Assistant, content, toolCalls };
48
+ } else {
49
+ const { role, name, content } = msg.inner;
50
+ return { role: role as Role.Tool, name, content };
51
+ }
52
+ }
53
+
54
+ /** @internal Convert ChatMessage to internal Message */
55
+ export function toInternal(msg: ChatMessage): InternalMessage {
56
+ if ("toolCalls" in msg) {
57
+ return new InternalMessage.ToolCalls({
58
+ role: msg.role,
59
+ content: msg.content,
60
+ toolCalls: msg.toolCalls,
61
+ });
62
+ } else if ("name" in msg) {
63
+ return new InternalMessage.ToolResp({
64
+ role: msg.role,
65
+ name: msg.name,
66
+ content: msg.content,
67
+ });
68
+ } else {
69
+ return new InternalMessage.Message({
70
+ role: msg.role,
71
+ content: msg.content,
72
+ assets: msg.assets ?? [],
73
+ });
74
+ }
75
+ }
package/src/model.ts ADDED
@@ -0,0 +1,63 @@
1
+ import {
2
+ loadModel,
3
+ type ModelInterface,
4
+ } from "../generated/ts/nobodywho";
5
+
6
+ /**
7
+ * A loaded LLM model that can be shared across Chat, Encoder, and CrossEncoder instances.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // Load from a local file
12
+ * const model = await Model.load({ modelPath: "model.gguf" });
13
+ * const chat = new Chat({ model, systemPrompt: "You are helpful." });
14
+ *
15
+ * // Download from HuggingFace (cached automatically)
16
+ * const model = await Model.load({
17
+ * modelPath: "hf://NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf",
18
+ * });
19
+ *
20
+ * // With vision support
21
+ * const visionModel = await Model.load({
22
+ * modelPath: "model.gguf",
23
+ * projectionModelPath: "mmproj.gguf",
24
+ * });
25
+ * ```
26
+ */
27
+ export class Model {
28
+ /** @internal */
29
+ readonly _inner: ModelInterface;
30
+
31
+ /** @internal */
32
+ private constructor(inner: ModelInterface) {
33
+ this._inner = inner;
34
+ }
35
+
36
+ /**
37
+ * Load a GGUF model from a local path or remote URL.
38
+ *
39
+ * @param opts.modelPath - Path to the GGUF model file, or a `hf://owner/repo/file.gguf` / `https://` URL to download and cache automatically
40
+ * @param opts.useGpu - Use GPU acceleration if available (default: true)
41
+ * @param opts.projectionModelPath - Path to a multimodal projector file for vision models
42
+ */
43
+ static async load(opts: {
44
+ modelPath: string;
45
+ useGpu?: boolean;
46
+ projectionModelPath?: string;
47
+ }): Promise<Model> {
48
+ const inner = await loadModel(
49
+ opts.modelPath,
50
+ opts.useGpu ?? true,
51
+ opts.projectionModelPath,
52
+ );
53
+ return new Model(inner);
54
+ }
55
+
56
+ /**
57
+ * Immediately free the underlying Rust resources (model weights, GPU memory).
58
+ * After calling this, the Model instance is no longer usable.
59
+ */
60
+ destroy(): void {
61
+ (this._inner as any).uniffiDestroy();
62
+ }
63
+ }
@@ -1,5 +1,5 @@
1
- import type { SamplerConfigInterface } from "../generated/ts/nobodywho";
2
1
  import {
2
+ type SamplerConfig,
3
3
  samplerPresetDefault,
4
4
  samplerPresetDry,
5
5
  samplerPresetGrammar,
@@ -23,42 +23,42 @@ export class SamplerPresets {
23
23
  private constructor() {}
24
24
 
25
25
  /** Get the default sampler configuration. */
26
- static default(): SamplerConfigInterface {
26
+ static default(): SamplerConfig {
27
27
  return samplerPresetDefault();
28
28
  }
29
29
 
30
30
  /** Create a sampler with top-k filtering only. */
31
- static topK(topK: number): SamplerConfigInterface {
31
+ static topK(topK: number): SamplerConfig {
32
32
  return samplerPresetTopK(topK);
33
33
  }
34
34
 
35
35
  /** Create a sampler with nucleus (top-p) sampling. */
36
- static topP(topP: number): SamplerConfigInterface {
36
+ static topP(topP: number): SamplerConfig {
37
37
  return samplerPresetTopP(topP);
38
38
  }
39
39
 
40
40
  /** Create a greedy sampler (always picks most probable token). */
41
- static greedy(): SamplerConfigInterface {
41
+ static greedy(): SamplerConfig {
42
42
  return samplerPresetGreedy();
43
43
  }
44
44
 
45
45
  /** Create a sampler with temperature scaling. */
46
- static temperature(temperature: number): SamplerConfigInterface {
46
+ static temperature(temperature: number): SamplerConfig {
47
47
  return samplerPresetTemperature(temperature);
48
48
  }
49
49
 
50
50
  /** Create a DRY sampler preset to reduce repetition. */
51
- static dry(): SamplerConfigInterface {
51
+ static dry(): SamplerConfig {
52
52
  return samplerPresetDry();
53
53
  }
54
54
 
55
55
  /** Create a sampler configured for JSON output generation. */
56
- static json(): SamplerConfigInterface {
56
+ static json(): SamplerConfig {
57
57
  return samplerPresetJson();
58
58
  }
59
59
 
60
60
  /** Create a sampler with a custom grammar constraint. */
61
- static grammar(grammar: string): SamplerConfigInterface {
61
+ static grammar(grammar: string): SamplerConfig {
62
62
  return samplerPresetGrammar(grammar);
63
63
  }
64
64
  }
package/src/wrapper.ts CHANGED
@@ -2,45 +2,37 @@
2
2
  * NobodyWho — React Native bindings for local LLM inference.
3
3
  *
4
4
  * This is the public entry point. It re-exports the public API:
5
- * - Wrapper classes: Chat, Tool, TokenStream (wrapping internal Rust* types)
6
- * - Direct re-exports: Model, Encoder, CrossEncoder, SamplerBuilder, SamplerConfig, etc.
7
- * - Utilities: SamplerPresets, loadModel, cosineSimilarity
5
+ * - Wrapper classes: Model, Chat, Encoder, CrossEncoder, Tool, TokenStream, Prompt
6
+ * - Direct re-exports: SamplerBuilder, SamplerConfig, Role
7
+ * - Types: ChatMessage, Asset, ToolCall
8
+ * - Utilities: SamplerPresets, cosineSimilarity
8
9
  *
9
10
  * This file is NOT generated — it is safe to edit.
10
11
  */
11
12
 
12
- // Wrapper classes that hide the internal Rust* types.
13
+ // Wrapper classes
14
+ export { Model } from "./model";
13
15
  export { Chat } from "./chat";
16
+ export { Encoder } from "./encoder";
17
+ export { CrossEncoder } from "./cross_encoder";
14
18
  export { Prompt } from "./prompt";
15
19
  export { Tool } from "./tool";
16
20
  export { TokenStream } from "./streaming";
17
21
 
22
+ // Message types
23
+ export type { ChatMessage } from "./message";
24
+
18
25
  // Re-export types that don't need wrapping.
19
26
  export {
20
- Model,
21
- Encoder,
22
- CrossEncoder,
23
27
  SamplerBuilder,
24
28
  SamplerConfig,
25
29
  Role,
26
- Message,
27
- Message_Tags,
28
- PromptPart,
29
- PromptPart_Tags,
30
30
  cosineSimilarity,
31
- loadModel,
32
- uniffiInitAsync,
33
31
  } from "./index";
34
32
 
35
33
  export type {
36
- ModelInterface,
37
- EncoderInterface,
38
- CrossEncoderInterface,
39
- SamplerBuilderInterface,
40
- SamplerConfigInterface,
41
34
  Asset,
42
35
  ToolCall,
43
- ToolParameter,
44
36
  } from "./index";
45
37
 
46
38
  // Ergonomic wrapper additions.