I am using Laravel and OpenAI to generate blog content in real-time using Server-Sent Events (SSE). However, instead of full sentences, the output is coming in broken words, which makes the generated text unreadable.
For example, instead of:
"Freelance work is a great way to become independent."
I get:
"Free lan ce work is a gre at way to be come in dep end ent."
It seems like the response is not properly handling tokenized streaming, causing words to be split incorrectly.
What I Have Tried
1.Ensured correct SSE headers
- My Laravel response already includes:
return response()->stream(function () { ... }, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'X-Accel-Buffering' => 'no',
]);
Current Laravel Controeller:
<?php
namespace AppHttpControllersDashboardBlogAI;
use AppHttpControllersController;
use IlluminateHttpRequest;
use IlluminateSupportFacadesLog;
use AppServicesOpenAIService;
use AppModelsAIDocument;
use AppModelsAIBlog;
class AIWriterController extends Controller
{
protected $openAIService;
public function __construct(OpenAIService $openAIService)
{
$this->openAIService = $openAIService;
}
public function index()
{
Log::info("AIWriterController@index çağrıldı.");
$models = config('openai.models');
$tones = config('openai.tones');
$languages = config('languages.supported');
$settings = $this->openAIService->getSettings();
return view('dashboard.blog.ai.index', compact('models', 'tones', 'languages', 'settings'));
}
public function generate(Request $request)
{
$validated = $request->validate([
'article_title' => 'required|string',
'focus_keywords' => 'nullable|string',
'exclude_keywords' => 'nullable|string',
'default_language' => 'required|string',
'default_tone' => 'required|string',
'max_words' => 'required|integer|min:50|max:2000',
]);
$prompt = "Başlık: " . $validated['article_title'] . "n";
$prompt .= "Anahtar Kelimeler: " . $validated['focus_keywords'] . "n";
$prompt .= "Hariç Tutulacak: " . $validated['exclude_keywords'] . "n";
$prompt .= "Ton: " . $validated['default_tone'] . "n";
$prompt .= "Dil: " . $validated['default_language'] . "n";
return $this->openAIService->generateStreamedText($prompt, $validated['max_words']);
}
public function save(Request $request)
{
Log::info("save metodu çalıştırıldı.", $request->all());
$validated = $request->validate([
'title' => 'required|string',
'description' => 'required|string',
]);
Log::info("save için doğrulama tamamlandı.", $validated);
AIDocument::create([
'title' => $request->title,
'content' => $request->description,
'user_id' => auth()->id(),
]);
Log::info("Makale başarıyla kaydedildi.");
return response()->json(['message' => 'Makale başarıyla kaydedildi.']);
}
public function publish(Request $request)
{
Log::info("publish metodu çalıştırıldı.", $request->all());
$validated = $request->validate([
'title' => 'required|string',
'description' => 'required|string',
]);
Log::info("publish için doğrulama tamamlandı.", $validated);
AIBlog::create([
'title' => $request->title,
'content' => $request->description,
'user_id' => auth()->id(),
'status' => 'published',
]);
Log::info("Makale başarıyla yayınlandı.");
return response()->json(['message' => 'Makale başarıyla yayınlandı.']);
}
}
My service file:
<?php
namespace AppServices;
use GuzzleHttpClient;
use IlluminateSupportFacadesLog;
use SymfonyComponentHttpFoundationStreamedResponse;
use AppServicesOpenAISettingsService;
class OpenAIService
{
protected $client;
protected $settingsService;
protected $settings;
public function __construct(OpenAISettingsService $settingsService)
{
$this->client = new Client();
$this->settingsService = $settingsService;
$this->settings = $this->settingsService->getSettings();
}
public function getSettings()
{
return $this->settings;
}
public function generateStreamedText($prompt, $maxTokens)
{
if (!$this->settings->api_key) {
Log::error("OpenAI API anahtarı eksik!");
return response()->json(['error' => 'OpenAI API anahtarı eksik!'], 400);
}
try {
$jsonData = [
'model' => $this->settings->default_model,
'messages' => [
['role' => 'system', 'content' => 'Sen bir blog yazarı asistansın.'],
['role' => 'user', 'content' => $prompt],
],
'max_tokens' => (int) $maxTokens,
'temperature' => 0.7,
'stream' => true,
];
$response = $this->client->post('https://api.openai.com/v1/chat/completions', [
'headers' => [
'Authorization' => "Bearer " . $this->settings->api_key,
'Content-Type' => 'application/json',
],
'json' => $jsonData,
'stream' => true,
]);
return response()->stream(function () use ($response) {
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
$buffer = "";
$body = $response->getBody();
while (!$body->eof()) {
$chunk = trim($body->read(4096));
if (!empty($chunk)) {
$lines = explode("n", $chunk);
foreach ($lines as $line) {
if (strpos($line, "data: ") === 0) {
$json = json_decode(substr($line, 5), true);
if (isset($json['choices'][0]['delta']['content'])) {
$text = trim($json['choices'][0]['delta']['content']);
$buffer .= $text;
if (preg_match('/s$/', $text) || mb_strlen($buffer) > 6) {
echo "event: updaten";
echo "data: " . $buffer . "nn";
ob_flush();
flush();
$buffer = "";
}
}
}
}
}
}
echo "event: updaten";
echo "data: <END_STREAMING_SSE>nn";
ob_flush();
flush();
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'X-Accel-Buffering' => 'no',
]);
} catch (Exception $e) {
Log::error("OpenAI API Hatası: " . $e->getMessage());
return response()->json(['error' => 'API isteğinde hata oluştu.'], 500);
}
}
}
My js code (included in blade)
<script>
document.addEventListener("DOMContentLoaded", function () {
console.log("JavaScript yüklendi ve çalışıyor!");
document.getElementById('generate-blog').addEventListener('click', function() {
console.log("Blog oluşturma butonuna basıldı!");
let formData = new FormData(document.getElementById('ai-blog-form'));
let outputDiv = document.getElementById('streamedData');
outputDiv.innerHTML = ""; // Önceki içeriği temizle
fetch("{{ route('dashboard.blog.ai.generate') }}", {
method: "POST",
body: formData,
headers: {
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').getAttribute("content")
}
})
.then(response => {
console.log("Fetch response geldi:", response);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.body.getReader();
})
.then(reader => {
let decoder = new TextDecoder();
function readStream() {
reader.read().then(({ done, value }) => {
if (done) {
console.log("Stream tamamlandı.");
return;
}
let text = decoder.decode(value, { stream: true });
console.log("Gelen veri:", text);
let lines = text.split("n");
lines.forEach(line => {
if (line.startsWith("data: ")) {
try {
let kelime = line.replace("data: ", "").trim();
// Her parçadan sonra boşluk ekleyerek yazıyoruz
outputDiv.innerHTML += kelime + " ";
console.log("Ekrana yazılan kelime:", kelime);
} catch (e) {
console.error("JSON parse hatası:", e, line);
}
}
});
readStream();
}).catch(error => console.error("Stream okuma hatası:", error));
}
readStream();
})
.catch(error => console.error("Fetch hatası:", error));
});
});
</script>
Routes:
Route::prefix('blog/ai')->group(function () {
Route::get('/', [AIWriterController::class, 'index'])->name('dashboard.blog.ai.index');
Route::post('/generate', [AIWriterController::class, 'generate'])->name('dashboard.blog.ai.generate');
Route::post('/save', [AIWriterController::class, 'save'])->name('dashboard.blog.ai.save');
Route::post('/publish', [AIWriterController::class, 'publish'])->name('dashboard.blog.ai.publish');
});