| 4793 | char *think_end = strstr(body, "</think>"); |
| 4794 | if (think_end) { |
| 4795 | *think_end = '\0'; |
| 4796 | *reasoning_out = xstrdup(body); |
| 4797 | *content_out = xstrdup(think_end + 8); |
| 4798 | } else { |
| 4799 | *reasoning_out = NULL; |
| 4800 | *content_out = xstrdup(s); |
| 4801 | } |
| 4802 | free(s); |
| 4803 | } |
| 4804 | |
| 4805 | /* Unterminated reasoning is not an answer. |
| 4806 | * |
| 4807 | * When generation hits the token cap before </think> arrives, the old behaviour |
| 4808 | * routed the whole unterminated buffer into content, so a truncated reasoning |
| 4809 | * chain reached clients looking exactly like a finished reply. split_reasoning_ |
| 4810 | * content() cannot tell that case apart from a legitimate non-thinking answer |
| 4811 | * (neither carries a </think>, and the buffer does not retain the opening tag), |
| 4812 | * so the discrimination has to happen here, where require_thinking_closed |
| 4813 | * already tells us thinking was expected. |
| 4814 | * |
| 4815 | * Routing the buffer to reasoning_content and emptying content makes truncation |
| 4816 | * self-describing on the wire for every consumer, not just ones that check |
| 4817 | * finish_reason. content is set to "" rather than NULL deliberately: the JSON |
| 4818 | * writer guards NULL, but not every path in this file has been audited for it, |
| 4819 | * and an empty answer is already unmistakably not an answer. */ |
| 4820 | static void ds4_local_unterminated_reasoning(const char *text, |
| 4821 | char **content_out, |
| 4822 | char **reasoning_out) { |