Skip to main content

dynamo_renderer/deepseek/
common.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Shared DeepSeek native prompt-formatting helpers.
5
6use anyhow::{Context, Result};
7use serde_json::Value as JsonValue;
8
9use crate::python::PyJsonFormatter;
10
11/// Special tokens for DeepSeek prompt formatting.
12pub mod tokens {
13    pub const BOS: &str = "<|begin▁of▁sentence|>";
14    pub const EOS: &str = "<|end▁of▁sentence|>";
15    pub const THINKING_START: &str = "<think>";
16    pub const THINKING_END: &str = "</think>";
17    pub const DSML_TOKEN: &str = "|DSML|";
18    pub const USER_START: &str = "<|User|>";
19    pub const ASSISTANT_START: &str = "<|Assistant|>";
20    pub const LATEST_REMINDER: &str = "<|latest_reminder|>";
21
22    // Quick-instruction task tokens
23    pub const TASK_ACTION: &str = "<|action|>";
24    pub const TASK_QUERY: &str = "<|query|>";
25    pub const TASK_AUTHORITY: &str = "<|authority|>";
26    pub const TASK_DOMAIN: &str = "<|domain|>";
27    pub const TASK_TITLE: &str = "<|title|>";
28    pub const TASK_READ_URL: &str = "<|read_url|>";
29}
30
31pub(crate) const TOOL_CALLS_BLOCK_NAME: &str = "tool_calls";
32
33pub(crate) const RESPONSE_FORMAT_TEMPLATE: &str =
34    "## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n{schema}";
35
36pub(crate) const TOOLS_TEMPLATE: &str = r#"## Tools
37
38You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<{dsml_token}tool_calls>" block like the following:
39
40<{dsml_token}tool_calls>
41<{dsml_token}invoke name="$TOOL_NAME">
42<{dsml_token}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</{dsml_token}parameter>
43...
44</{dsml_token}invoke>
45<{dsml_token}invoke name="$TOOL_NAME2">
46...
47</{dsml_token}invoke>
48</{dsml_token}tool_calls>
49
50String parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.
51
52If thinking_mode is enabled (triggered by {thinking_start_token}), you MUST output your complete reasoning inside {thinking_start_token}...{thinking_end_token} BEFORE any tool calls or final response.
53
54Otherwise, output directly after {thinking_end_token} with tool calls or final response.
55
56### Available Tool Schemas
57
58{tool_schemas}
59
60You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
61"#;
62
63/// System message template for tools.
64pub(crate) const TOOLS_SYSTEM_TEMPLATE: &str = r#"## Tools
65
66You have access to a set of tools you can use to answer the user's question.
67You can invoke functions by writing a "<{dsml_token}function_calls>" block like the following as part of your reply to the user:
68<{dsml_token}function_calls>
69<{dsml_token}invoke name="$FUNCTION_NAME">
70<{dsml_token}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</{dsml_token}parameter>
71...
72</{dsml_token}invoke>
73<{dsml_token}invoke name="$FUNCTION_NAME2">
74...
75</{dsml_token}invoke>
76</{dsml_token}function_calls>
77
78String and scalar parameters should be specified as is without any escaping or quotes, while lists and objects should use JSON format. The "string" attribute should be set to "true" for string type parameters and "false" for other types (numbers, booleans, arrays, objects).
79
80If the thinking_mode is enabled, then after function results you should strongly consider outputting a thinking block. Here is an example:
81
82<{dsml_token}function_calls>
83...
84</{dsml_token}function_calls>
85
86<function_results>
87...
88</function_results>
89
90{thinking_start_token}...thinking about results{thinking_end_token}
91
92Here are the functions available in JSONSchema format:
93<functions>
94{tool_schemas}
95</functions>
96"#;
97
98pub(crate) const REASONING_EFFORT_HIGH: &str = "Reasoning Effort: Absolute maximum with no shortcuts permitted.\nYou MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\nExplicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n";
99
100pub(crate) const REASONING_EFFORT_MAX: &str = "Reasoning Effort: Beyond maximum — exhaustive, relentless, and uncompromising.\nYou MUST reason with the utmost depth and rigor, leaving absolutely nothing to chance: exhaustively decompose the problem into its most fundamental components, trace every causal chain to its root, and resolve the underlying cause rather than any surface symptom.\nDo not stop reasoning until you have independently verified the solution from multiple angles and are certain that no assumption remains unchecked and no error remains undiscovered.\n\n";
101
102/// Thinking mode for the model.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum ThinkingMode {
105    Chat,
106    Thinking,
107}
108
109impl ThinkingMode {
110    pub fn as_str(&self) -> &'static str {
111        match self {
112            ThinkingMode::Chat => "chat",
113            ThinkingMode::Thinking => "thinking",
114        }
115    }
116}
117
118/// Reasoning effort level. `None` conveyed as `Option<ReasoningEffort>`.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum ReasoningEffort {
121    Max,
122    High,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub(crate) enum NormalizeNonText {
127    SerializeJson,
128    LeaveUntouched,
129}
130
131// Serialize a JSON value to match Python's `json.dumps(ensure_ascii=False)` spacing
132// and float spelling. We use a custom `Formatter` so escape sequences inside strings
133// can't confuse state tracking.
134pub(crate) fn to_json(value: &JsonValue) -> String {
135    use serde::Serialize;
136
137    // Serializing a JsonValue into Vec<u8> is infallible; the output is always UTF-8.
138    let mut buf = Vec::with_capacity(64);
139    let mut ser = serde_json::Serializer::with_formatter(&mut buf, PyJsonFormatter);
140    value
141        .serialize(&mut ser)
142        .expect("JsonValue serialization to Vec<u8> is infallible");
143    String::from_utf8(buf).expect("serde_json output is always valid UTF-8")
144}
145
146pub(crate) fn render_tools(template: &str, tools: &[JsonValue]) -> String {
147    let tools_json: Vec<String> = tools
148        .iter()
149        .filter_map(|tool| tool.get("function"))
150        .map(to_json)
151        .collect();
152
153    // Always do the tool_schemas last because they are user controlled.
154    // See test_render_tools_preserves_placeholder_text_inside_tool_schema.
155    template
156        .replace("{dsml_token}", tokens::DSML_TOKEN)
157        .replace("{thinking_start_token}", tokens::THINKING_START)
158        .replace("{thinking_end_token}", tokens::THINKING_END)
159        .replace("{tool_schemas}", &tools_json.join("\n"))
160}
161
162pub(crate) fn find_last_user_index(messages: &[JsonValue]) -> Option<usize> {
163    messages
164        .iter()
165        .enumerate()
166        .rev()
167        .find(|(_, msg)| {
168            msg.get("role")
169                .and_then(|r| r.as_str())
170                .map(|r| r == "user" || r == "developer")
171                .unwrap_or(false)
172        })
173        .map(|(idx, _)| idx)
174}
175
176pub(crate) fn extract_visible_text(content: &JsonValue) -> String {
177    match content {
178        JsonValue::String(text) => text.clone(),
179        JsonValue::Array(items) => items
180            .iter()
181            .filter_map(|item| {
182                if let Some(text) = item.as_str() {
183                    return Some(text.to_string());
184                }
185                let item_type = item.get("type").and_then(|v| v.as_str());
186                if item_type == Some("text") {
187                    return item
188                        .get("text")
189                        .and_then(|v| v.as_str())
190                        .map(|text| text.to_string());
191                }
192                tracing::warn!(
193                    chunk_type = item_type.unwrap_or("unknown"),
194                    "DeepSeek formatter dropped non-text content chunk while normalizing message content",
195                );
196                None
197            })
198            .collect::<String>(),
199        _ => to_json(content),
200    }
201}
202
203pub(crate) fn normalize_message_contents(messages: &mut [JsonValue], non_text: NormalizeNonText) {
204    for msg in messages {
205        let Some(content) = msg.get("content") else {
206            continue;
207        };
208        // Text content is already normalized. Keep its allocation in place.
209        if content.is_string() {
210            continue;
211        }
212        if !content.is_array() && non_text == NormalizeNonText::LeaveUntouched {
213            continue;
214        }
215        let normalized = extract_visible_text(content);
216        if let Some(obj) = msg.as_object_mut() {
217            obj.insert("content".to_string(), JsonValue::String(normalized));
218        }
219    }
220}
221
222pub(crate) fn encode_arguments_to_dsml(tool_call: &JsonValue) -> Result<String> {
223    let arguments_str = tool_call
224        .get("arguments")
225        .and_then(|a| a.as_str())
226        .context("Missing or invalid 'arguments' field")?;
227
228    // Python falls back to `{"arguments": raw_string}` on parse failure.
229    let arguments: JsonValue = match serde_json::from_str(arguments_str) {
230        Ok(v) => v,
231        Err(_) => serde_json::json!({ "arguments": arguments_str }),
232    };
233
234    let arguments_obj = arguments
235        .as_object()
236        .context("Arguments must be a JSON object")?;
237
238    let mut params = Vec::new();
239    for (key, value) in arguments_obj {
240        let value_str = if let Some(vs) = value.as_str() {
241            vs.to_string()
242        } else {
243            to_json(value)
244        };
245        params.push(format!(
246            "<{}parameter name=\"{}\" string=\"{}\">{}</{}parameter>",
247            tokens::DSML_TOKEN,
248            key,
249            if value.is_string() { "true" } else { "false" },
250            value_str,
251            tokens::DSML_TOKEN
252        ));
253    }
254
255    Ok(params.join("\n"))
256}
257
258pub(crate) fn task_token(task: &str) -> Option<&'static str> {
259    match task {
260        "action" => Some(tokens::TASK_ACTION),
261        "query" => Some(tokens::TASK_QUERY),
262        "authority" => Some(tokens::TASK_AUTHORITY),
263        "domain" => Some(tokens::TASK_DOMAIN),
264        "title" => Some(tokens::TASK_TITLE),
265        "read_url" => Some(tokens::TASK_READ_URL),
266        _ => None,
267    }
268}
269
270const USER_FIELDS_TO_PRESERVE: [&str; 3] = ["task", "wo_eos", "mask"];
271
272fn preserve_user_fields(target: &mut JsonValue, source: &JsonValue) {
273    if let Some(obj) = target.as_object_mut() {
274        for key in USER_FIELDS_TO_PRESERVE {
275            if let Some(v) = source.get(key) {
276                obj.insert(key.to_string(), v.clone());
277            }
278        }
279    }
280}
281
282// Merge `tool` role messages into preceding user `content_blocks` and collapse
283// consecutive user turns with Python-compatible rendering. The internal user
284// representation keeps text only in content_blocks, without Python's duplicate
285// content field.
286pub(crate) fn merge_tool_messages(messages: Vec<JsonValue>) -> Vec<JsonValue> {
287    let mut merged: Vec<JsonValue> = Vec::with_capacity(messages.len());
288
289    for mut msg in messages {
290        let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");
291
292        if role == "tool" {
293            let obj = msg
294                .as_object_mut()
295                .expect("a message with a role is an object");
296            let tool_block = JsonValue::Object(serde_json::Map::from_iter([
297                ("type".into(), JsonValue::String("tool_result".into())),
298                (
299                    "tool_use_id".into(),
300                    obj.remove("tool_call_id")
301                        .unwrap_or_else(|| JsonValue::String(String::new())),
302                ),
303                (
304                    "content".into(),
305                    obj.remove("content")
306                        .unwrap_or_else(|| JsonValue::String(String::new())),
307                ),
308            ]));
309
310            let can_merge = merged
311                .last()
312                .map(|m| {
313                    m.get("role").and_then(|r| r.as_str()) == Some("user")
314                        && m.get("content_blocks").is_some()
315                })
316                .unwrap_or(false);
317
318            if can_merge {
319                let last = merged.last_mut().unwrap();
320                if let Some(blocks) = last
321                    .as_object_mut()
322                    .and_then(|o| o.get_mut("content_blocks"))
323                    .and_then(|v| v.as_array_mut())
324                {
325                    blocks.push(tool_block);
326                }
327            } else {
328                merged.push(JsonValue::Object(serde_json::Map::from_iter([
329                    ("role".into(), JsonValue::String("user".into())),
330                    ("content_blocks".into(), JsonValue::Array(vec![tool_block])),
331                ])));
332            }
333        } else if role == "user" {
334            let text = msg
335                .as_object_mut()
336                .expect("a message with a role is an object")
337                .remove("content")
338                .filter(JsonValue::is_string)
339                .unwrap_or_else(|| JsonValue::String(String::new()));
340            let text_block = JsonValue::Object(serde_json::Map::from_iter([
341                ("type".into(), JsonValue::String("text".into())),
342                ("text".into(), text),
343            ]));
344
345            let can_merge = merged
346                .last()
347                .map(|m| {
348                    m.get("role").and_then(|r| r.as_str()) == Some("user")
349                        && m.get("content_blocks").is_some()
350                        && m.get("task").map(|v| v.is_null()).unwrap_or(true)
351                })
352                .unwrap_or(false);
353
354            if can_merge {
355                // The reference appends only the text; the merged user's own fields are dropped.
356                if let Some(blocks) = merged
357                    .last_mut()
358                    .and_then(|last| last.get_mut("content_blocks"))
359                    .and_then(|v| v.as_array_mut())
360                {
361                    blocks.push(text_block);
362                }
363            } else {
364                // Rendering reads content_blocks; retaining content would copy
365                // the entire user message for an unused second representation.
366                let mut new_msg = JsonValue::Object(serde_json::Map::from_iter([
367                    ("role".into(), JsonValue::String("user".into())),
368                    ("content_blocks".into(), JsonValue::Array(vec![text_block])),
369                ]));
370                preserve_user_fields(&mut new_msg, &msg);
371                merged.push(new_msg);
372            }
373        } else {
374            merged.push(msg);
375        }
376    }
377
378    merged
379}
380
381// Sort `tool_result` blocks within user messages by the `tool_calls[].id` order
382// of the preceding assistant message.
383pub(crate) fn sort_tool_results_by_call_order(mut messages: Vec<JsonValue>) -> Vec<JsonValue> {
384    use std::collections::HashMap;
385    let mut last_order: HashMap<String, usize> = HashMap::new();
386
387    for msg in &mut messages {
388        let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");
389        if role == "assistant" {
390            // Like the reference's `if msg.get("tool_calls")`, an empty list keeps the order.
391            if let Some(tcs) = msg
392                .get("tool_calls")
393                .and_then(|t| t.as_array())
394                .filter(|t| !t.is_empty())
395            {
396                last_order.clear();
397                for (idx, tc) in tcs.iter().enumerate() {
398                    let id = tc
399                        .get("id")
400                        .and_then(|v| v.as_str())
401                        .or_else(|| {
402                            tc.get("function")
403                                .and_then(|f| f.get("id"))
404                                .and_then(|v| v.as_str())
405                        })
406                        .unwrap_or("");
407                    if !id.is_empty() {
408                        last_order.insert(id.to_string(), idx);
409                    }
410                }
411            }
412        } else if role == "user" && !last_order.is_empty() {
413            let Some(blocks) = msg
414                .as_object_mut()
415                .and_then(|o| o.get_mut("content_blocks"))
416                .and_then(|v| v.as_array_mut())
417            else {
418                continue;
419            };
420
421            // Collect tool_result blocks with their positions.
422            let tool_positions: Vec<usize> = blocks
423                .iter()
424                .enumerate()
425                .filter(|(_, b)| b.get("type").and_then(|v| v.as_str()) == Some("tool_result"))
426                .map(|(i, _)| i)
427                .collect();
428
429            if tool_positions.len() > 1 {
430                let start = *tool_positions
431                    .first()
432                    .expect("tool_positions has length > 1");
433                let end = *tool_positions
434                    .last()
435                    .expect("tool_positions has length > 1");
436                let is_contiguous = end - start + 1 == tool_positions.len();
437
438                if is_contiguous {
439                    // Fast path: sort the contiguous slice in place
440                    blocks[start..=end].sort_by_key(|b| {
441                        let id = b.get("tool_use_id").and_then(|v| v.as_str()).unwrap_or("");
442                        *last_order.get(id).unwrap_or(&0)
443                    });
444                } else {
445                    // Fallback: extract, sort, and replace for non-contiguous blocks
446                    let mut tool_blocks: Vec<JsonValue> = tool_positions
447                        .iter()
448                        .map(|&i| std::mem::take(&mut blocks[i]))
449                        .collect();
450
451                    tool_blocks.sort_by_key(|b| {
452                        let id = b.get("tool_use_id").and_then(|v| v.as_str()).unwrap_or("");
453                        *last_order.get(id).unwrap_or(&0)
454                    });
455
456                    for (sorted_idx, &pos) in tool_positions.iter().enumerate() {
457                        blocks[pos] = std::mem::take(&mut tool_blocks[sorted_idx]);
458                    }
459                }
460            }
461        }
462    }
463
464    messages
465}
466
467// Drop reasoning and non-essential messages before the last user message.
468pub(crate) fn drop_thinking_messages(messages: Vec<JsonValue>) -> Vec<JsonValue> {
469    let last_user_idx = find_last_user_index(&messages);
470    let mut out = Vec::with_capacity(messages.len());
471    const KEEP: &[&str] = &[
472        "user",
473        "system",
474        "tool",
475        "latest_reminder",
476        "direct_search_results",
477    ];
478
479    for (idx, mut msg) in messages.into_iter().enumerate() {
480        let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");
481        if KEEP.contains(&role) || last_user_idx.is_none_or(|u| idx >= u) {
482            out.push(msg);
483        } else if role == "assistant" {
484            if let Some(obj) = msg.as_object_mut() {
485                obj.remove("reasoning_content");
486            }
487            out.push(msg);
488        }
489        // developer and other roles before last_user_idx are dropped.
490    }
491    out
492}
493
494pub(crate) fn resolve_thinking_mode(
495    args: Option<&std::collections::HashMap<String, serde_json::Value>>,
496    default_mode: ThinkingMode,
497) -> ThinkingMode {
498    if let Some(enabled) = crate::thinking_bool_from_args(args) {
499        return if enabled {
500            ThinkingMode::Thinking
501        } else {
502            ThinkingMode::Chat
503        };
504    }
505    if let Some(args) = args
506        && let Some(mode) = args.get("thinking_mode").and_then(|v| v.as_str())
507    {
508        match mode {
509            "chat" => return ThinkingMode::Chat,
510            "thinking" => return ThinkingMode::Thinking,
511            _ => {}
512        }
513    }
514    default_mode
515}
516
517pub(crate) fn inject_tools_and_response_format(
518    messages_array: &mut Vec<JsonValue>,
519    req: &dyn crate::OAIChatLikeRequest,
520) -> Result<()> {
521    let tools_json = req
522        .tools()
523        .map(|t| serde_json::to_value(&t))
524        .transpose()
525        .context("Failed to convert tools to JSON")?;
526
527    // OpenAI semantics for `tool_choice: "none"`: the model must not call
528    // tools. Strip the tool definitions from the prompt so the model never
529    // sees the DSML tool instructions and cannot emit raw tool markup.
530    // response_format is kept intact. Mirrors the jinja path's
531    // exclude_tools_when_tool_choice_none handling (template/oai.rs).
532    let tools_json = match req.tool_choice() {
533        Some(ref tc) if tc.as_str() == Some("none") => None,
534        _ => tools_json,
535    };
536
537    let response_format_json = req
538        .response_format()
539        .map(|rf| serde_json::to_value(&rf))
540        .transpose()
541        .context("Failed to convert response_format to JSON")?;
542
543    if tools_json.is_some() || response_format_json.is_some() {
544        let system_idx = messages_array
545            .iter()
546            .position(|msg| msg.get("role").and_then(|r| r.as_str()) == Some("system"));
547
548        if let Some(idx) = system_idx {
549            if let Some(msg) = messages_array.get_mut(idx)
550                && let Some(obj) = msg.as_object_mut()
551            {
552                if let Some(tools) = tools_json {
553                    obj.insert("tools".to_string(), tools);
554                }
555                if let Some(rf) = response_format_json {
556                    obj.insert("response_format".to_string(), rf);
557                }
558            }
559        } else {
560            let mut system_msg = serde_json::json!({
561                "role": "system",
562                "content": ""
563            });
564            if let Some(obj) = system_msg.as_object_mut() {
565                if let Some(tools) = tools_json {
566                    obj.insert("tools".to_string(), tools);
567                }
568                if let Some(rf) = response_format_json {
569                    obj.insert("response_format".to_string(), rf);
570                }
571            }
572            messages_array.insert(0, system_msg);
573        }
574    }
575    Ok(())
576}
577
578#[cfg(test)]
579mod tests {
580
581    #[test]
582    fn to_json_writes_python_floats() {
583        let value = serde_json::json!({"a": 0.000001, "b": [1e16, 3]});
584        assert_eq!(to_json(&value), r#"{"a": 1e-06, "b": [1e+16, 3]}"#);
585    }
586
587    use super::*;
588    use serde_json::json;
589
590    #[test]
591    fn merged_users_drop_their_metadata_like_the_reference() {
592        // Expected output comes from the official DeepSeek-V4-Flash encoding_dsv4.py:
593        // a user merged into a group drops its fields; a group's task stops merging.
594        let merged = merge_tool_messages(vec![
595            json!({"role": "tool", "tool_call_id": "c1", "content": "result"}),
596            json!({"role": "user", "content": "one", "wo_eos": true, "mask": [1, 0]}),
597            json!({"role": "user", "content": "two", "task": "action"}),
598            json!({"role": "assistant", "content": "a"}),
599            json!({"role": "user", "content": "three", "task": "action"}),
600            json!({"role": "user", "content": "four", "wo_eos": true}),
601        ]);
602        assert_eq!(
603            merged,
604            [
605                json!({"role": "user", "content_blocks": [
606                    {"type": "tool_result", "tool_use_id": "c1", "content": "result"},
607                    {"type": "text", "text": "one"},
608                    {"type": "text", "text": "two"}
609                ]}),
610                json!({"role": "assistant", "content": "a"}),
611                json!({"role": "user", "content_blocks": [{"type": "text", "text": "three"}], "task": "action"}),
612                json!({"role": "user", "content_blocks": [{"type": "text", "text": "four"}], "wo_eos": true}),
613            ]
614        );
615    }
616
617    #[test]
618    fn test_extract_visible_text_from_content_array() {
619        let content = json!([
620            {"type": "text", "text": "who "},
621            {"type": "text", "text": "are "},
622            {"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}},
623            {"type": "text", "text": "you?"}
624        ]);
625        assert_eq!(extract_visible_text(&content), "who are you?");
626    }
627
628    #[test]
629    fn test_render_tools_preserves_placeholder_text_inside_tool_schema() {
630        let tools = json!([{
631            "type": "function",
632            "function": {
633                "name": "placeholder_tool",
634                "description": "literal {dsml_token} {thinking_start_token} {thinking_end_token}",
635                "parameters": {"type": "object", "properties": {}}
636            }
637        }]);
638        let rendered = render_tools(
639            "static {dsml_token} {thinking_start_token} {thinking_end_token}\n{tool_schemas}",
640            tools.as_array().unwrap(),
641        );
642
643        assert!(rendered.starts_with(&format!(
644            "static {} {} {}\n",
645            tokens::DSML_TOKEN,
646            tokens::THINKING_START,
647            tokens::THINKING_END
648        )));
649        assert!(
650            rendered.contains("literal {dsml_token} {thinking_start_token} {thinking_end_token}")
651        );
652    }
653}