Skip to content

Dev/issues 1#146

Closed
luk384090-cloud wants to merge 11 commits into
mainfrom
dev/issues-1
Closed

Dev/issues 1#146
luk384090-cloud wants to merge 11 commits into
mainfrom
dev/issues-1

Conversation

@luk384090-cloud

Copy link
Copy Markdown
Collaborator

Description

[Describe what this PR does and why]

Related Issue: Fixes #[issue_number] or Relates to #[issue_number]

Security Considerations: [Check if API keys or sensitive credentials are exposed in code/logs]

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Refactoring

Component(s) Affected

  • Model
  • Application
  • Common
  • Documentation
  • Tests
  • CI/CD

Checklist

  • Pre-commit hooks pass
  • Tests pass locally
  • Documentation updated (if needed)
  • Ready for review

Testing

[How to test these changes]

Additional Notes

[Optional: any other context]

zhansheng.lzs and others added 11 commits June 12, 2026 14:12
Replace per-request aiohttp.ClientSession creation with a shared session
pool (one per event loop) to enable HTTP connection reuse across async API
calls. SSL context is cached and shared across all sessions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Streaming requests were using aiohttp.ClientTimeout(total=...) which kills
the connection after the deadline even if the model is still actively
producing tokens. Switch to ClientTimeout(sock_read=...) for streaming
requests so the timeout only triggers on idle connections (no data between
chunks). Non-streaming requests still use total timeout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The atexit handler created a new event loop to close sessions bound to
the original loop, which silently failed. OS reclaims resources on exit,
so explicit close is unnecessary. Also adds stale-entry cleanup in
get_shared_aio_session() to prevent unbounded dict growth when event
loops are repeatedly created and closed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ings to English

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The server currently does not support session.finish event for
qwen3.5-omni-flash-realtime, returning an error instead of
session.finished. Previously end_session() would wait the full
timeout duration (20s) before falling back to ws.close().

Now the method detects server error events immediately and closes
the connection without waiting, reducing wait time from 20s to
<0.1s when the server rejects session.finish.

Related to issue #140
tiktoken's recursive BPE merging overflows the call stack on very
long inputs, causing pyo3_runtime.PanicException: StackOverflow.

Split text into chunks (<= 100k chars) at line boundaries before
encoding. Short text takes the original fast path unchanged.

Fixes #116

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a shared aiohttp session pool with a cached SSL context to enable connection reuse across async API calls, refactors request handlers to utilize this pool, adds support for merging incremental application response chunks, and implements chunking in the Qwen tokenizer to prevent stack overflows on long inputs. The review feedback highlights several critical improvement opportunities: addressing thread-safety issues in the shared session pool by introducing a lock, optimizing string concatenation in the tokenizer's chunking logic to avoid O(N^2) complexity, performing case-insensitive lookups for the User-Agent header, and raising an exception instead of silently returning when a server error occurs during end_session.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +17 to +54
_shared_ssl_context: Optional[ssl.SSLContext] = None
_aio_sessions: "weakref.WeakKeyDictionary" = weakref.WeakKeyDictionary()


def get_ssl_context() -> ssl.SSLContext:
global _shared_ssl_context
if _shared_ssl_context is None:
_shared_ssl_context = ssl.create_default_context(
cafile=certifi.where(),
)
return _shared_ssl_context


async def get_shared_aio_session() -> aiohttp.ClientSession:
"""Return a shared aiohttp.ClientSession bound to the running event loop.

The session is lazily created on first use and reused for all
subsequent calls on the same event loop. Connection pooling (keep-alive)
is handled by the underlying TCPConnector.
"""
loop = asyncio.get_running_loop()

session = _aio_sessions.get(loop)
if session is not None and not session.closed:
return session

connector = aiohttp.TCPConnector(ssl=get_ssl_context())
session = aiohttp.ClientSession(connector=connector, trust_env=True)
_aio_sessions[loop] = session
return session


async def close_shared_aio_session() -> None:
"""Close the shared session for the current event loop."""
loop = asyncio.get_running_loop()
session = _aio_sessions.pop(loop, None)
if session is not None and not session.closed:
await session.close()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The global _aio_sessions dictionary is a weakref.WeakKeyDictionary, which is implemented in pure Python and is not thread-safe. In multi-threaded environments (such as web servers like Uvicorn/Gunicorn or multi-threaded workers), concurrent calls to get_shared_aio_session() from different threads will modify _aio_sessions concurrently, leading to race conditions, KeyError, or RuntimeError.

We should introduce a threading.Lock to synchronize access to _aio_sessions and _shared_ssl_context.

import threading

_shared_ssl_context: Optional[ssl.SSLContext] = None
_aio_sessions: "weakref.WeakKeyDictionary" = weakref.WeakKeyDictionary()
_lock = threading.Lock()


def get_ssl_context() -> ssl.SSLContext:
    global _shared_ssl_context
    if _shared_ssl_context is None:
        with _lock:
            if _shared_ssl_context is None:
                _shared_ssl_context = ssl.create_default_context(
                    cafile=certifi.where(),
                )
    return _shared_ssl_context


async def get_shared_aio_session() -> aiohttp.ClientSession:
    """Return a shared aiohttp.ClientSession bound to the running event loop.

    The session is lazily created on first use and reused for all
    subsequent calls on the same event loop. Connection pooling (keep-alive)
    is handled by the underlying TCPConnector.
    """
    loop = asyncio.get_running_loop()

    with _lock:
        session = _aio_sessions.get(loop)
        if session is not None and not session.closed:
            return session

        connector = aiohttp.TCPConnector(ssl=get_ssl_context())
        session = aiohttp.ClientSession(connector=connector, trust_env=True)
        _aio_sessions[loop] = session
        return session


async def close_shared_aio_session() -> None:
    """Close the shared session for the current event loop."""
    loop = asyncio.get_running_loop()
    with _lock:
        session = _aio_sessions.pop(loop, None)
    if session is not None and not session.closed:
        await session.close()

Comment on lines +140 to +146
chunks: List[str] = []
for part in parts:
if chunks and len(chunks[-1]) + len(part) <= chunk_size:
chunks[-1] += part
else:
chunks.append(part)
return chunks

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In _split_text, the chunks are merged using repeated string concatenation (chunks[-1] += part). Because Python strings are immutable, this operation has O(N^2) time complexity relative to the number of parts. For very large inputs (which is exactly when this chunking logic is triggered), this can cause severe performance degradation, high CPU usage, or timeouts.

We should accumulate the parts in a list and join them using "".join() when the chunk size limit is reached. This is a standard Python best practice and runs in O(N) time.

Suggested change
chunks: List[str] = []
for part in parts:
if chunks and len(chunks[-1]) + len(part) <= chunk_size:
chunks[-1] += part
else:
chunks.append(part)
return chunks
chunks: List[str] = []
current_chunk: List[str] = []
current_len = 0
for part in parts:
if current_len + len(part) <= chunk_size:
current_chunk.append(part)
current_len += len(part)
else:
if current_chunk:
chunks.append("".join(current_chunk))
current_chunk = [part]
current_len = len(part)
if current_chunk:
chunks.append("".join(current_chunk))
return chunks

Comment on lines +164 to +171
if "headers" not in kwargs:
kwargs["headers"] = {}
flag = "1" if to_merge_incremental_output else "0"
existing_ua = kwargs["headers"].get("user-agent", "")
new_ua = f"incremental_to_full/{flag}"
kwargs["headers"]["user-agent"] = (
f"{existing_ua} {new_ua}".strip() if existing_ua else new_ua
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When appending the incremental_to_full flag to the user-agent header, the code assumes the key is exactly lowercase "user-agent". However, users or other parts of the SDK might pass "User-Agent" (capitalized). This case-sensitivity mismatch can result in duplicate User-Agent headers (both "User-Agent" and "user-agent"), which can cause unexpected behavior or failures in HTTP clients.

We should perform a case-insensitive lookup for the User-Agent header key and update it accordingly.

        if "headers" not in kwargs:
            kwargs["headers"] = {}
        flag = "1" if to_merge_incremental_output else "0"
        ua_key = "user-agent"
        existing_ua = ""
        for k, v in list(kwargs["headers"].items()):
            if k.lower() == "user-agent":
                ua_key = k
                existing_ua = v
                break
        new_ua = f"incremental_to_full/{flag}"
        kwargs["headers"][ua_key] = (
            f"{existing_ua} {new_ua}".strip() if existing_ua else new_ua
        )

Comment on lines +371 to +374
# if the server returned an error or timed out, close the connection
if error is not None:
self.close()
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In end_session, if the server returns an error during the disconnect process, the code closes the connection and returns silently. This hides the error from the caller, who will assume the session ended successfully. It is better to raise an exception so the caller is notified of the failure.

Suggested change
# if the server returned an error or timed out, close the connection
if error is not None:
self.close()
return
# if the server returned an error or timed out, close the connection
if error is not None:
self.close()
raise RuntimeError(f"Session ended with error: {error}")

@lzsweb lzsweb closed this Jun 18, 2026
@lzsweb lzsweb deleted the dev/issues-1 branch June 18, 2026 02:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants