withToken($key) ->post(rtrim((string) ($config['base_url'] ?? 'https://api.together.xyz/v1'), '/').'/chat/completions', [ 'model' => $model, 'temperature' => 0, 'response_format' => ['type' => 'json_object'], 'messages' => [ ['role' => 'system', 'content' => 'Classify the comment as spam or not spam. Return only JSON: {"spam":true|false,"confidence":0.0,"reason":"short explanation"}. Confidence must be a number from 0 to 1. Spam includes advertising, SEO promotion, unsolicited links, scams, and bot-like promotional repetition.'], ['role' => 'user', 'content' => mb_substr($content, 0, (int) ($config['max_input_chars'] ?? 2500))], ], ]); if ($response->failed()) { throw new RuntimeException('Together AI request failed with status '.$response->status().'.'); } $message = Arr::get($response->json(), 'choices.0.message.content'); $decoded = is_string($message) ? json_decode($message, true) : null; if (! is_array($decoded) || ! is_bool($decoded['spam'] ?? null) || ! is_numeric($decoded['confidence'] ?? null) || $decoded['confidence'] < 0 || $decoded['confidence'] > 1 || ! is_string($decoded['reason'] ?? null)) { throw new RuntimeException('Together AI returned invalid moderation JSON.'); } return new CommentSpamClassification( $decoded['spam'], (float) $decoded['confidence'], mb_substr(trim($decoded['reason']), 0, 500), 'together', $model, (int) round((microtime(true) - $started) * 1000), ); } }