การใช้งาน Server-Sent Events (SSE) สำหรับ streaming response จาก Large Language Model เป็น pattern ที่ได้รับความนิยมอย่างมากในปัจจุบัน แต่การจัดการ timeout ที่ไม่เหมาะสมอาจทำให้แอปพลิเคชันของคุณ hang, leak memory หรือแม้แต่ crash อย่างไม่คาดคิด บทความนี้จะพาคุณเจาะลึกวิธีการจัดการ SSE timeout อย่างมีประสิทธิภาพเมื่อใช้งานผ่าน HolySheep AI API relay พร้อมโค้ดตัวอย่างที่พร้อมใช้งานจริง

ทำความรู้จัก SSE และปัญหา Timeout

Server-Sent Events เป็นเทคโนโลยีที่ช่วยให้ server ส่งข้อมูลไปยัง client ได้แบบ real-time โดยใช้ HTTP connection เดียว เมื่อใช้งานกับ LLM API เช่น GPT หรือ Claude แต่ละ token ที่ generate จะถูกส่งมาทีละตัว ทำให้ผู้ใช้เห็นผลลัพธ์ได้ทันทีโดยไม่ต้องรอ response ทั้งหมด

ปัญหาหลักที่พบบ่อยคือ:

ตารางเปรียบเทียบ: HolySheep vs API อย่างเป็นทางการ vs บริการรีเลย์อื่นๆ

คุณสมบัติ HolySheep AI OpenAI API บริการรีเลย์ทั่วไป
Latency เฉลี่ย <50ms 150-300ms 80-200ms
SSE Timeout Support ✅ Native + Auto-retry ⚠️ ต้องตั้งค่าเอง ⚠️ บางรายไม่รองรับ
Error Recovery ✅ รองรับ streaming resume ❌ ต้อง restart ⚠️ ขึ้นอยู่กับผู้ให้บริการ
ราคา GPT-4.1 $8/MTok (อัตรา ¥1=$1) $2/MTok $2.5-4/MTok
ราคา Claude Sonnet 4.5 $15/MTok $3/MTok $4-6/MTok
ราคา Gemini 2.5 Flash $2.50/MTok $0.125/MTok $0.30-0.50/MTok
ราคา DeepSeek V3.2 $0.42/MTok ไม่มี $0.50-0.80/MTok
วิธีชำระเงิน WeChat/Alipay บัตรเครดิต/PayPal หลากหลาย
เครดิตฟรี ✅ มีเมื่อลงทะเบียน $5 trial น้อยครั้ง
Streaming Buffer ✅ Intelligent chunking ❌ Raw streaming ⚠️ ขึ้นอยู่กับผู้ให้บริการ

การตั้งค่า SSE Timeout ใน HolySheep API Relay

เมื่อใช้งาน HolySheep เป็น relay คุณจะได้รับประโยชน์จาก timeout handling ที่ถูก optimize แล้ว แต่ยังคงต้องตั้งค่า client-side ให้เหมาะสม เพื่อประสบการณ์ที่ราบรื่นที่สุด

พื้นฐาน: SSE Client ใน JavaScript/TypeScript

// การตั้งค่า SSE Client พร้อม Timeout ที่เหมาะสม
const BASE_URL = 'https://api.holysheep.ai/v1';
const API_KEY = 'YOUR_HOLYSHEEP_API_KEY';

class HolySheepStreamingClient {
  private baseUrl: string;
  private apiKey: string;
  
  constructor(apiKey: string) {
    this.baseUrl = BASE_URL;
    this.apiKey = apiKey;
  }

  async *streamChatCompletion(
    model: string,
    messages: Array<{ role: string; content: string }>,
    options: {
      timeout?: number;        // timeout รวม (ms)
      readTimeout?: number;    // timeout ระหว่างข้อมูล (ms)
      maxRetries?: number;
    } = {}
  ): AsyncGenerator<string, void, unknown> {
    const {
      timeout = 120000,        // 2 นาที
      readTimeout = 60000,     // 1 นาทีไม่มีข้อมูล
      maxRetries = 3
    } = options;

    let lastEventId: string | null = null;
    
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        const response = await fetch(${this.baseUrl}/chat/completions, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': Bearer ${this.apiKey},
            'Accept': 'text/event-stream',
            'X-Request-ID': crypto.randomUUID()
          },
          body: JSON.stringify({
            model: model,
            messages: messages,
            stream: true,
            stream_options: { include_usage: true }
          }),
          signal: AbortSignal.timeout(timeout)
        });

        if (!response.ok) {
          const error = await response.json().catch(() => ({}));
          throw new Error(API Error: ${response.status} - ${JSON.stringify(error)});
        }

        const reader = response.body?.getReader();
        if (!reader) throw new Error('No response body');

        const decoder = new TextDecoder();
        let buffer = '';
        let lastDataTime = Date.now();

        try {
          while (true) {
            const { done, value } = await this.readWithTimeout(
              reader,
              readTimeout,
              () => Date.now() - lastDataTime
            );

            if (done) break;

            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split('\n');
            buffer = lines.pop() || '';

            for (const line of lines) {
              if (line.startsWith('data: ')) {
                const data = line.slice(6);
                if (data === '[DONE]') return;
                
                try {
                  const parsed = JSON.parse(data);
                  if (parsed.choices?.[0]?.delta?.content) {
                    lastDataTime = Date.now();
                    yield parsed.choices[0].delta.content;
                  }
                  if (parsed.id) lastEventId = parsed.id;
                } catch (parseError) {
                  console.warn('Parse error:', parseError);
                }
              }
            }
          }
        } finally {
          reader.releaseLock();
        }

        return; // Success

      } catch (error) {
        if (attempt === maxRetries) throw error;
        
        const waitTime = Math.min(1000 * Math.pow(2, attempt), 10000);
        console.warn(Attempt ${attempt + 1} failed, retrying in ${waitTime}ms...);
        await this.sleep(waitTime);
      }
    }
  }

  private async readWithTimeout(
    reader: ReadableStreamDefaultReader,
    timeout: number,
    getIdleTime: () => number
  ): Promise<{ done: boolean; value: Uint8Array }> {
    return new Promise((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        reader.cancel();
        reject(new Error(Read timeout after ${getIdleTime()}ms idle));
      }, timeout);

      reader.read().then(
        (result) => {
          clearTimeout(timeoutId);
          resolve(result);
        },
        (error) => {
          clearTimeout(timeoutId);
          reject(error);
        }
      );
    });
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// วิธีใช้งาน
const client = new HolySheepStreamingClient('YOUR_HOLYSHEEP_API_KEY');

async function main() {
  try {
    const stream = client.streamChatCompletion('gpt-4.1', [
      { role: 'user', content: 'อธิบาย quantum computing ให้เข้าใจง่าย' }
    ], {
      timeout: 120000,
      readTimeout: 60000,
      maxRetries: 3
    });

    let fullResponse = '';
    for await (const chunk of stream) {
      process.stdout.write(chunk);
      fullResponse += chunk;
    }
    console.log('\n\nFull response length:', fullResponse.length);
  } catch (error) {
    console.error('Streaming failed:', error);
  }
}

main();

Python Implementation ด้วย httpx

# Python SSE Client พร้อม Timeout Handling ที่แข็งแกร่ง
import asyncio
import httpx
import json
from typing import AsyncGenerator, Optional, Callable
from dataclasses import dataclass
from datetime import datetime, timedelta

BASE_URL = 'https://api.holysheep.ai/v1'
API_KEY = 'YOUR_HOLYSHEEP_API_KEY'

@dataclass
class StreamConfig:
    """การตั้งค่าสำหรับ SSE streaming"""
    total_timeout: float = 120.0  # timeout รวม (วินาที)
    read_timeout: float = 60.0    # timeout ระหว่างข้อมูล (วินาที)
    max_retries: int = 3
    retry_delay: float = 1.0      # วินาที
    retry_multiplier: float = 2.0
    max_retry_delay: float = 10.0

class HolySheepStreamError(Exception):
    """Custom exception สำหรับ streaming errors"""
    def __init__(self, message: str, error_type: str = 'unknown', retryable: bool = True):
        super().__init__(message)
        self.error_type = error_type
        self.retryable = retryable

class HolySheepSSEClient:
    """SSE Client พร้อม comprehensive timeout handling"""
    
    def __init__(self, api_key: str, base_url: str = BASE_URL):
        self.api_key = api_key
        self.base_url = base_url
        self.config = StreamConfig()
        self._request_count = 0
    
    async def stream_chat_completion(
        self,
        model: str,
        messages: list[dict],
        system_prompt: Optional[str] = None,
        temperature: float = 0.7,
        max_tokens: int = 4096,
        on_chunk: Optional[Callable[[str], None]] = None,
        on_complete: Optional[Callable[[dict], None]] = None,
        on_error: Optional[Callable[[Exception], None]] = None
    ) -> str:
        """
        Stream chat completion พร้อม timeout handling
        
        Args:
            model: ชื่อ model (เช่น 'gpt-4.1', 'claude-sonnet-4.5')
            messages: รายการ messages
            on_chunk: callback เมื่อได้รับ chunk ใหม่
            on_complete: callback เมื่อ stream เสร็จสมบูรณ์
            on_error: callback เมื่อเกิด error
            
        Returns:
            full_text: text ทั้งหมดที่ได้รับ
        """
        all_messages = []
        if system_prompt:
            all_messages.append({'role': 'system', 'content': system_prompt})
        all_messages.extend(messages)
        
        full_text = ''
        last_data_time = datetime.now()
        attempt = 0
        
        while attempt <= self.config.max_retries:
            try:
                async with httpx.AsyncClient(
                    timeout=httpx.Timeout(
                        connect=10.0,
                        read=self.config.total_timeout,
                        write=10.0,
                        pool=5.0
                    ),
                    limits=httpx.Limits(max_keepalive_connections=20, max_connections=100)
                ) as client:
                    
                    async with client.stream(
                        'POST',
                        f'{self.base_url}/chat/completions',
                        json={
                            'model': model,
                            'messages': all_messages,
                            'stream': True,
                            'stream_options': {'include_usage': True},
                            'temperature': temperature,
                            'max_tokens': max_tokens
                        },
                        headers={
                            'Authorization': f'Bearer {self.api_key}',
                            'Content-Type': 'application/json',
                            'Accept': 'text/event-stream',
                            'X-Request-ID': f'req_{self._request_count}_{int(datetime.now().timestamp())}'
                        }
                    ) as response:
                        
                        if response.status_code != 200:
                            error_body = await response.aread()
                            raise HolySheepStreamError(
                                f'HTTP {response.status_code}: {error_body.decode()}',
                                error_type='http_error',
                                retryable=response.status_code >= 500
                            )
                        
                        buffer = ''
                        async for line in response.aiter_lines():
                            if not line:
                                continue
                                
                            if line.startswith('data: '):
                                data = line[6:]
                                
                                if data == '[DONE]':
                                    if on_complete:
                                        on_complete({'text': full_text, 'chunks': self._request_count})
                                    return full_text
                                
                                try:
                                    parsed = json.loads(data)
                                    delta = parsed.get('choices', [{}])[0].get('delta', {})
                                    content = delta.get('content', '')
                                    
                                    if content:
                                        last_data_time = datetime.now()
                                        full_text += content
                                        if on_chunk:
                                            on_chunk(content)
                                            
                                except json.JSONDecodeError:
                                    pass
                                    
                            # Check read timeout (no data for too long)
                            idle_time = (datetime.now() - last_data_time).total_seconds()
                            if idle_time > self.config.read_timeout:
                                raise HolySheepStreamError(
                                    f'Read timeout: no data received for {idle_time:.1f}s',
                                    error_type='read_timeout',
                                    retryable=True
                                )
                        
                        # Stream completed normally
                        return full_text
                        
            except HolySheepStreamError as e:
                if not e.retryable or attempt == self.config.max_retries:
                    if on_error:
                        on_error(e)
                    raise
                    
                attempt += 1
                wait_time = min(
                    self.config.retry_delay * (self.config.retry_multiplier ** (attempt - 1)),
                    self.config.max_retry_delay
                )
                print(f'Attempt {attempt} failed: {e}. Retrying in {wait_time:.1f}s...')
                await asyncio.sleep(wait_time)
                
            except Exception as e:
                if on_error:
                    on_error(e)
                raise HolySheepStreamError(str(e), error_type='unknown', retryable=True)
                
            finally:
                self._request_count += 1
        
        raise HolySheepStreamError('Max retries exceeded', error_type='max_retries')

ตัวอย่างการใช้งาน

async def main(): client = HolySheepSSEClient(API_KEY) client.config.read_timeout = 60.0 # 60 วินาทีไม่มีข้อมูล chunks_received = 0 def on_chunk(chunk: str): nonlocal chunks_received chunks_received += 1 print(chunk, end='', flush=True) def on_complete(info: dict): print(f'\n\n✓ Stream เสร็จสมบูรณ์') print(f' - จำนวน chunks: {chunks_received}') print(f' - ความยาว text: {len(info["text"])} ตัวอักษร') try: result = await client.stream_chat_completion( model='gpt-4.1', messages=[ {'role': 'user', 'content': 'เล่าประวัติ AI สั้นๆ 5 บรรทัด'} ], on_chunk=on_chunk, on_complete=on_complete ) except HolySheepStreamError as e: print(f'\n❌ Streaming failed: {e}') if __name__ == '__main__': asyncio.run(main())

กลยุทธ์ Timeout ขั้นสูง

Adaptive Timeout ตาม Response Length

// Advanced: Adaptive timeout ที่ปรับตาม model และ expected response
interface ModelProfile {
  name: string;
  avgTokensPerChar: number;
  baseTimeout: number;      // seconds
  readTimeout: number;      // seconds
  charsPerSecond: number;   // estimated streaming speed
}

const MODEL_PROFILES: Record<string, ModelProfile> = {
  'gpt-4.1': {
    name: 'GPT-4.1',
    avgTokensPerChar: 0.25,
    baseTimeout: 180,
    readTimeout: 90,
    charsPerSecond: 150
  },
  'claude-sonnet-4.5': {
    name: 'Claude Sonnet 4.5',
    avgTokensPerChar: 0.28,
    baseTimeout: 240,
    readTimeout: 120,
    charsPerSecond: 120
  },
  'gemini-2.5-flash': {
    name: 'Gemini 2.5 Flash',
    avgTokensPerChar: 0.22,
    baseTimeout: 60,
    readTimeout: 30,
    charsPerSecond: 300
  },
  'deepseek-v3.2': {
    name: 'DeepSeek V3.2',
    avgTokensPerChar: 0.26,
    baseTimeout: 120,
    readTimeout: 60,
    charsPerSecond: 200
  }
};

function calculateAdaptiveTimeout(
  model: string,
  promptLength: number,
  estimatedResponseTokens: number
): { totalTimeout: number; readTimeout: number } {
  const profile = MODEL_PROFILES[model] || MODEL_PROFILES['gpt-4.1'];
  
  // ประมาณความยาว response เป็น characters
  const estimatedChars = estimatedResponseTokens / profile.avgTokensPerChar;
  
  // คำนวณเวลาที่ใช้ streaming
  const streamingTime = estimatedChars / profile.charsPerSecond;
  
  // เพิ่ม buffer 50% และ minimum timeout
  const totalTimeout = Math.max(
    profile.baseTimeout,
    streamingTime * 1.5 + 30 // เพิ่ม 30 วินาทีสำหรับ processing
  );
  
  // Read timeout = เวลาเฉลี่ยที่รอระหว่าง chunks
  const avgChunkInterval = estimatedChars / 100 / profile.charsPerSecond;
  const readTimeout = Math.max(
    profile.readTimeout,
    avgChunkInterval * 3 // 3x ค่าเฉลี่ย
  );
  
  return {
    totalTimeout: Math.round(totalTimeout) * 1000, // แปลงเป็น ms
    readTimeout: Math.round(readTimeout) * 1000
  };
}

// การใช้งาน
const timeouts = calculateAdaptiveTimeout(
  'gpt-4.1',
  promptLength = 500,
  estimatedResponseTokens = 2000
);

const stream = client.streamChatCompletion('gpt-4.1', messages, {
  timeout: timeouts.totalTimeout,
  readTimeout: timeouts.readTimeout
});

ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข

กรณีที่ 1: "Connection timeout after 30s" — ECONNABORTED

สาเหตุ: Server ไม่ตอบสนองภายในเวลาที่กำหนด มักเกิดจากการตั้งค่า timeout ต่ำเกินไป หรือ server กำลังประมวลผลหนัก

โค้ดแก้ไข:
// ❌ ผิด: timeout ต่ำเกินไป
const response = await fetch(url, {
  signal: AbortSignal.timeout(5000) // แค่ 5 วินาที
});

// ✅ ถูก: timeout ที่เหมาะสมสำหรับ LLM streaming
const response = await fetch(url, {
  signal: AbortSignal.timeout(120000), // 2 นาที
  // หรือใช้ AbortController สำหรับควบคุมที่ยืดหยุ่นกว่า
});

// ตัวอย่างการใช้ AbortController
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 120000);

try {
  const response = await fetch(url, {
    signal: controller.signal
  });
  // process response...
} catch (error) {
  if (error.name === 'AbortError') {
    console.error('Request timeout - no response within 120 seconds');
    // จัดการ timeout: แสดง error ให้ user, เสนอ retry
  }
} finally {
  clearTimeout(timeoutId);
}

กรณีที่ 2: "Read timeout: no data received for 60s" — Streaming หยุดกลางคัน

สาเหตุ: ไม่มีข้อมูลมาถึง client นานเกินไป อาจเกิดจาก network issue, server overload, หรือ model ใช้เวลา generate นานผิดปกติ

โค้ดแก้ไข:
// ✅ ถูก: ตั้งค่า readTimeout ที่เหมาะสม + auto-resume

class ResilientSSEClient {
  constructor(private maxResumeAttempts = 3) {}
  
  async streamWithResume(url: string, options: StreamOptions): Promise<string> {
    let fullText = '';
    let lastEventId: string | null = null;
    
    for (let attempt = 0; attempt < this.maxResumeAttempts; attempt++) {
      try {
        const response = await this.fetchWithReadTimeout(url, {
          ...options,
          headers: {
            ...options.headers,
            // ส่ง Last-Event-ID เพื่อขอ resume จากจุดที่หยุด
            ...(lastEventId && { 'X-Last-Event-ID': lastEventId })
          }
        });
        
        for await (const chunk of this.parseSSE(response)) {
          if (chunk.id) lastEventId = chunk.id;
          if (chunk.data) {
            fullText += chunk.data;
            // callback เมื่อได้รับข้อมูล
            options.onChunk?.(chunk.data);
          }
        }
        
        return fullText; // Success
        
      } catch (error) {
        if (error instanceof ReadTimeoutError) {
          console.warn(Read timeout on attempt ${attempt + 1}, retrying...);
          const waitTime = Math.min(1000 * Math.pow(2, attempt), 8000);
          await this.sleep(waitTime);
          continue;
        }
        throw error; // ไม่ใช่ timeout error
      }
    }
    
    throw new Error(Failed after ${this.maxResumeAttempts} attempts);
  }
  
  private async fetchWithReadTimeout(
    url: string, 
    options: RequestInit
  ): Promise<Response> {
    const controller = new AbortController();
    const readTimeout = options.readTimeout || 60000;
    
    const timeoutId = setTimeout(() => {
      controller.abort(new ReadTimeoutError(No data for ${readTimeout}ms));
    }, readTimeout);
    
    try {
      return await fetch(url, { ...options, signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }
}

// ใน Python
class ReadTimeoutError(Exception):
    pass

async def stream_with_retry(client, url, headers, read_timeout=60, max_retries=3):
    """Stream พร้อม auto-retry เมื่อ read timeout"""
    
    async def attempt_stream(attempt):
        async with httpx.AsyncClient() as http_client:
            last_data_time = asyncio.get_event_loop().time()
            
            async with http_client.stream(
                'GET', url, headers=headers, timeout=httpx.Timeout(60.0)
            ) as response:
                
                buffer = b''
                async for raw_data in response.aiter_bytes():
                    buffer += raw_data
                    last_data_time = asyncio.get_event_loop().time()
                    
                    # Process buffer...
                    
                # Check if timed out
                idle_time = asyncio.get_event_loop().time() - last_data_time
                if idle_time > read_timeout:
                    raise ReadTimeoutError(f"No data for {idle_time:.1f}s")
    
    for attempt in range(max_retries):
        try:
            return await attempt_stream(attempt)
        except ReadTimeoutError:
            if attempt == max_retries - 1:
                raise
            await asyncio.sleep(min(2 ** attempt, 10))

กรณีที่ 3: "CORS error on streaming response" — Preflight ล้มเหลว

สาเหตุ: Browser ส่ง OPTIONS preflight request ก่อน แต่ไม่ได้รับ response ที่ถูกต้อง หรือ headers ไม่ครบ

โค้ดแก้ไข:
// ✅ ถูก: ตั้งค่า CORS headers ที่ถูกต้องสำหรั