C.W.K.
Stream
Lesson 03 of 04 · published

SSE Parsing 과 Dual Adapter

~14 min · sse, raw-http, adapter, dual-auth

Level 0Spark
0 XP0/35 lessons0/10 achievements
0/140 XP to next level140 XP to go0% complete

SDK 없을 때 SSE 직접 parse

가끔 loop 에 SDK 안 두고 싶음 — proxy 하는 중, 작은 컨테이너 위, 공식 SDK 가 완전히 지원 안 하는 OAuth-only endpoint 호출 중. SSE 포맷이 간단해서 직접 parse 가능: data: 로 시작하는 line, event 사이 빈 line, prefix 후 JSON payload.

Dual-adapter 패턴

두 인증 path 를 single interface 뒤에 wrap. Caller 가 stream 요청; adapter 가 가용성 + 최근 실패 history 기반으로 API key 또는 OAuth 선택. Fallback 이 session 에 sticky — OAuth 에서 API key 로 한 번 flip 하면 (예: 401 시) session 나머지 동안 API key 유지. 그렇지 않으면 oscillate.

Code

SSE parser — async, line-based·python
import httpx, json
from typing import AsyncIterator

async def stream_gemini_api_key(
    prompt: str, api_key: str, model: str = 'gemini-2.5-flash',
) -> AsyncIterator[str]:
    url = (
        f'https://generativelanguage.googleapis.com/v1beta/models/{model}'
        ':streamGenerateContent?alt=sse'
    )
    body = {'contents': [{'role': 'user', 'parts': [{'text': prompt}]}]}

    async with httpx.AsyncClient(timeout=120) as client:
        async with client.stream('POST', url,
            headers={'x-goog-api-key': api_key, 'Content-Type': 'application/json'},
            json=body,
        ) as resp:
            async for line in resp.aiter_lines():
                if not line.startswith('data: '):
                    continue
                chunk = json.loads(line[6:])
                for cand in chunk.get('candidates', []):
                    for part in cand.get('content', {}).get('parts', []):
                        if text := part.get('text'):
                            yield text
OAuth variant — 같은 모양, 다른 envelope·python
async def stream_gemini_oauth(
    prompt: str, access_token: str, project: str,
    model: str = 'gemini-2.5-flash',
) -> AsyncIterator[str]:
    url = 'https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse'
    body = {
        'model':   model,
        'project': project,
        'request': {
            'contents': [{'role': 'user', 'parts': [{'text': prompt}]}],
            'generationConfig': {},
        },
    }

    async with httpx.AsyncClient(timeout=120) as client:
        async with client.stream('POST', url,
            headers={
                'Authorization': f'Bearer {access_token}',
                'Content-Type':  'application/json',
            },
            json=body,
        ) as resp:
            async for line in resp.aiter_lines():
                if not line.startswith('data: '):
                    continue
                chunk = json.loads(line[6:])
                # OAuth wraps the response inside chunk['response']
                resp_obj = chunk.get('response', chunk)
                for cand in resp_obj.get('candidates', []):
                    for part in cand.get('content', {}).get('parts', []):
                        if text := part.get('text'):
                            yield text
Dual adapter — sticky fallback·python
class GeminiDualAdapter:
    def __init__(self, api_key: str, oauth_creds_path: str):
        self.api_key = api_key
        self.oauth_creds_path = oauth_creds_path
        self.fallback_active = False  # session-sticky

    async def stream(self, prompt: str):
        if not self.fallback_active:
            try:
                token   = get_access_token()  # from previous lesson
                project = load_code_assist(token)
                async for text in stream_gemini_oauth(
                    prompt, token, project,
                ):
                    yield text
                return
            except Exception as e:
                # Toast it visibly — never silent
                print(f'[OAuth failed: {e}. Falling back to API key, sticky for session.]')
                self.fallback_active = True

        # API key path
        async for text in stream_gemini_api_key(prompt, self.api_key):
            yield text

External links

Exercise

세 번째 코드 블록의 dual adapter 빌드. 너 실제 credential 로 두 path 다 wire. OAuth path 에 실패 강제 (예: credential 파일 임시 corrupt), 확인: (a) fallback 이 visible 메시지로 engage, (b) credential fix 해도 같은 process 의 이후 호출이 API key path 유지.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.