react-native-nobodywho 0.1.2 → 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/README.md +19 -1
- package/generated/cpp/nobodywho.cpp +1 -1
- package/generated/ts/nobodywho-ffi.ts +1 -1
- package/generated/ts/nobodywho.ts +8 -4
- package/ios/Nobodywho.h +1 -1
- package/package.json +1 -1
- package/src/chat.ts +28 -20
- package/src/cross_encoder.ts +67 -0
- package/src/encoder.ts +51 -0
- package/src/message.ts +75 -0
- package/src/model.ts +63 -0
- package/src/sampler_presets.ts +9 -9
- package/src/wrapper.ts +11 -19
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: "/
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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(
|
|
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,
|
|
@@ -2840,7 +2844,7 @@ function uniffiEnsureInitialized() {
|
|
|
2840
2844
|
if (nativeModule().ubrn_uniffi_nobodywho_uniffi_checksum_func_cosine_similarity() !== 63439) {
|
|
2841
2845
|
throw new UniffiInternalError.ApiChecksumMismatch("uniffi_nobodywho_uniffi_checksum_func_cosine_similarity");
|
|
2842
2846
|
}
|
|
2843
|
-
if (nativeModule().ubrn_uniffi_nobodywho_uniffi_checksum_func_load_model() !==
|
|
2847
|
+
if (nativeModule().ubrn_uniffi_nobodywho_uniffi_checksum_func_load_model() !== 57455) {
|
|
2844
2848
|
throw new UniffiInternalError.ApiChecksumMismatch("uniffi_nobodywho_uniffi_checksum_func_load_model");
|
|
2845
2849
|
}
|
|
2846
2850
|
if (nativeModule().ubrn_uniffi_nobodywho_uniffi_checksum_func_sampler_preset_default() !== 10834) {
|
package/ios/Nobodywho.h
CHANGED
package/package.json
CHANGED
package/src/chat.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RustChat,
|
|
3
|
-
|
|
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
|
|
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:
|
|
34
|
+
model: Model;
|
|
36
35
|
systemPrompt?: string;
|
|
37
36
|
contextSize?: number;
|
|
38
37
|
templateVariables?: Map<string, boolean>;
|
|
39
38
|
tools?: Tool[];
|
|
40
|
-
sampler?:
|
|
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
|
-
|
|
66
|
+
projectionModelPath?: string;
|
|
68
67
|
systemPrompt?: string;
|
|
69
68
|
contextSize?: number;
|
|
70
69
|
templateVariables?: Map<string, boolean>;
|
|
71
70
|
tools?: Tool[];
|
|
72
|
-
sampler?:
|
|
71
|
+
sampler?: SamplerConfig;
|
|
73
72
|
}): Promise<Chat> {
|
|
74
|
-
const model = await
|
|
75
|
-
opts.modelPath,
|
|
76
|
-
opts.useGpu
|
|
77
|
-
opts.
|
|
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<
|
|
113
|
-
|
|
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:
|
|
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:
|
|
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
|
+
}
|
package/src/sampler_presets.ts
CHANGED
|
@@ -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():
|
|
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):
|
|
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):
|
|
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():
|
|
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):
|
|
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():
|
|
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():
|
|
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):
|
|
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
|
|
6
|
-
* - Direct re-exports:
|
|
7
|
-
* -
|
|
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
|
|
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.
|