1 // Copyright 2026 Splitfire AB (Onde Inference). All rights reserved.
2 // SPDX-License-Identifier: MIT OR Apache-2.0
3
4 //! Flutter Rust Bridge v2 API for the Onde Inference Dart SDK.
5 //!
6 //! This module declares mirror types (re-declared plain Rust structs/enums
7 //! without UniFFI derives) and an `OndeChatEngine` opaque wrapper so that
8 //! `flutter_rust_bridge_codegen generate` can produce clean Dart bindings.
9 //!
10 //! All `onde::inference` types are imported with an `Onde*` alias to avoid
11 //! name collisions with the bridge mirrors defined below.
12
13 use crate::frb_generated::StreamSink;
14 use flutter_rust_bridge::frb;
15 use onde::inference::{
16 ChatEngine, ChatMessage as OndeChatMessage, ChatRole as OndeChatRole,
17 EngineInfo as OndeEngineInfo, EngineStatus as OndeEngineStatus,
18 GgufModelConfig as OndeGgufModelConfig, InferenceError, InferenceResult as OndeInferenceResult,
19 SamplingConfig as OndeSamplingConfig, StreamChunk as OndeStreamChunk,
20 ToolCallInfo as OndeToolCallInfo,
21 };
22
23 // ── Mirror: ChatRole ──────────────────────────────────────────────────────────
24
25 /// Role of a participant in a chat conversation.
26 #[derive(Debug, Clone, PartialEq, Eq)]
27 pub enum ChatRole {
28 System,
29 User,
30 Assistant,
31 }
32
33 impl From<OndeChatRole> for ChatRole {
34 fn from(r: OndeChatRole) -> Self {
35 match r {
36 OndeChatRole::System => ChatRole::System,
37 OndeChatRole::User => ChatRole::User,
38 OndeChatRole::Assistant => ChatRole::Assistant,
39 }
40 }
41 }
42
43 impl From<ChatRole> for OndeChatRole {
44 fn from(r: ChatRole) -> Self {
45 match r {
46 ChatRole::System => OndeChatRole::System,
47 ChatRole::User => OndeChatRole::User,
48 ChatRole::Assistant => OndeChatRole::Assistant,
49 }
50 }
51 }
52
53 // ── Mirror: ChatMessage ───────────────────────────────────────────────────────
54
55 /// A single message in a conversation.
56 #[derive(Debug, Clone)]
57 pub struct ChatMessage {
58 pub role: ChatRole,
59 pub content: String,
60 }
61
62 impl From<OndeChatMessage> for ChatMessage {
63 fn from(m: OndeChatMessage) -> Self {
64 Self {
65 role: m.role.into(),
66 content: m.content,
67 }
68 }
69 }
70
71 impl From<ChatMessage> for OndeChatMessage {
72 fn from(m: ChatMessage) -> Self {
73 Self {
74 role: m.role.into(),
75 content: m.content,
76 }
77 }
78 }
79
80 // ── Mirror: SamplingConfig ────────────────────────────────────────────────────
81
82 /// Sampling parameters for text generation. All fields are optional — `None`
83 /// means "use the engine default".
84 #[derive(Debug, Clone)]
85 pub struct SamplingConfig {
86 pub temperature: Option<f64>,
87 pub top_p: Option<f64>,
88 pub top_k: Option<u64>,
89 pub min_p: Option<f64>,
90 pub max_tokens: Option<u64>,
91 pub frequency_penalty: Option<f32>,
92 pub presence_penalty: Option<f32>,
93 }
94
95 impl From<OndeSamplingConfig> for SamplingConfig {
96 fn from(s: OndeSamplingConfig) -> Self {
97 Self {
98 temperature: s.temperature,
99 top_p: s.top_p,
100 top_k: s.top_k,
101 min_p: s.min_p,
102 max_tokens: s.max_tokens,
103 frequency_penalty: s.frequency_penalty,
104 presence_penalty: s.presence_penalty,
105 }
106 }
107 }
108
109 impl From<SamplingConfig> for OndeSamplingConfig {
110 fn from(s: SamplingConfig) -> Self {
111 Self {
112 temperature: s.temperature,
113 top_p: s.top_p,
114 top_k: s.top_k,
115 min_p: s.min_p,
116 max_tokens: s.max_tokens,
117 frequency_penalty: s.frequency_penalty,
118 presence_penalty: s.presence_penalty,
119 }
120 }
121 }
122
123 // ── Mirror: GgufModelConfig ───────────────────────────────────────────────────
124
125 /// Configuration for loading a pre-quantised GGUF model.
126 #[derive(Debug, Clone)]
127 pub struct GgufModelConfig {
128 /// HuggingFace repository ID, e.g. `"bartowski/Qwen2.5-1.5B-Instruct-GGUF"`.
129 pub model_id: String,
130 /// GGUF filename(s) within the repository.
131 pub files: Vec<String>,
132 /// Optional explicit tokenizer model ID (required on Android).
133 pub tok_model_id: Option<String>,
134 /// Human-friendly display name, e.g. `"Qwen 2.5 1.5B"`.
135 pub display_name: String,
136 /// Approximate memory footprint, e.g. `"~941 MB (GGUF Q4_K_M)"`.
137 pub approx_memory: String,
138 }
139
140 impl From<OndeGgufModelConfig> for GgufModelConfig {
141 fn from(c: OndeGgufModelConfig) -> Self {
142 Self {
143 model_id: c.model_id,
144 files: c.files,
145 tok_model_id: c.tok_model_id,
146 display_name: c.display_name,
147 approx_memory: c.approx_memory,
148 }
149 }
150 }
151
152 impl From<GgufModelConfig> for OndeGgufModelConfig {
153 fn from(c: GgufModelConfig) -> Self {
154 Self {
155 model_id: c.model_id,
156 files: c.files,
157 tok_model_id: c.tok_model_id,
158 display_name: c.display_name,
159 approx_memory: c.approx_memory,
160 chat_template: None,
161 }
162 }
163 }
164
165 // ── Mirror: ToolCallInfo ────────────────────────────────────────────────────
166
167 /// Structured tool-call request emitted by the model.
168 #[derive(Debug, Clone)]
169 pub struct ToolCallInfo {
170 pub id: String,
171 pub function_name: String,
172 pub arguments: String,
173 }
174
175 impl From<OndeToolCallInfo> for ToolCallInfo {
176 fn from(t: OndeToolCallInfo) -> Self {
177 Self {
178 id: t.id,
179 function_name: t.function_name,
180 arguments: t.arguments,
181 }
182 }
183 }
184
185 // ── Mirror: InferenceResult ───────────────────────────────────────────────────
186
187 /// The result of a completed inference request.
188 #[derive(Debug, Clone)]
189 pub struct InferenceResult {
190 /// The generated assistant reply text.
191 pub text: String,
192 /// Wall-clock inference duration in seconds.
193 pub duration_secs: f64,
194 /// Human-readable duration string, e.g. `"4.5s"` or `"2m 3.1s"`.
195 pub duration_display: String,
196 /// Finish reason reported by the model, e.g. `"stop"` or `"length"`.
197 pub finish_reason: String,
198 /// Tool calls requested by the model (empty if none).
199 pub tool_calls: Vec<ToolCallInfo>,
200 }
201
202 impl From<OndeInferenceResult> for InferenceResult {
203 fn from(r: OndeInferenceResult) -> Self {
204 Self {
205 text: r.text,
206 duration_secs: r.duration_secs,
207 duration_display: r.duration_display,
208 finish_reason: r.finish_reason,
209 tool_calls: r.tool_calls.into_iter().map(ToolCallInfo::from).collect(),
210 }
211 }
212 }
213
214 // ── Mirror: StreamChunk ───────────────────────────────────────────────────────
215
216 /// A single streaming token chunk emitted during inference.
217 #[derive(Debug, Clone)]
218 pub struct StreamChunk {
219 /// The token text delta (may be empty on the final chunk).
220 pub delta: String,
221 /// `true` when this is the last chunk in the stream.
222 pub done: bool,
223 /// Finish reason (set only on the final chunk).
224 pub finish_reason: Option<String>,
225 }
226
227 impl From<OndeStreamChunk> for StreamChunk {
228 fn from(c: OndeStreamChunk) -> Self {
229 Self {
230 delta: c.delta,
231 done: c.done,
232 finish_reason: c.finish_reason,
233 }
234 }
235 }
236
237 // ── Mirror: EngineStatus ──────────────────────────────────────────────────────
238
239 /// Lifecycle status of the inference engine.
240 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
241 pub enum EngineStatus {
242 Unloaded,
243 Loading,
244 Ready,
245 Generating,
246 Error,
247 }
248
249 impl From<OndeEngineStatus> for EngineStatus {
250 fn from(s: OndeEngineStatus) -> Self {
251 match s {
252 OndeEngineStatus::Unloaded => EngineStatus::Unloaded,
253 OndeEngineStatus::Loading => EngineStatus::Loading,
254 OndeEngineStatus::Ready => EngineStatus::Ready,
255 OndeEngineStatus::Generating => EngineStatus::Generating,
256 OndeEngineStatus::Error => EngineStatus::Error,
257 }
258 }
259 }
260
261 // ── Mirror: EngineInfo ────────────────────────────────────────────────────────
262
263 /// A point-in-time snapshot of the engine's state.
264 #[derive(Debug, Clone)]
265 pub struct EngineInfo {
266 pub status: EngineStatus,
267 /// Display name of the currently loaded model, if any.
268 pub model_name: Option<String>,
269 /// Approximate memory footprint of the loaded model, if any.
270 pub approx_memory: Option<String>,
271 /// Number of conversation turns in the current history.
272 pub history_length: u64,
273 }
274
275 impl From<OndeEngineInfo> for EngineInfo {
276 fn from(i: OndeEngineInfo) -> Self {
277 Self {
278 status: i.status.into(),
279 model_name: i.model_name,
280 approx_memory: i.approx_memory,
281 history_length: i.history_length,
282 }
283 }
284 }
285
286 // ── Error type ────────────────────────────────────────────────────────────────
287
288 /// Bridge error type mapping from [`onde::inference::InferenceError`].
289 #[derive(Debug, thiserror::Error)]
290 pub enum OndeError {
291 #[error("No model loaded — call load_gguf_model first")]
292 NoModelLoaded,
293
294 #[error("Model already loaded: {model_name}")]
295 AlreadyLoaded { model_name: String },
296
297 #[error("Failed to build model: {reason}")]
298 ModelBuild { reason: String },
299
300 #[error("Inference failed: {reason}")]
301 Inference { reason: String },
302
303 #[error("Operation was cancelled")]
304 Cancelled,
305
306 #[error("{reason}")]
307 Other { reason: String },
308 }
309
310 impl From<InferenceError> for OndeError {
311 fn from(e: InferenceError) -> Self {
312 match e {
313 InferenceError::NoModelLoaded => OndeError::NoModelLoaded,
314 InferenceError::AlreadyLoaded { model_name } => OndeError::AlreadyLoaded { model_name },
315 InferenceError::ModelBuild { reason } => OndeError::ModelBuild { reason },
316 InferenceError::Inference { reason } => OndeError::Inference { reason },
317 InferenceError::Cancelled => OndeError::Cancelled,
318 InferenceError::Other { reason } => OndeError::Other { reason },
319 }
320 }
321 }
322
323 // ── OndeChatEngine ────────────────────────────────────────────────────────────
324
325 /// Opaque wrapper around `onde::inference::ChatEngine`.
326 ///
327 /// Dart/Flutter receives this as an opaque `RustOpaque<OndeChatEngine>` handle.
328 /// All methods delegate directly to the inner `ChatEngine`.
329 pub struct OndeChatEngine {
330 inner: ChatEngine,
331 }
332
333 impl OndeChatEngine {
334 // ── Construction ──────────────────────────────────────────────────────
335
336 /// Create a new engine with no model loaded.
337 #[frb(sync)]
338 pub fn new() -> OndeChatEngine {
339 OndeChatEngine {
340 inner: ChatEngine::new(),
341 }
342 }
343
344 // ── Model lifecycle ───────────────────────────────────────────────────
345
346 /// Load a GGUF model into the engine.
347 ///
348 /// Returns the load duration in seconds on success.
349 pub async fn load_gguf_model(
350 &self,
351 config: GgufModelConfig,
352 system_prompt: Option<String>,
353 sampling: Option<SamplingConfig>,
354 ) -> Result<f64, OndeError> {
355 let elapsed = self
356 .inner
357 .load_gguf_model(config.into(), system_prompt, sampling.map(Into::into))
358 .await
359 .map_err(OndeError::from)?;
360 Ok(elapsed.as_secs_f64())
361 }
362
363 /// Load the model assigned to this Onde app from the dashboard.
364 ///
365 /// Returns the wall-clock seconds taken to load the model.
366 pub async fn load_assigned_model(
367 &self,
368 app_id: String,
369 app_secret: String,
370 system_prompt: Option<String>,
371 sampling: Option<SamplingConfig>,
372 ) -> Result<f64, OndeError> {
373 let elapsed = self
374 .inner
375 .load_assigned_model(
376 smbcloud_gresiq_sdk::Environment::Production,
377 &app_id,
378 &app_secret,
379 system_prompt,
380 sampling.map(Into::into),
381 )
382 .await
383 .map_err(OndeError::from)?;
384 Ok(elapsed.as_secs_f64())
385 }
386
387 /// Unload the current model and release its memory.
388 ///
389 /// Returns the display name of the model that was unloaded, or `None`
390 /// if no model was loaded.
391 pub async fn unload_model(&self) -> Option<String> {
392 self.inner.unload_model().await
393 }
394
395 /// Returns `true` if a model is currently loaded and ready.
396 pub async fn is_loaded(&self) -> bool {
397 self.inner.is_loaded().await
398 }
399
400 /// Returns a point-in-time snapshot of the engine's state.
401 pub async fn info(&self) -> EngineInfo {
402 self.inner.info().await.into()
403 }
404
405 // ── Configuration ─────────────────────────────────────────────────────
406
407 /// Set or replace the system prompt.
408 ///
409 /// The prompt is prepended to every inference request and is **not**
410 /// stored in the conversation history.
411 pub async fn set_system_prompt(&self, prompt: String) {
412 self.inner.set_system_prompt(prompt).await;
413 }
414
415 /// Clear the current system prompt.
416 pub async fn clear_system_prompt(&self) {
417 self.inner.clear_system_prompt().await;
418 }
419
420 /// Replace the sampling configuration used for all subsequent inference.
421 pub async fn set_sampling(&self, sampling: SamplingConfig) {
422 self.inner.set_sampling(sampling.into()).await;
423 }
424
425 // ── Conversation history ──────────────────────────────────────────────
426
427 /// Returns a clone of the full conversation history.
428 pub async fn history(&self) -> Vec<ChatMessage> {
429 self.inner
430 .history()
431 .await
432 .into_iter()
433 .map(ChatMessage::from)
434 .collect()
435 }
436
437 /// Clear the conversation history while keeping the model loaded.
438 ///
439 /// Returns the number of turns that were removed.
440 pub async fn clear_history(&self) -> u64 {
441 self.inner.clear_history().await as u64
442 }
443
444 /// Append a message to history without running inference.
445 ///
446 /// Useful for restoring a saved conversation or injecting context.
447 pub async fn push_history(&self, message: ChatMessage) {
448 self.inner.push_history(message.into()).await;
449 }
450
451 // ── Non-streaming inference ───────────────────────────────────────────
452
453 /// Send a user message and receive a complete assistant reply.
454 ///
455 /// Both the user message and assistant reply are appended to the
456 /// conversation history on success.
457 pub async fn send_message(&self, message: String) -> Result<InferenceResult, OndeError> {
458 self.inner
459 .send_message(message)
460 .await
461 .map(InferenceResult::from)
462 .map_err(OndeError::from)
463 }
464
465 /// One-shot generation from an explicit message list.
466 ///
467 /// Does **not** modify the conversation history.
468 pub async fn generate(
469 &self,
470 messages: Vec<ChatMessage>,
471 sampling: Option<SamplingConfig>,
472 ) -> Result<InferenceResult, OndeError> {
473 let onde_messages: Vec<OndeChatMessage> =
474 messages.into_iter().map(OndeChatMessage::from).collect();
475 self.inner
476 .generate(onde_messages, sampling.map(Into::into))
477 .await
478 .map(InferenceResult::from)
479 .map_err(OndeError::from)
480 }
481
482 // ── Streaming inference ───────────────────────────────────────────────
483
484 /// Stream a reply token-by-token.
485 ///
486 /// Each [`StreamChunk`] carries a `delta` string. The final chunk has
487 /// `done == true`. The `sink` is closed automatically when the stream
488 /// ends or if the receiver is dropped.
489 pub async fn stream_message(
490 &self,
491 message: String,
492 sink: StreamSink<StreamChunk>,
493 ) -> Result<(), OndeError> {
494 let mut rx = self
495 .inner
496 .stream_message(message)
497 .await
498 .map_err(OndeError::from)?;
499
500 while let Some(chunk) = rx.recv().await {
501 let bridge_chunk = StreamChunk::from(chunk);
502 let done = bridge_chunk.done;
503 let _ = sink.add(bridge_chunk);
504 if done {
505 break;
506 }
507 }
508
509 Ok(())
510 }
511 }
512
513 // ── Free functions ────────────────────────────────────────────────────────────
514
515 /// Return the platform-appropriate default `GgufModelConfig`.
516 ///
517 /// Selects the 1.5B model on iOS / tvOS / Android and the 3B model on
518 /// macOS / Windows / Linux.
519 #[frb(sync)]
520 pub fn default_model_config() -> GgufModelConfig {
521 OndeGgufModelConfig::platform_default().into()
522 }
523
524 /// `GgufModelConfig` for Qwen 2.5 1.5B Instruct Q4_K_M (~941 MB).
525 #[frb(sync)]
526 pub fn qwen25_1_5b_config() -> GgufModelConfig {
527 OndeGgufModelConfig::qwen25_1_5b().into()
528 }
529
530 /// `GgufModelConfig` for Qwen 2.5 3B Instruct Q4_K_M (~1.93 GB).
531 #[frb(sync)]
532 pub fn qwen25_3b_config() -> GgufModelConfig {
533 OndeGgufModelConfig::qwen25_3b().into()
534 }
535
536 /// `GgufModelConfig` for Qwen 2.5 Coder 1.5B Instruct Q4_K_M (~941 MB).
537 #[frb(sync)]
538 pub fn qwen25_coder_1_5b_config() -> GgufModelConfig {
539 OndeGgufModelConfig::qwen25_coder_1_5b().into()
540 }
541
542 /// `GgufModelConfig` for Qwen 2.5 Coder 3B Instruct Q4_K_M (~1.93 GB).
543 #[frb(sync)]
544 pub fn qwen25_coder_3b_config() -> GgufModelConfig {
545 OndeGgufModelConfig::qwen25_coder_3b().into()
546 }
547
548 // ── Qwen 3 family ────────────────────────────────────────────────────────────
549
550 /// `GgufModelConfig` for Qwen 3 0.6B Q4_K_M (~0.5 GB).
551 #[frb(sync)]
552 pub fn qwen3_0_6b_config() -> GgufModelConfig {
553 OndeGgufModelConfig::qwen3_0_6b().into()
554 }
555
556 /// `GgufModelConfig` for Qwen 3 1.7B Q4_K_M (~1.3 GB).
557 #[frb(sync)]
558 pub fn qwen3_1_7b_config() -> GgufModelConfig {
559 OndeGgufModelConfig::qwen3_1_7b().into()
560 }
561
562 /// `GgufModelConfig` for Qwen 3 4B Q4_K_M (~2.7 GB).
563 #[frb(sync)]
564 pub fn qwen3_4b_config() -> GgufModelConfig {
565 OndeGgufModelConfig::qwen3_4b().into()
566 }
567
568 /// `GgufModelConfig` for Qwen 3 8B Q4_K_M (~5 GB).
569 #[frb(sync)]
570 pub fn qwen3_8b_config() -> GgufModelConfig {
571 OndeGgufModelConfig::qwen3_8b().into()
572 }
573
574 /// `GgufModelConfig` for Qwen 3 14B Q4_K_M (~8.4 GB).
575 #[frb(sync)]
576 pub fn qwen3_14b_config() -> GgufModelConfig {
577 OndeGgufModelConfig::qwen3_14b().into()
578 }
579
580 /// `GgufModelConfig` for Qwen 3 32B Q4_K_M (~19.8 GB).
581 #[frb(sync)]
582 pub fn qwen3_32b_config() -> GgufModelConfig {
583 OndeGgufModelConfig::qwen3_32b().into()
584 }
585
586 /// `GgufModelConfig` for Qwen 3 4B Instruct 2507 Q4_K_M (~2.5 GB).
587 #[frb(sync)]
588 pub fn qwen3_4b_instruct_2507_config() -> GgufModelConfig {
589 OndeGgufModelConfig::qwen3_4b_instruct_2507().into()
590 }
591
592 /// `GgufModelConfig` for Qwen 3 4B Thinking 2507 Q4_K_M (~2.5 GB).
593 #[frb(sync)]
594 pub fn qwen3_4b_thinking_2507_config() -> GgufModelConfig {
595 OndeGgufModelConfig::qwen3_4b_thinking_2507().into()
596 }
597
598 /// `GgufModelConfig` for Qwen 3 30B-A3B Instruct 2507 (MoE) Q4_K_M (~18.6 GB).
599 #[frb(sync)]
600 pub fn qwen3_30b_a3b_instruct_2507_config() -> GgufModelConfig {
601 OndeGgufModelConfig::qwen3_30b_a3b_instruct_2507().into()
602 }
603
604 /// Default sampling config: `temperature=0.7`, `top_p=0.95`, `max_tokens=512`.
605 #[frb(sync)]
606 pub fn default_sampling_config() -> SamplingConfig {
607 OndeSamplingConfig::default().into()
608 }
609
610 /// Deterministic sampling config: `temperature=0.0`, greedy, `max_tokens=512`.
611 #[frb(sync)]
612 pub fn deterministic_sampling_config() -> SamplingConfig {
613 OndeSamplingConfig::deterministic().into()
614 }
615
616 /// Mobile sampling config: `temperature=0.7`, `top_p=0.95`, `max_tokens=128`.
617 #[frb(sync)]
618 pub fn mobile_sampling_config() -> SamplingConfig {
619 OndeSamplingConfig::mobile().into()
620 }
621
622 /// Seed the HuggingFace cache environment for sandboxed platforms.
623 ///
624 /// On iOS (and Android), the default `~/.cache/huggingface/hub` path is outside
625 /// the app sandbox and writes fail with `Operation not permitted (os error 1)`.
626 ///
627 /// Call this **once** at startup — before any `load_gguf_model` or
628 /// `download_model` — with the app's writable data directory.
629 ///
630 /// # Dart usage
631 ///
632 /// ```dart
633 /// import 'package:path_provider/path_provider.dart';
634 ///
635 /// final dir = await getApplicationSupportDirectory();
636 /// configureCacheDir(appDataDir: dir.path);
637 /// ```
638 ///
639 /// Internally this:
640 /// 1. Creates `<app_data_dir>/huggingface/hub/` if it doesn't exist.
641 /// 2. Sets the `HF_HOME` and `HF_HUB_CACHE` environment variables.
642 /// 3. Seeds `mistralrs_core::GLOBAL_HF_CACHE` so the model loader never
643 /// falls back to `Cache::default()`.
644 #[frb(sync)]
645 pub fn configure_cache_dir(app_data_dir: String) -> Result<(), OndeError> {
646 use std::fs;
647 use std::path::PathBuf;
648
649 let data_dir = PathBuf::from(&app_data_dir);
650 let hf_home = data_dir.join("models");
651 let hf_hub_cache = hf_home.join("hub");
652
653 fs::create_dir_all(&hf_hub_cache).map_err(|e| OndeError::Other {
654 reason: format!(
655 "Cannot create HF cache dir {}: {}",
656 hf_hub_cache.display(),
657 e
658 ),
659 })?;
660
661 // Set env vars BEFORE any hf-hub or mistralrs call. Both libraries
662 // read these to resolve the cache directory — no need to touch
663 // GLOBAL_HF_CACHE directly (that's seeded lazily by load_gguf_model
664 // on Android/iOS when it reads HF_HOME back).
665 std::env::set_var("HF_HOME", &hf_home);
666 std::env::set_var("HF_HUB_CACHE", &hf_hub_cache);
667
668 // Pre-create tmp dir — iOS sandbox also restricts the system TMPDIR.
669 let tmp_dir = data_dir.join("tmp");
670 let _ = fs::create_dir_all(&tmp_dir);
671 std::env::set_var("TMPDIR", &tmp_dir);
672
673 log::info!(
674 "HF cache configured: HF_HOME={}, HF_HUB_CACHE={}",
675 hf_home.display(),
676 hf_hub_cache.display(),
677 );
678
679 Ok(())
680 }