use std::path::Path;
use bonsai_gguf::{Metadata, MetadataArray};
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use crate::chat::ChatTemplate;
use crate::model::{Model, ModelMetadata};
use crate::special_input::SpecialInputPolicy;
use crate::special_tokens::SpecialTokens;
use crate::tokenization::{
SpecialTokenInsertion, TextTokenizationPolicy, TokenBudget, TokenizationResult,
TokenizationSession,
};
use crate::{Result, invalid};
const MODEL_BYTES: u64 = 7_206_168_928;
const MODEL_SHA256: &str = "5912bb739217cf25b4283a098c7b893d9baff36bdf2e72d3dbf4cb999d5633d5";
const TEMPLATE_SHA256: &str = "c3cf9e34abf4f9e36c2d72165aa9c132d3e2a725b6c2586aaa3a8af9d7a81041";
const VOCAB_SIZE: usize = 248_320;
const NATIVE_STOPS: [(u32, &str); 5] = [
(248_044, "<|endoftext|>"),
(248_046, "<|im_end|>"),
(248_063, "<|fim_pad|>"),
(248_064, "<|repo_name|>"),
(248_065, "<|file_sep|>"),
];
const NATIVE_STOP_IDS: [u32; 5] = [248_044, 248_046, 248_063, 248_064, 248_065];
pub const CONTEXT_TOKENS: usize = 8192;
pub const MAX_OUTPUT_TOKENS: usize = 4096;
pub const MAX_MESSAGE_BYTES: usize = 1024 * 1024;
pub const MAX_MESSAGES: usize = 128;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SwiftPrompt {
pub rendered: String,
pub tokenization: TokenizationResult,
}
pub struct SwiftChatSession {
template: ChatTemplate,
tokenization: TokenizationSession,
}
impl SwiftChatSession {
pub fn load(path: &Path, memory_limit: usize) -> Result<Self> {
let model = ModelMetadata::load(path, MODEL_BYTES, MODEL_SHA256, memory_limit)?;
Self::from_metadata(&model)
}
pub fn from_metadata(model: &ModelMetadata) -> Result<Self> {
Self::from_parts(&model.metadata, model.vocab_size, &model.identity)
}
pub fn from_model(model: &Model) -> Result<Self> {
let embedding = model
.tensors
.get("token_embd.weight")
.ok_or_else(|| invalid("Swift chat requires a token embedding"))?;
let vocab_size = embedding
.dimensions
.get(1)
.copied()
.filter(|_| embedding.dimensions.len() == 2)
.ok_or_else(|| invalid("Swift chat requires a rank-two token embedding"))?;
Self::from_parts(&model.metadata, vocab_size, &model.identity)
}
fn from_parts(metadata: &Metadata, vocab_size: usize, identity: &str) -> Result<Self> {
if identity != MODEL_SHA256 || vocab_size != VOCAB_SIZE {
return Err(invalid(
"Swift chat requires the pinned model identity/vocabulary",
));
}
if metadata
.get("general.architecture")
.and_then(|value| value.as_str())
!= Some("qwen35")
{
return Err(invalid("Swift chat requires the qwen35 architecture"));
}
let source = metadata
.get("tokenizer.chat_template")
.and_then(|value| value.as_str())
.ok_or_else(|| invalid("Swift chat requires its embedded template"))?;
let template_digest: String = Sha256::digest(source.as_bytes())
.into_iter()
.flat_map(|byte| [byte >> 4, byte & 15])
.map(|nibble| char::from(b"0123456789abcdef"[usize::from(nibble)]))
.collect();
if template_digest != TEMPLATE_SHA256 {
return Err(invalid("Swift embedded chat template identity mismatch"));
}
let special = SpecialTokens::from_metadata(metadata, vocab_size)?;
if special.bos_token() != Some(248_044)
|| special.eos_token() != 248_046
|| special.padding_token() != Some(248_044)
|| special.add_bos_token() != Some(false)
|| special.add_eos_token().is_some()
|| special.eot_token().is_some()
|| special.eom_token().is_some()
{
return Err(invalid(
"Swift special-token declarations differ from the pin",
));
}
let Some(MetadataArray::String(tokens)) = metadata
.get("tokenizer.ggml.tokens")
.and_then(|value| value.as_array())
else {
return Err(invalid("Swift chat requires a string vocabulary"));
};
let bos = tokens
.get(248_044)
.ok_or_else(|| invalid("missing Swift BOS spelling"))?;
let eos = tokens
.get(248_046)
.ok_or_else(|| invalid("missing Swift EOS spelling"))?;
for (id, spelling) in NATIVE_STOPS {
if tokens.get(usize::try_from(id)?).map(String::as_str) != Some(spelling) {
return Err(invalid("Swift native stop vocabulary identity mismatch"));
}
}
let template = ChatTemplate::new(source, bos, eos)?;
let tokenization = TokenizationSession::from_parts(metadata, vocab_size, identity)?;
Ok(Self {
template,
tokenization,
})
}
#[must_use]
pub fn identity(&self) -> &str {
self.tokenization.identity()
}
#[must_use]
pub const fn template_sha256(&self) -> &'static str {
TEMPLATE_SHA256
}
#[must_use]
pub const fn tokenization(&self) -> &TokenizationSession {
&self.tokenization
}
#[must_use]
pub const fn special_tokens(&self) -> &SpecialTokens {
self.tokenization.special_tokens()
}
#[must_use]
pub const fn stop_tokens(&self) -> &'static [u32] {
&NATIVE_STOP_IDS
}
pub fn generation(&self, max_tokens: usize) -> Result<crate::runtime::Generation> {
output_budget(max_tokens)?;
self.special_tokens()
.generation(max_tokens, self.stop_tokens())
}
pub fn prepare(&self, messages: &Value, reserved_output_tokens: usize) -> Result<SwiftPrompt> {
let budget = output_budget(reserved_output_tokens)?;
validate_messages(messages)?;
let rendered = self.template.render_json(
messages.clone(),
true,
&json!({"enable_thinking": true, "reasoning_effort": "xhigh", "preserve_thinking": true}),
)?;
let tokenization = self.tokenization.encode(
&rendered,
TextTokenizationPolicy {
special_input: SpecialInputPolicy::Parse,
insert_special: SpecialTokenInsertion::None,
budget,
},
)?;
Ok(SwiftPrompt {
rendered,
tokenization,
})
}
}
fn output_budget(reserved_output_tokens: usize) -> Result<TokenBudget> {
if !(1..=MAX_OUTPUT_TOKENS).contains(&reserved_output_tokens) {
return Err(invalid(
"Swift output allowance must be from 1 through 4096 tokens",
));
}
Ok(TokenBudget {
context: CONTEXT_TOKENS,
reserved_output_tokens,
})
}
pub(crate) fn validate_messages(messages: &Value) -> Result<()> {
let messages = messages
.as_array()
.ok_or_else(|| invalid("Swift messages must be an array"))?;
if messages.is_empty() || messages.len() > MAX_MESSAGES {
return Err(invalid("Swift message count exceeds supported bounds"));
}
let mut supplied_bytes = 0_usize;
for (index, message) in messages.iter().enumerate() {
let object = message
.as_object()
.ok_or_else(|| invalid("Swift message must be an object"))?;
if object
.keys()
.any(|key| !matches!(key.as_str(), "role" | "content" | "reasoning_content"))
{
return Err(invalid("unsupported Swift message field"));
}
let role = message
.get("role")
.and_then(Value::as_str)
.ok_or_else(|| invalid("Swift message role must be text"))?;
if !(matches!(role, "user" | "assistant") || role == "system" && index == 0) {
return Err(invalid("unsupported Swift message role"));
}
let content = message
.get("content")
.and_then(Value::as_str)
.ok_or_else(|| invalid("Swift message content must be text"))?;
supplied_bytes = supplied_bytes
.checked_add(content.len())
.ok_or_else(|| invalid("Swift message byte bound exceeded"))?;
if let Some(reasoning) = message.get("reasoning_content") {
if role != "assistant" {
return Err(invalid(
"Swift reasoning history requires an assistant turn",
));
}
let reasoning = reasoning
.as_str()
.ok_or_else(|| invalid("Swift reasoning history must be text"))?;
supplied_bytes = supplied_bytes
.checked_add(reasoning.len())
.ok_or_else(|| invalid("Swift message byte bound exceeded"))?;
}
if supplied_bytes > MAX_MESSAGE_BYTES {
return Err(invalid("Swift message byte bound exceeded"));
}
}
if messages
.last()
.and_then(|message| message.get("role"))
.and_then(Value::as_str)
!= Some("user")
{
return Err(invalid("Swift chat generation requires a final user turn"));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{MAX_MESSAGE_BYTES, output_budget, validate_messages};
use serde_json::json;
#[test]
fn swift_chat_accepts_supplied_reasoning_and_exact_bounds() -> crate::Result<()> {
validate_messages(&json!([
{"role":"system", "content":"Keep whitespace.\n"},
{"role":"user", "content":"before"},
{"role":"assistant", "content":"answer", "reasoning_content":"private\nthought"},
{"role":"user", "content":" after "}
]))?;
assert_eq!(output_budget(4096)?.prompt_token_limit()?, 4096);
assert!(output_budget(0).is_err());
assert!(output_budget(4097).is_err());
Ok(())
}
#[test]
fn swift_chat_rejects_unsupported_shapes_before_rendering() {
for messages in [
json!([]),
json!({}),
json!([{"role":"assistant","content":"answer"}]),
json!([{"role":"user","content":"q","reasoning_content":"r"}]),
json!([{"role":"user","content":[{"type":"text","text":"q"}]}]),
json!([{"role":"user","content":"q","tools":[]}]),
json!([{"role":"tool","content":"q"}]),
json!([{"role":"user","content":"q"},{"role":"system","content":"s"}]),
json!([{"role":"assistant","content":"a","reasoning_content":null},{"role":"user","content":"q"}]),
json!([{"role":"user","content":"x".repeat(MAX_MESSAGE_BYTES+1)}]),
] {
assert!(validate_messages(&messages).is_err());
}
}
}