1use core::ffi::{c_char, c_void};
4use core::ptr;
5use std::ffi::CString;
6use std::sync::mpsc;
7use std::sync::{Arc, Mutex};
8
9use serde::Deserialize;
10use serde_json::json;
11
12use crate::content::{BridgeGeneratedContent, GeneratedContent};
13use crate::error::FMError;
14use crate::ffi;
15use crate::generation::{GenerationOptions, SamplingMode};
16use crate::model::ConfiguredSystemLanguageModel;
17use crate::prompt::{Instructions, Prompt, ToInstructions, ToPrompt};
18use crate::schema::GenerationSchema;
19use crate::tool::{tool_callback_trampoline, Tool, ToolRegistry};
20use crate::transcript::Transcript;
21
22pub struct LanguageModelSession {
40 ptr: *mut c_void,
41 _tool_registry: Option<Arc<ToolRegistry>>,
42}
43
44unsafe impl Send for LanguageModelSession {}
50unsafe impl Sync for LanguageModelSession {}
51
52impl LanguageModelSession {
53 #[must_use]
61 pub fn new() -> Self {
62 Self::try_new(None).expect("FoundationModels is not available on this OS")
63 }
64
65 #[must_use]
72 pub fn with_instructions(instructions: &str) -> Self {
73 Self::try_new(Some(instructions)).expect("FoundationModels is not available on this OS")
74 }
75
76 #[must_use]
80 pub fn try_new(instructions: Option<&str>) -> Option<Self> {
81 let cstring = match instructions {
82 Some(s) => Some(CString::new(s).ok()?),
83 None => None,
84 };
85 let ptr =
86 unsafe { ffi::fm_session_create(cstring.as_ref().map_or(ptr::null(), |s| s.as_ptr())) };
87 if ptr.is_null() {
88 return None;
89 }
90 Some(Self {
91 ptr,
92 _tool_registry: None,
93 })
94 }
95
96 pub fn respond(&self, prompt: &str) -> Result<String, FMError> {
104 self.respond_with(prompt, GenerationOptions::new())
105 }
106
107 pub fn prewarm(&self) {
111 unsafe { ffi::fm_session_prewarm(self.ptr) };
112 }
113
114 #[must_use]
117 pub fn is_responding(&self) -> bool {
118 unsafe { ffi::fm_session_is_responding(self.ptr) }
119 }
120
121 #[must_use]
126 pub fn transcript_json(&self) -> String {
127 let p = unsafe { ffi::fm_session_transcript_json(self.ptr) };
128 if p.is_null() {
129 return String::from("{}");
130 }
131 let s = unsafe { core::ffi::CStr::from_ptr(p) }
132 .to_string_lossy()
133 .into_owned();
134 unsafe { ffi::fm_string_free(p) };
135 s
136 }
137
138 pub fn log_feedback(&self, sentiment: i32, description: Option<&str>) {
142 let cstr = description.and_then(|s| CString::new(s).ok());
143 let p = cstr.as_ref().map_or(core::ptr::null(), |c| c.as_ptr());
144 unsafe { ffi::fm_session_log_feedback(self.ptr, sentiment, p) };
145 }
146
147 pub fn respond_with_json_schema(
161 &self,
162 prompt: &str,
163 schema_description: &str,
164 ) -> Result<String, FMError> {
165 let wrapped = format!(
166 "{prompt}\n\n\
167 IMPORTANT: respond with VALID JSON ONLY (no prose, no markdown \
168 fences) that matches this schema:\n\n{schema_description}\n\n\
169 Your entire response must be parseable by JSON.parse()."
170 );
171 self.respond(&wrapped)
172 }
173
174 pub fn respond_with(
180 &self,
181 prompt: &str,
182 options: GenerationOptions,
183 ) -> Result<String, FMError> {
184 self.respond_prompt_with(prompt, options)
185 }
186
187 pub fn respond_with_schema(
220 &self,
221 prompt: &str,
222 schema: &str,
223 include_schema_in_prompt: bool,
224 ) -> Result<String, FMError> {
225 self.respond_with_schema_options(
226 prompt,
227 schema,
228 include_schema_in_prompt,
229 GenerationOptions::new(),
230 )
231 }
232
233 pub fn respond_with_schema_options(
240 &self,
241 prompt: &str,
242 schema: &str,
243 include_schema_in_prompt: bool,
244 options: GenerationOptions,
245 ) -> Result<String, FMError> {
246 let prompt_c = CString::new(prompt)
247 .map_err(|e| FMError::InvalidArgument(format!("prompt NUL byte: {e}")))?;
248 let schema_c = CString::new(schema)
249 .map_err(|e| FMError::InvalidArgument(format!("schema NUL byte: {e}")))?;
250 let opts = options.to_ffi();
251 let (tx, rx) = mpsc::channel();
252 let tx_box: Box<mpsc::Sender<Result<String, FMError>>> = Box::new(tx);
253 let context = Box::into_raw(tx_box).cast::<c_void>();
254
255 unsafe {
256 ffi::fm_session_respond_with_schema(
257 self.ptr,
258 prompt_c.as_ptr(),
259 schema_c.as_ptr(),
260 include_schema_in_prompt,
261 opts.temperature,
262 opts.maximum_response_tokens,
263 opts.sampling_mode,
264 opts.top_k,
265 opts.top_p,
266 context,
267 respond_trampoline,
268 );
269 }
270
271 rx.recv().map_err(|_| FMError::Unknown {
272 code: ffi::status::UNKNOWN,
273 message: "Swift bridge dropped the callback channel".into(),
274 })?
275 }
276
277 pub fn stream<F>(&self, prompt: &str, mut on_chunk: F) -> Result<(), FMError>
286 where
287 F: FnMut(StreamEvent<'_>) + Send + 'static,
288 {
289 self.stream_with(prompt, GenerationOptions::new(), move |event| {
290 on_chunk(event);
291 })
292 }
293
294 pub fn stream_with<F>(
300 &self,
301 prompt: &str,
302 options: GenerationOptions,
303 on_chunk: F,
304 ) -> Result<(), FMError>
305 where
306 F: FnMut(StreamEvent<'_>) + Send + 'static,
307 {
308 let payload = respond_request_json(&Prompt::from(prompt), options, None, true)?;
309
310 let (done_tx, done_rx) = mpsc::channel::<Result<(), FMError>>();
311 let state = Arc::new(StreamState {
312 on_chunk: Mutex::new(Box::new(on_chunk)),
313 done_tx: Mutex::new(Some(done_tx)),
314 });
315 let context = Arc::into_raw(state).cast::<c_void>().cast_mut();
316
317 unsafe {
318 ffi::fm_session_stream_request_json(
319 self.ptr,
320 payload.as_ptr(),
321 context,
322 json_text_stream_trampoline,
323 )
324 };
325
326 done_rx.recv().map_err(|_| FMError::Unknown {
327 code: ffi::status::UNKNOWN,
328 message: "Swift bridge dropped the stream channel".into(),
329 })?
330 }
331}
332
333impl LanguageModelSession {
334 #[must_use]
336 pub fn builder<'a>() -> SessionBuilder<'a> {
337 SessionBuilder::new()
338 }
339
340 pub fn from_transcript(transcript: Transcript) -> Result<Self, FMError> {
346 Self::builder().transcript(transcript).build()
347 }
348
349 pub fn transcript(&self) -> Result<Transcript, FMError> {
356 Transcript::from_json_str(&self.transcript_json())
357 }
358
359 pub fn prewarm_with_prompt<P>(&self, prompt: P) -> Result<(), FMError>
365 where
366 P: ToPrompt,
367 {
368 let prompt = prompt.to_prompt()?;
369 let prompt_json = CString::new(prompt.to_bridge_json()?).map_err(|error| {
370 FMError::InvalidArgument(format!("prompt JSON contains a NUL byte: {error}"))
371 })?;
372 let mut error: *mut c_char = ptr::null_mut();
373 let status = unsafe {
374 ffi::fm_session_prewarm_prompt_json(self.ptr, prompt_json.as_ptr(), &mut error)
375 };
376 if status != ffi::status::OK {
377 return Err(crate::error::from_swift(status, error));
378 }
379 Ok(())
380 }
381
382 pub fn respond_prompt<P>(&self, prompt: P) -> Result<String, FMError>
388 where
389 P: ToPrompt,
390 {
391 self.respond_prompt_with(prompt, GenerationOptions::new())
392 }
393
394 pub fn respond_prompt_with<P>(
400 &self,
401 prompt: P,
402 options: GenerationOptions,
403 ) -> Result<String, FMError>
404 where
405 P: ToPrompt,
406 {
407 self.respond_prompt_detailed(prompt, options)
408 .map(|response| response.content)
409 }
410
411 pub fn respond_prompt_detailed<P>(
417 &self,
418 prompt: P,
419 options: GenerationOptions,
420 ) -> Result<SessionResponse<String>, FMError>
421 where
422 P: ToPrompt,
423 {
424 let prompt = prompt.to_prompt()?;
425 let payload = respond_request_json(&prompt, options, None, true)?;
426 let payload = request_response(self.ptr, &payload)?;
427 let response: BridgeTextResponse = serde_json::from_str(&payload)
428 .map_err(|error| FMError::DecodingFailure(error.to_string()))?;
429 Ok(SessionResponse {
430 content: response.content,
431 raw_content: GeneratedContent::from_bridge_payload(response.raw_content, true)?,
432 transcript: Transcript::from_json_str(&response.transcript_json)?,
433 })
434 }
435
436 pub fn respond_generated<P>(
442 &self,
443 prompt: P,
444 schema: &GenerationSchema,
445 include_schema_in_prompt: bool,
446 ) -> Result<GeneratedContent, FMError>
447 where
448 P: ToPrompt,
449 {
450 self.respond_generated_with(
451 prompt,
452 schema,
453 include_schema_in_prompt,
454 GenerationOptions::new(),
455 )
456 .map(|response| response.content)
457 }
458
459 pub fn respond_generated_with<P>(
465 &self,
466 prompt: P,
467 schema: &GenerationSchema,
468 include_schema_in_prompt: bool,
469 options: GenerationOptions,
470 ) -> Result<SessionResponse<GeneratedContent>, FMError>
471 where
472 P: ToPrompt,
473 {
474 let prompt = prompt.to_prompt()?;
475 let payload =
476 respond_request_json(&prompt, options, Some(schema), include_schema_in_prompt)?;
477 let payload = request_response(self.ptr, &payload)?;
478 let response: BridgeStructuredResponse = serde_json::from_str(&payload)
479 .map_err(|error| FMError::DecodingFailure(error.to_string()))?;
480 Ok(SessionResponse {
481 content: GeneratedContent::from_bridge_payload(response.content, true)?,
482 raw_content: GeneratedContent::from_bridge_payload(response.raw_content, true)?,
483 transcript: Transcript::from_json_str(&response.transcript_json)?,
484 })
485 }
486
487 pub fn respond_generating<P, T>(
494 &self,
495 prompt: P,
496 include_schema_in_prompt: bool,
497 options: GenerationOptions,
498 ) -> Result<SessionResponse<T>, FMError>
499 where
500 P: ToPrompt,
501 T: crate::schema::Generable,
502 {
503 let response = self.respond_generated_with(
504 prompt,
505 &T::generation_schema()?,
506 include_schema_in_prompt,
507 options,
508 )?;
509 Ok(SessionResponse {
510 content: T::from_generated_content(&response.content)?,
511 raw_content: response.raw_content,
512 transcript: response.transcript,
513 })
514 }
515
516 pub fn stream_prompt<P, F>(&self, prompt: P, on_chunk: F) -> Result<(), FMError>
522 where
523 P: ToPrompt,
524 F: FnMut(StreamEvent<'_>) + Send + 'static,
525 {
526 let prompt = prompt.to_prompt()?;
527 let prompt_text = prompt_to_plain_text(&prompt).ok_or_else(|| {
528 FMError::InvalidArgument(
529 "text streaming only supports prompts composed of text segments".into(),
530 )
531 })?;
532 self.stream_with(&prompt_text, GenerationOptions::new(), on_chunk)
533 }
534
535 pub fn stream_generated<P, F>(
541 &self,
542 prompt: P,
543 schema: &GenerationSchema,
544 include_schema_in_prompt: bool,
545 options: GenerationOptions,
546 on_event: F,
547 ) -> Result<(), FMError>
548 where
549 P: ToPrompt,
550 F: FnMut(StructuredStreamEvent) + Send + 'static,
551 {
552 let prompt = prompt.to_prompt()?;
553 let payload =
554 respond_request_json(&prompt, options, Some(schema), include_schema_in_prompt)?;
555 let (done_tx, done_rx) = mpsc::channel::<Result<(), FMError>>();
556 let state = Arc::new(StructuredStreamState {
557 on_event: Mutex::new(Box::new(on_event)),
558 done_tx: Mutex::new(Some(done_tx)),
559 });
560 let context = Arc::into_raw(state).cast::<c_void>().cast_mut();
561 unsafe {
562 ffi::fm_session_stream_request_json(
563 self.ptr,
564 payload.as_ptr(),
565 context,
566 structured_stream_trampoline,
567 )
568 };
569 done_rx.recv().map_err(|_| FMError::Unknown {
570 code: ffi::status::UNKNOWN,
571 message: "Swift bridge dropped the structured stream channel".into(),
572 })?
573 }
574
575 pub fn log_feedback_attachment(
581 &self,
582 request: FeedbackAttachmentRequest,
583 ) -> Result<Vec<u8>, FMError> {
584 let request_json = CString::new(request.to_bridge_json()?).map_err(|error| {
585 FMError::InvalidArgument(format!("feedback request contains a NUL byte: {error}"))
586 })?;
587 let mut length = 0usize;
588 let mut error: *mut c_char = ptr::null_mut();
589 let ptr = unsafe {
590 ffi::fm_session_log_feedback_attachment_json(
591 self.ptr,
592 request_json.as_ptr(),
593 &mut length,
594 &mut error,
595 )
596 };
597 if ptr.is_null() && !error.is_null() {
598 return Err(crate::error::from_swift(
599 ffi::status::INVALID_ARGUMENT,
600 error,
601 ));
602 }
603 if ptr.is_null() || length == 0 {
604 return Ok(Vec::new());
605 }
606 let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), length) }.to_vec();
607 unsafe { ffi::fm_bytes_free(ptr) };
608 Ok(bytes)
609 }
610}
611
612pub struct SessionBuilder<'a> {
614 model: Option<&'a ConfiguredSystemLanguageModel>,
615 instructions: Option<Instructions>,
616 transcript: Option<Transcript>,
617 tools: Vec<Tool>,
618}
619
620impl<'a> SessionBuilder<'a> {
621 const fn new() -> Self {
622 Self {
623 model: None,
624 instructions: None,
625 transcript: None,
626 tools: Vec::new(),
627 }
628 }
629
630 #[must_use]
632 pub const fn model(mut self, model: &'a ConfiguredSystemLanguageModel) -> Self {
633 self.model = Some(model);
634 self
635 }
636
637 pub fn instructions<I>(mut self, instructions: I) -> Result<Self, FMError>
639 where
640 I: ToInstructions,
641 {
642 self.instructions = Some(instructions.to_instructions()?);
643 Ok(self)
644 }
645
646 #[must_use]
648 pub fn transcript(mut self, transcript: Transcript) -> Self {
649 self.transcript = Some(transcript);
650 self
651 }
652
653 #[must_use]
655 pub fn tool(mut self, tool: Tool) -> Self {
656 self.tools.push(tool);
657 self
658 }
659
660 #[must_use]
662 pub fn tools(mut self, tools: impl IntoIterator<Item = Tool>) -> Self {
663 self.tools.extend(tools);
664 self
665 }
666
667 pub fn build(self) -> Result<LanguageModelSession, FMError> {
673 if self.instructions.is_some() && self.transcript.is_some() {
674 return Err(FMError::InvalidArgument(
675 "session builder accepts either instructions or a transcript, not both".into(),
676 ));
677 }
678
679 let instructions_json = self
680 .instructions
681 .as_ref()
682 .map(Instructions::to_bridge_json)
683 .transpose()?;
684 let transcript_json = self
685 .transcript
686 .as_ref()
687 .map(Transcript::to_json_string)
688 .transpose()?;
689 let tool_registry = if self.tools.is_empty() {
690 None
691 } else {
692 Some(Arc::new(ToolRegistry::new(self.tools)))
693 };
694 let tools_json = tool_registry
695 .as_ref()
696 .map(|registry| registry.specs_json())
697 .transpose()?;
698
699 let instructions_c = instructions_json
700 .as_deref()
701 .map(CString::new)
702 .transpose()
703 .map_err(|error| {
704 FMError::InvalidArgument(format!("instructions JSON contains a NUL byte: {error}"))
705 })?;
706 let transcript_c = transcript_json
707 .as_deref()
708 .map(CString::new)
709 .transpose()
710 .map_err(|error| {
711 FMError::InvalidArgument(format!("transcript JSON contains a NUL byte: {error}"))
712 })?;
713 let tools_c = tools_json
714 .as_deref()
715 .map(CString::new)
716 .transpose()
717 .map_err(|error| {
718 FMError::InvalidArgument(format!("tool JSON contains a NUL byte: {error}"))
719 })?;
720
721 let tool_context = tool_registry.as_ref().map_or(ptr::null_mut(), |registry| {
722 Arc::as_ptr(registry).cast_mut().cast::<c_void>()
723 });
724 let mut error: *mut c_char = ptr::null_mut();
725 let ptr = unsafe {
726 ffi::fm_session_create_ex(
727 self.model.map_or(ptr::null_mut(), |model| model.ptr),
728 instructions_c
729 .as_ref()
730 .map_or(ptr::null(), |json| json.as_ptr()),
731 transcript_c
732 .as_ref()
733 .map_or(ptr::null(), |json| json.as_ptr()),
734 tools_c.as_ref().map_or(ptr::null(), |json| json.as_ptr()),
735 tool_context,
736 tool_registry
737 .as_ref()
738 .map(|_| tool_callback_trampoline as ffi::FmToolCallback),
739 &mut error,
740 )
741 };
742 if ptr.is_null() {
743 return Err(crate::error::from_swift(
744 ffi::status::MODEL_UNAVAILABLE,
745 error,
746 ));
747 }
748 Ok(LanguageModelSession {
749 ptr,
750 _tool_registry: tool_registry,
751 })
752 }
753}
754
755#[derive(Debug, Clone, PartialEq)]
757pub struct SessionResponse<T> {
758 pub content: T,
759 pub raw_content: GeneratedContent,
760 pub transcript: Transcript,
761}
762
763#[derive(Debug, Clone, PartialEq, Eq)]
765pub struct StructuredStreamSnapshot {
766 pub content_json: String,
767 pub raw_content_json: String,
768 pub is_complete: bool,
769}
770
771#[derive(Debug, Clone, PartialEq)]
773#[non_exhaustive]
774pub enum StructuredStreamEvent {
775 Snapshot(StructuredStreamSnapshot),
776 Done,
777 Error(FMError),
778}
779
780#[derive(Debug, Clone, Copy, PartialEq, Eq)]
782pub enum FeedbackIssueCategory {
783 Unhelpful,
784 TooVerbose,
785 DidNotFollowInstructions,
786 Incorrect,
787 StereotypeOrBias,
788 SuggestiveOrSexual,
789 VulgarOrOffensive,
790 TriggeredGuardrailUnexpectedly,
791}
792
793impl FeedbackIssueCategory {
794 const fn as_str(self) -> &'static str {
795 match self {
796 Self::Unhelpful => "unhelpful",
797 Self::TooVerbose => "too_verbose",
798 Self::DidNotFollowInstructions => "did_not_follow_instructions",
799 Self::Incorrect => "incorrect",
800 Self::StereotypeOrBias => "stereotype_or_bias",
801 Self::SuggestiveOrSexual => "suggestive_or_sexual",
802 Self::VulgarOrOffensive => "vulgar_or_offensive",
803 Self::TriggeredGuardrailUnexpectedly => "triggered_guardrail_unexpectedly",
804 }
805 }
806}
807
808#[derive(Debug, Clone, PartialEq, Eq)]
810pub struct FeedbackIssue {
811 pub category: FeedbackIssueCategory,
812 pub explanation: Option<String>,
813}
814
815#[derive(Debug, Clone, Copy, PartialEq, Eq)]
817pub enum FeedbackSentiment {
818 Positive,
819 Negative,
820 Neutral,
821}
822
823impl FeedbackSentiment {
824 const fn as_str(self) -> &'static str {
825 match self {
826 Self::Positive => "positive",
827 Self::Negative => "negative",
828 Self::Neutral => "neutral",
829 }
830 }
831}
832
833#[derive(Debug, Clone, PartialEq)]
835pub struct FeedbackAttachmentRequest {
836 pub sentiment: Option<FeedbackSentiment>,
837 pub issues: Vec<FeedbackIssue>,
838 pub desired_response_text: Option<String>,
839 pub desired_response_content: Option<GeneratedContent>,
840 pub desired_output: Option<crate::transcript::Entry>,
841}
842
843impl FeedbackAttachmentRequest {
844 #[must_use]
846 pub const fn new() -> Self {
847 Self {
848 sentiment: None,
849 issues: Vec::new(),
850 desired_response_text: None,
851 desired_response_content: None,
852 desired_output: None,
853 }
854 }
855
856 fn to_bridge_json(&self) -> Result<String, FMError> {
857 let issues = self
858 .issues
859 .iter()
860 .map(|issue| {
861 json!({
862 "category": issue.category.as_str(),
863 "explanation": issue.explanation,
864 })
865 })
866 .collect::<Vec<_>>();
867 let desired_output_json = self
868 .desired_output
869 .as_ref()
870 .map(|entry| Transcript::from(vec![entry.clone()]).to_json_string())
871 .transpose()?;
872 let desired_response_content = self
873 .desired_response_content
874 .as_ref()
875 .map(GeneratedContent::to_bridge_value)
876 .transpose()?;
877 serde_json::to_string(&json!({
878 "sentiment": self.sentiment.map(FeedbackSentiment::as_str),
879 "issues": issues,
880 "desiredResponseText": self.desired_response_text,
881 "desiredResponseContent": desired_response_content,
882 "desiredOutputTranscriptJSON": desired_output_json,
883 }))
884 .map_err(|error| {
885 FMError::InvalidArgument(format!(
886 "feedback request is not JSON-serializable: {error}"
887 ))
888 })
889 }
890}
891
892#[derive(Debug, Deserialize)]
893struct BridgeTextResponse {
894 content: String,
895 #[serde(rename = "rawContent")]
896 raw_content: BridgeGeneratedContent,
897 #[serde(rename = "transcriptJSON")]
898 transcript_json: String,
899}
900
901#[derive(Debug, Deserialize)]
902struct BridgeStructuredResponse {
903 content: BridgeGeneratedContent,
904 #[serde(rename = "rawContent")]
905 raw_content: BridgeGeneratedContent,
906 #[serde(rename = "transcriptJSON")]
907 transcript_json: String,
908}
909
910#[derive(Debug, Deserialize)]
911struct BridgeStructuredSnapshot {
912 content: BridgeGeneratedContent,
913 #[serde(rename = "rawContent")]
914 raw_content: BridgeGeneratedContent,
915 #[serde(rename = "isComplete")]
916 is_complete: bool,
917}
918
919#[derive(Debug, Deserialize)]
920struct BridgeTextStreamSnapshot {
921 delta: String,
922}
923
924fn respond_request_json(
925 prompt: &Prompt,
926 options: GenerationOptions,
927 schema: Option<&GenerationSchema>,
928 include_schema_in_prompt: bool,
929) -> Result<CString, FMError> {
930 let sampling = match options.sampling() {
931 SamplingMode::Default => json!({ "mode": "default" }),
932 SamplingMode::Greedy => json!({ "mode": "greedy" }),
933 SamplingMode::TopK(k) => json!({
934 "mode": "top_k",
935 "topK": k,
936 "seed": options.sampling_seed(),
937 }),
938 SamplingMode::TopP(p) => json!({
939 "mode": "top_p",
940 "topP": p,
941 "seed": options.sampling_seed(),
942 }),
943 };
944 let payload = serde_json::to_string(&json!({
945 "prompt": prompt.to_bridge_value(),
946 "options": {
947 "temperature": options.temperature(),
948 "maximumResponseTokens": options.maximum_response_tokens(),
949 "sampling": sampling,
950 },
951 "schemaJSON": schema.map(GenerationSchema::json_schema),
952 "includeSchemaInPrompt": include_schema_in_prompt,
953 }))
954 .map_err(|error| {
955 FMError::InvalidArgument(format!("request is not JSON-serializable: {error}"))
956 })?;
957 CString::new(payload).map_err(|error| {
958 FMError::InvalidArgument(format!("request JSON contains a NUL byte: {error}"))
959 })
960}
961
962fn request_response(session: *mut c_void, payload: &CString) -> Result<String, FMError> {
963 let (tx, rx) = mpsc::channel();
964 let tx_box: Box<mpsc::Sender<Result<String, FMError>>> = Box::new(tx);
965 let context = Box::into_raw(tx_box).cast::<c_void>();
966 unsafe {
967 ffi::fm_session_respond_request_json(session, payload.as_ptr(), context, respond_trampoline)
968 };
969 rx.recv().map_err(|_| FMError::Unknown {
970 code: ffi::status::UNKNOWN,
971 message: "Swift bridge dropped the JSON response channel".into(),
972 })?
973}
974
975pub(crate) fn decode_bridge_text_response(
976 payload: &str,
977) -> Result<SessionResponse<String>, FMError> {
978 let response: BridgeTextResponse = serde_json::from_str(payload)
979 .map_err(|error| FMError::DecodingFailure(error.to_string()))?;
980 Ok(SessionResponse {
981 content: response.content,
982 raw_content: GeneratedContent::from_bridge_payload(response.raw_content, true)?,
983 transcript: Transcript::from_json_str(&response.transcript_json)?,
984 })
985}
986
987pub(crate) fn request_text_response_with<F>(invoke: F) -> Result<SessionResponse<String>, FMError>
988where
989 F: FnOnce(*mut c_void, ffi::FmRespondCallback),
990{
991 let (tx, rx) = mpsc::channel();
992 let tx_box: Box<mpsc::Sender<Result<String, FMError>>> = Box::new(tx);
993 let context = Box::into_raw(tx_box).cast::<c_void>();
994 invoke(context, respond_trampoline);
995 let payload = rx.recv().map_err(|_| FMError::Unknown {
996 code: ffi::status::UNKNOWN,
997 message: "Swift bridge dropped the JSON response channel".into(),
998 })??;
999 decode_bridge_text_response(&payload)
1000}
1001
1002pub(crate) fn run_text_stream_with<F, C>(invoke: F, on_chunk: C) -> Result<(), FMError>
1003where
1004 F: FnOnce(*mut c_void, ffi::FmStreamCallback),
1005 C: FnMut(StreamEvent<'_>) + Send + 'static,
1006{
1007 let (done_tx, done_rx) = mpsc::channel::<Result<(), FMError>>();
1008 let state = Arc::new(StreamState {
1009 on_chunk: Mutex::new(Box::new(on_chunk)),
1010 done_tx: Mutex::new(Some(done_tx)),
1011 });
1012 let context = Arc::into_raw(state).cast::<c_void>().cast_mut();
1013 invoke(context, json_text_stream_trampoline);
1014 done_rx.recv().map_err(|_| FMError::Unknown {
1015 code: ffi::status::UNKNOWN,
1016 message: "Swift bridge dropped the stream channel".into(),
1017 })?
1018}
1019
1020fn prompt_to_plain_text(prompt: &Prompt) -> Option<String> {
1021 let mut text = String::new();
1022 for segment in prompt.segments() {
1023 match segment {
1024 crate::prompt::Segment::Text(segment) => text.push_str(&segment.text),
1025 crate::prompt::Segment::Structure(_) => return None,
1026 }
1027 }
1028 Some(text)
1029}
1030
1031impl Default for LanguageModelSession {
1032 fn default() -> Self {
1033 Self::new()
1034 }
1035}
1036
1037impl Drop for LanguageModelSession {
1038 fn drop(&mut self) {
1039 if !self.ptr.is_null() {
1040 unsafe { ffi::fm_object_release(self.ptr) };
1041 }
1042 }
1043}
1044
1045impl core::fmt::Debug for LanguageModelSession {
1046 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1047 f.debug_struct("LanguageModelSession")
1048 .field("ptr", &self.ptr)
1049 .finish()
1050 }
1051}
1052
1053#[derive(Debug)]
1055#[non_exhaustive]
1056pub enum StreamEvent<'a> {
1057 Chunk(&'a str),
1059 Done,
1061 Error(FMError),
1063}
1064
1065unsafe extern "C" fn respond_trampoline(
1068 context: *mut c_void,
1069 response: *mut c_char,
1070 error: *mut c_char,
1071 status: i32,
1072) {
1073 let tx = Box::from_raw(context.cast::<mpsc::Sender<Result<String, FMError>>>());
1074 let result = if status == ffi::status::OK && !response.is_null() {
1075 let s = core::ffi::CStr::from_ptr(response)
1076 .to_string_lossy()
1077 .into_owned();
1078 ffi::fm_string_free(response);
1079 Ok(s)
1080 } else {
1081 Err(crate::error::from_swift(status, error))
1082 };
1083 let _ = tx.send(result);
1084}
1085
1086type StreamCallback = Box<dyn FnMut(StreamEvent<'_>) + Send>;
1087
1088struct StreamState {
1089 on_chunk: Mutex<StreamCallback>,
1090 done_tx: Mutex<Option<mpsc::Sender<Result<(), FMError>>>>,
1091}
1092
1093unsafe extern "C" fn json_text_stream_trampoline(
1094 context: *mut c_void,
1095 chunk: *mut c_char,
1096 done: bool,
1097 status: i32,
1098) {
1099 let state = Arc::from_raw(context.cast::<StreamState>());
1100 let state_for_swift = state.clone();
1101 core::mem::forget(state_for_swift);
1102
1103 let payload: Option<String> = if chunk.is_null() {
1104 None
1105 } else {
1106 let value = core::ffi::CStr::from_ptr(chunk)
1107 .to_string_lossy()
1108 .into_owned();
1109 ffi::fm_string_free(chunk);
1110 Some(value)
1111 };
1112
1113 if status != ffi::status::OK {
1114 let err = payload
1115 .map(|message| {
1116 crate::error::from_swift(
1117 status,
1118 ffi::fm_string_dup(
1119 CString::new(message)
1120 .expect("stream errors must not contain NUL bytes")
1121 .as_ptr(),
1122 ),
1123 )
1124 })
1125 .unwrap_or_else(|| crate::error::from_swift(status, ptr::null_mut()));
1126 let mut callback = state.on_chunk.lock().expect("user callback mutex poisoned");
1127 callback(StreamEvent::Error(err.clone()));
1128 drop(callback);
1129 if let Some(tx) = state.done_tx.lock().expect("done_tx mutex poisoned").take() {
1130 let _ = tx.send(Err(err));
1131 }
1132 drop(Arc::from_raw(Arc::as_ptr(&state)));
1133 drop(state);
1134 return;
1135 }
1136
1137 if let Some(payload) = payload {
1138 match serde_json::from_str::<BridgeTextStreamSnapshot>(&payload) {
1139 Ok(snapshot) if !snapshot.delta.is_empty() => {
1140 let mut callback = state.on_chunk.lock().expect("user callback mutex poisoned");
1141 callback(StreamEvent::Chunk(&snapshot.delta));
1142 }
1143 Ok(_) => {}
1144 Err(error) => {
1145 let err = FMError::DecodingFailure(error.to_string());
1146 let mut callback = state.on_chunk.lock().expect("user callback mutex poisoned");
1147 callback(StreamEvent::Error(err.clone()));
1148 drop(callback);
1149 if let Some(tx) = state.done_tx.lock().expect("done_tx mutex poisoned").take() {
1150 let _ = tx.send(Err(err));
1151 }
1152 drop(Arc::from_raw(Arc::as_ptr(&state)));
1153 drop(state);
1154 return;
1155 }
1156 }
1157 }
1158
1159 if done {
1160 let mut callback = state.on_chunk.lock().expect("user callback mutex poisoned");
1161 callback(StreamEvent::Done);
1162 drop(callback);
1163 if let Some(tx) = state.done_tx.lock().expect("done_tx mutex poisoned").take() {
1164 let _ = tx.send(Ok(()));
1165 }
1166 drop(Arc::from_raw(Arc::as_ptr(&state)));
1167 }
1168 drop(state);
1169}
1170
1171type StructuredStreamCallback = Box<dyn FnMut(StructuredStreamEvent) + Send>;
1172
1173struct StructuredStreamState {
1174 on_event: Mutex<StructuredStreamCallback>,
1175 done_tx: Mutex<Option<mpsc::Sender<Result<(), FMError>>>>,
1176}
1177
1178unsafe extern "C" fn structured_stream_trampoline(
1179 context: *mut c_void,
1180 chunk: *mut c_char,
1181 done: bool,
1182 status: i32,
1183) {
1184 let state = Arc::from_raw(context.cast::<StructuredStreamState>());
1185 let state_for_swift = state.clone();
1186 core::mem::forget(state_for_swift);
1187
1188 let payload: Option<String> = if chunk.is_null() {
1189 None
1190 } else {
1191 let value = core::ffi::CStr::from_ptr(chunk)
1192 .to_string_lossy()
1193 .into_owned();
1194 ffi::fm_string_free(chunk);
1195 Some(value)
1196 };
1197
1198 if status != ffi::status::OK {
1199 let err = payload
1200 .map(|message| {
1201 crate::error::from_swift(
1202 status,
1203 ffi::fm_string_dup(
1204 CString::new(message)
1205 .expect("stream errors must not contain NUL bytes")
1206 .as_ptr(),
1207 ),
1208 )
1209 })
1210 .unwrap_or_else(|| crate::error::from_swift(status, ptr::null_mut()));
1211 let mut callback = state
1212 .on_event
1213 .lock()
1214 .expect("structured callback mutex poisoned");
1215 callback(StructuredStreamEvent::Error(err.clone()));
1216 drop(callback);
1217 if let Some(tx) = state
1218 .done_tx
1219 .lock()
1220 .expect("structured done_tx mutex poisoned")
1221 .take()
1222 {
1223 let _ = tx.send(Err(err));
1224 }
1225 drop(Arc::from_raw(Arc::as_ptr(&state)));
1226 drop(state);
1227 return;
1228 }
1229
1230 if let Some(payload) = payload {
1231 let snapshot: BridgeStructuredSnapshot = match serde_json::from_str(&payload) {
1232 Ok(snapshot) => snapshot,
1233 Err(error) => {
1234 let err = FMError::DecodingFailure(error.to_string());
1235 let mut callback = state
1236 .on_event
1237 .lock()
1238 .expect("structured callback mutex poisoned");
1239 callback(StructuredStreamEvent::Error(err.clone()));
1240 drop(callback);
1241 if let Some(tx) = state
1242 .done_tx
1243 .lock()
1244 .expect("structured done_tx mutex poisoned")
1245 .take()
1246 {
1247 let _ = tx.send(Err(err));
1248 }
1249 drop(Arc::from_raw(Arc::as_ptr(&state)));
1250 drop(state);
1251 return;
1252 }
1253 };
1254 let mut callback = state
1255 .on_event
1256 .lock()
1257 .expect("structured callback mutex poisoned");
1258 callback(StructuredStreamEvent::Snapshot(StructuredStreamSnapshot {
1259 content_json: snapshot.content.json,
1260 raw_content_json: snapshot.raw_content.json,
1261 is_complete: snapshot.is_complete,
1262 }));
1263 }
1264
1265 if done {
1266 let mut callback = state
1267 .on_event
1268 .lock()
1269 .expect("structured callback mutex poisoned");
1270 callback(StructuredStreamEvent::Done);
1271 drop(callback);
1272 if let Some(tx) = state
1273 .done_tx
1274 .lock()
1275 .expect("structured done_tx mutex poisoned")
1276 .take()
1277 {
1278 let _ = tx.send(Ok(()));
1279 }
1280 drop(Arc::from_raw(Arc::as_ptr(&state)));
1281 }
1282 drop(state);
1283}