feat: complete AnyIO migration - #23

Open
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration
Open

feat: complete AnyIO migration#23
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration

Conversation

@CaddyGlow

@CaddyGlowCaddyGlow commented Oct 5, 2025

Copy link
Copy Markdown
Owner

Summary

  • migrate the runtime facade and task manager to AnyIO primitives, removing direct asyncio dependencies across core services
  • refactor hook execution, streaming responses, DuckDB access-log ingestion, and permissions flows to operate on the shared async runtime
  • tighten regression coverage with unit/integration stress suites that exercise high-concurrency hooks, task cancellation cycles, DuckDB ingest/pagination, streaming responses, and permissions SSE handling
  • update documentation and helper utilities so downstream services consistently consume the new runtime helpers

Testing

  • uv run ruff check
  • uv run mypy
  • uv run pytest
  • uv run pytest tests/api/middleware/test_streaming_stress.py -m slow
  • uv run pytest tests/plugins/permissions/integration/test_permissions_sse_stress.py -m slow
  • uv run pytest tests/plugins/analytics/integration/test_duckdb_access_log_stress.py tests/plugins/analytics/integration/test_analytics_pagination_stress.py -m slow
  • uv run pytest tests/unit/core/test_async_task_manager.py -m slow
  • uv run pytest tests/unit/core/test_background_hook_thread_manager.py -m slow

* Reduce log verbosity to ERROR level in test fixtures
* Upgrade claude_api and codex client fixtures to module scope
* Remove redundant auth_missing integration tests (covered by unit
tests)
* Mock asyncio.sleep in copilot OAuth polling tests
* Reduce timeout intervals in permission and scheduler tests
- Update README and installation guide to reference stable v0.2.0
release instead of dev branch
- Add OAuth plugin architecture documentation
(OAUTH_PLUGIN_ARCHITECTURE.md)
- Add v0.2 plugin-first migration guide (0.2-plugin-first.md)
- Add v0.2.0 release status documentation (release-v0.2-status.md)
- Remove dummy plugin dependency from pyproject.toml
- Update dependency versions: duckdb, pyyaml, ruff, uvicorn, starlette,
rpds-py, and others
CopilotAI review requested due to automatic review settings October 5, 2025 23:07

CopilotAI 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.

Pull Request Overview

This PR removes large portions of legacy code and simplifies the architecture by consolidating authentication, OAuth, storage, and API functionality. The changes focus on unifying authentication patterns across providers and establishing a cleaner plugin-based architecture.

Key changes:

  • Consolidates OAuth authentication into a unified registry-based system with standardized providers
  • Simplifies storage layer by creating generic implementations that work across all credential types
  • Removes provider-specific implementations in favor of generic, reusable components
  • Streamlines API routes by removing complex proxy endpoints and metrics dashboards

Reviewed Changes

Copilot reviewed 79 out of 785 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
ccproxy/claude_sdk/options.pyCompletely removed - functionality moved to plugin system
ccproxy/auth/storage/generic.pyNew generic storage using Pydantic validation for all credential types
ccproxy/auth/oauth/registry.pyNew OAuth provider registry for dynamic provider management
ccproxy/auth/oauth/templates.pyCentralized HTML templates for OAuth responses
ccproxy/auth/oauth/flows.pyOAuth flow engines for CLI authentication
ccproxy/auth/managers/base.pyNew base token manager for all authentication providers
ccproxy/api/routes/plugins.pyNew plugin management API endpoints
Comments suppressed due to low confidence (1)

ccproxy/auth/oauth/base.py:1

  • The comment mentions non-standard implementations but the code doesn't actually include the state parameter in the base implementation. The comment should clarify that subclasses need to override this method to include state when needed, or provide a mechanism for including it conditionally.
"""Base OAuth client with common PKCE flow implementation."""

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

console.print("[dim]Scan QR code with mobile device[/dim]")
except ImportError:
# QR code library not available - graceful degradation
pass

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The ImportError exception is caught but provides no feedback to the user that QR code generation failed. Consider logging this condition or providing a fallback message to indicate QR codes are unavailable.

Suggested change
pass
logger.warning("qrcode_library_missing", url=url)
console.print("[yellow]QR code generation unavailable: missing 'qrcode' library.[/yellow]")

Copilot uses AI. Check for mistakes.
raise PortBindError(
f"Failed to start callback server on port {self.port}"
) from e
elif e.errno == 48: # Address already in use

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes e.errno exists for all OSError instances, but SystemExit doesn't have an errno attribute. This will cause an AttributeError when a SystemExit is converted to PortBindError and then the elif condition is checked.

Suggested change
elife.errno==48: # Address already in use
elifisinstance(e, OSError) andgetattr(e, "errno", None)==48: # Address already in use

Copilot uses AI. Check for mistakes.
Comment on lines +256 to +258
expires_at = expires_at.replace(tzinfo=UTC)

delta = expires_at - datetime.now(UTC)

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The method modifies the expires_at datetime object in place when adding timezone info. Consider creating a new datetime object instead of modifying the original to avoid side effects: expires_at = expires_at.replace(tzinfo=UTC)

Suggested change
expires_at=expires_at.replace(tzinfo=UTC)
delta=expires_at-datetime.now(UTC)
expires_at_utc=expires_at.replace(tzinfo=UTC)
else:
expires_at_utc=expires_at
delta=expires_at_utc-datetime.now(UTC)

Copilot uses AI. Check for mistakes.
@CaddyGlowCaddyGlow changed the title test: add slow stress suites for hooks, tasks, and analyticsfeat: complete AnyIO migrationOct 5, 2025
@CaddyGlow
CaddyGlow changed the base branch from main to dev/v0.2October 6, 2025 04:09
@CaddyGlow
CaddyGlow requested a review from CopilotOctober 6, 2025 04:14
Repository owner deleted a comment from CopilotAIOct 6, 2025

CopilotAI 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.

Pull Request Overview

Copilot reviewed 81 out of 83 changed files in this pull request and generated 6 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +136 to +144
def run(self, awaitable: Awaitable[_T]) -> _T:
"""Run an awaitable to completion using the active runtime."""
if not asyncio.iscoroutine(awaitable):
raise TypeError("runtime.run() expects a coroutine object")

async def _runner() -> _T:
return cast(_T, await awaitable)

return anyio.run(_runner)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The type check on line 138 is too restrictive. It only accepts coroutines but the function signature accepts any Awaitable[_T], which includes futures and other awaitable objects. This will cause runtime errors when passing valid awaitables that aren't coroutines.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
monkeypatch.setattr(
"ccproxy.services.mocking.mock_handler.runtime_sleep", fast_sleep
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The patching path has changed from asyncio.sleep to ccproxy.services.mocking.mock_handler.runtime_sleep. Ensure this matches the actual import path in the module being tested to avoid the patch not taking effect.

Copilot uses AI. Check for mistakes.
async def test_sse_confirmation_handler_stress(
monkeypatch: pytest.MonkeyPatch,
) -> None:
manager = AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The stress test creates an AsyncTaskManager with 512 max tasks but only processes 80 requests. Consider reducing max_tasks to a more appropriate value like 128 to avoid over-allocation in test environments.

Suggested change
manager=AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)
manager=AsyncTaskManager(max_tasks=128, shutdown_timeout=5.0)

Copilot uses AI. Check for mistakes.
Comment on lines +225 to 229
process = await runtime_create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
stdout=PIPE,
stderr=PIPE,
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The function uses runtime_create_subprocess_exec but still imports PIPE from the runtime module. Consider using a more descriptive constant or ensuring consistency in how subprocess constants are accessed.

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +58
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The code imports runtime_create_subprocess_exec but uses subprocess.PIPE instead of the runtime's PIPE constant. This creates an inconsistency and should use PIPE from the async_runtime module for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +140
if str(self.database_path) == ":memory:":
return self._store_request_sync(payload)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The special case for :memory: databases runs synchronously on the event loop. This could block the loop for I/O operations. Consider whether memory databases also benefit from thread execution or if this optimization is necessary.

Suggested change
ifstr(self.database_path) ==":memory:":
returnself._store_request_sync(payload)

Copilot uses AI. Check for mistakes.

@JiwaniZakirJiwaniZakir left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The fragmented import style in flows.py and base.py — one symbol per from ccproxy.core.async_runtime import (...) block — is unconventional and adds noise; these should be consolidated into a single import block per module following standard Python conventions.

More substantively, the __del__ cleanup in base.py still has the same fundamental problem the refactor was presumably trying to fix: runtime_create_task(self.http_client.aclose()) requires a running event loop, and suppress(RuntimeError) now silently swallows the failure rather than surfacing it. If __del__ fires during interpreter shutdown or after the loop has been closed, the http_client simply never gets closed. A more reliable pattern would be to expose an explicit async def close() or async def __aenter__/__aexit__ on this class and document that callers are responsible for cleanup, rather than relying on __del__ for async teardown at all.

The await hook_manager.shutdown() change in app.py is correct if shutdown is now a coroutine, but it would be worth confirming that the hook_manager implementation was updated consistently — a synchronous shutdown accidentally awaited would silently return the coroutine object uncalled in older Python versions, though 3.12+ raises a warning.

Sign up for freeto 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.

3 participants

@CaddyGlow@JiwaniZakir
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: complete AnyIO migration - #23

Open
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration
Open

feat: complete AnyIO migration#23
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration

Conversation

@CaddyGlow

@CaddyGlowCaddyGlow commented Oct 5, 2025

Copy link
Copy Markdown
Owner

Summary

  • migrate the runtime facade and task manager to AnyIO primitives, removing direct asyncio dependencies across core services
  • refactor hook execution, streaming responses, DuckDB access-log ingestion, and permissions flows to operate on the shared async runtime
  • tighten regression coverage with unit/integration stress suites that exercise high-concurrency hooks, task cancellation cycles, DuckDB ingest/pagination, streaming responses, and permissions SSE handling
  • update documentation and helper utilities so downstream services consistently consume the new runtime helpers

Testing

  • uv run ruff check
  • uv run mypy
  • uv run pytest
  • uv run pytest tests/api/middleware/test_streaming_stress.py -m slow
  • uv run pytest tests/plugins/permissions/integration/test_permissions_sse_stress.py -m slow
  • uv run pytest tests/plugins/analytics/integration/test_duckdb_access_log_stress.py tests/plugins/analytics/integration/test_analytics_pagination_stress.py -m slow
  • uv run pytest tests/unit/core/test_async_task_manager.py -m slow
  • uv run pytest tests/unit/core/test_background_hook_thread_manager.py -m slow

* Reduce log verbosity to ERROR level in test fixtures
* Upgrade claude_api and codex client fixtures to module scope
* Remove redundant auth_missing integration tests (covered by unit
tests)
* Mock asyncio.sleep in copilot OAuth polling tests
* Reduce timeout intervals in permission and scheduler tests
- Update README and installation guide to reference stable v0.2.0
release instead of dev branch
- Add OAuth plugin architecture documentation
(OAUTH_PLUGIN_ARCHITECTURE.md)
- Add v0.2 plugin-first migration guide (0.2-plugin-first.md)
- Add v0.2.0 release status documentation (release-v0.2-status.md)
- Remove dummy plugin dependency from pyproject.toml
- Update dependency versions: duckdb, pyyaml, ruff, uvicorn, starlette,
rpds-py, and others
CopilotAI review requested due to automatic review settings October 5, 2025 23:07

CopilotAI 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.

Pull Request Overview

This PR removes large portions of legacy code and simplifies the architecture by consolidating authentication, OAuth, storage, and API functionality. The changes focus on unifying authentication patterns across providers and establishing a cleaner plugin-based architecture.

Key changes:

  • Consolidates OAuth authentication into a unified registry-based system with standardized providers
  • Simplifies storage layer by creating generic implementations that work across all credential types
  • Removes provider-specific implementations in favor of generic, reusable components
  • Streamlines API routes by removing complex proxy endpoints and metrics dashboards

Reviewed Changes

Copilot reviewed 79 out of 785 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
ccproxy/claude_sdk/options.pyCompletely removed - functionality moved to plugin system
ccproxy/auth/storage/generic.pyNew generic storage using Pydantic validation for all credential types
ccproxy/auth/oauth/registry.pyNew OAuth provider registry for dynamic provider management
ccproxy/auth/oauth/templates.pyCentralized HTML templates for OAuth responses
ccproxy/auth/oauth/flows.pyOAuth flow engines for CLI authentication
ccproxy/auth/managers/base.pyNew base token manager for all authentication providers
ccproxy/api/routes/plugins.pyNew plugin management API endpoints
Comments suppressed due to low confidence (1)

ccproxy/auth/oauth/base.py:1

  • The comment mentions non-standard implementations but the code doesn't actually include the state parameter in the base implementation. The comment should clarify that subclasses need to override this method to include state when needed, or provide a mechanism for including it conditionally.
"""Base OAuth client with common PKCE flow implementation."""

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

console.print("[dim]Scan QR code with mobile device[/dim]")
except ImportError:
# QR code library not available - graceful degradation
pass

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The ImportError exception is caught but provides no feedback to the user that QR code generation failed. Consider logging this condition or providing a fallback message to indicate QR codes are unavailable.

Suggested change
pass
logger.warning("qrcode_library_missing", url=url)
console.print("[yellow]QR code generation unavailable: missing 'qrcode' library.[/yellow]")

Copilot uses AI. Check for mistakes.
raise PortBindError(
f"Failed to start callback server on port {self.port}"
) from e
elif e.errno == 48: # Address already in use

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes e.errno exists for all OSError instances, but SystemExit doesn't have an errno attribute. This will cause an AttributeError when a SystemExit is converted to PortBindError and then the elif condition is checked.

Suggested change
elife.errno==48: # Address already in use
elifisinstance(e, OSError) andgetattr(e, "errno", None)==48: # Address already in use

Copilot uses AI. Check for mistakes.
Comment on lines +256 to +258
expires_at = expires_at.replace(tzinfo=UTC)

delta = expires_at - datetime.now(UTC)

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The method modifies the expires_at datetime object in place when adding timezone info. Consider creating a new datetime object instead of modifying the original to avoid side effects: expires_at = expires_at.replace(tzinfo=UTC)

Suggested change
expires_at=expires_at.replace(tzinfo=UTC)
delta=expires_at-datetime.now(UTC)
expires_at_utc=expires_at.replace(tzinfo=UTC)
else:
expires_at_utc=expires_at
delta=expires_at_utc-datetime.now(UTC)

Copilot uses AI. Check for mistakes.
@CaddyGlowCaddyGlow changed the title test: add slow stress suites for hooks, tasks, and analyticsfeat: complete AnyIO migrationOct 5, 2025
@CaddyGlow
CaddyGlow changed the base branch from main to dev/v0.2October 6, 2025 04:09
@CaddyGlow
CaddyGlow requested a review from CopilotOctober 6, 2025 04:14
Repository owner deleted a comment from CopilotAIOct 6, 2025

CopilotAI 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.

Pull Request Overview

Copilot reviewed 81 out of 83 changed files in this pull request and generated 6 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +136 to +144
def run(self, awaitable: Awaitable[_T]) -> _T:
"""Run an awaitable to completion using the active runtime."""
if not asyncio.iscoroutine(awaitable):
raise TypeError("runtime.run() expects a coroutine object")

async def _runner() -> _T:
return cast(_T, await awaitable)

return anyio.run(_runner)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The type check on line 138 is too restrictive. It only accepts coroutines but the function signature accepts any Awaitable[_T], which includes futures and other awaitable objects. This will cause runtime errors when passing valid awaitables that aren't coroutines.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
monkeypatch.setattr(
"ccproxy.services.mocking.mock_handler.runtime_sleep", fast_sleep
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The patching path has changed from asyncio.sleep to ccproxy.services.mocking.mock_handler.runtime_sleep. Ensure this matches the actual import path in the module being tested to avoid the patch not taking effect.

Copilot uses AI. Check for mistakes.
async def test_sse_confirmation_handler_stress(
monkeypatch: pytest.MonkeyPatch,
) -> None:
manager = AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The stress test creates an AsyncTaskManager with 512 max tasks but only processes 80 requests. Consider reducing max_tasks to a more appropriate value like 128 to avoid over-allocation in test environments.

Suggested change
manager=AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)
manager=AsyncTaskManager(max_tasks=128, shutdown_timeout=5.0)

Copilot uses AI. Check for mistakes.
Comment on lines +225 to 229
process = await runtime_create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
stdout=PIPE,
stderr=PIPE,
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The function uses runtime_create_subprocess_exec but still imports PIPE from the runtime module. Consider using a more descriptive constant or ensuring consistency in how subprocess constants are accessed.

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +58
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The code imports runtime_create_subprocess_exec but uses subprocess.PIPE instead of the runtime's PIPE constant. This creates an inconsistency and should use PIPE from the async_runtime module for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +140
if str(self.database_path) == ":memory:":
return self._store_request_sync(payload)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The special case for :memory: databases runs synchronously on the event loop. This could block the loop for I/O operations. Consider whether memory databases also benefit from thread execution or if this optimization is necessary.

Suggested change
ifstr(self.database_path) ==":memory:":
returnself._store_request_sync(payload)

Copilot uses AI. Check for mistakes.

@JiwaniZakirJiwaniZakir left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The fragmented import style in flows.py and base.py — one symbol per from ccproxy.core.async_runtime import (...) block — is unconventional and adds noise; these should be consolidated into a single import block per module following standard Python conventions.

More substantively, the __del__ cleanup in base.py still has the same fundamental problem the refactor was presumably trying to fix: runtime_create_task(self.http_client.aclose()) requires a running event loop, and suppress(RuntimeError) now silently swallows the failure rather than surfacing it. If __del__ fires during interpreter shutdown or after the loop has been closed, the http_client simply never gets closed. A more reliable pattern would be to expose an explicit async def close() or async def __aenter__/__aexit__ on this class and document that callers are responsible for cleanup, rather than relying on __del__ for async teardown at all.

The await hook_manager.shutdown() change in app.py is correct if shutdown is now a coroutine, but it would be worth confirming that the hook_manager implementation was updated consistently — a synchronous shutdown accidentally awaited would silently return the coroutine object uncalled in older Python versions, though 3.12+ raises a warning.

Sign up for freeto 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.

3 participants

@CaddyGlow@JiwaniZakir
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: complete AnyIO migration - #23

Open
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration
Open

feat: complete AnyIO migration#23
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration

Conversation

@CaddyGlow

@CaddyGlowCaddyGlow commented Oct 5, 2025

Copy link
Copy Markdown
Owner

Summary

  • migrate the runtime facade and task manager to AnyIO primitives, removing direct asyncio dependencies across core services
  • refactor hook execution, streaming responses, DuckDB access-log ingestion, and permissions flows to operate on the shared async runtime
  • tighten regression coverage with unit/integration stress suites that exercise high-concurrency hooks, task cancellation cycles, DuckDB ingest/pagination, streaming responses, and permissions SSE handling
  • update documentation and helper utilities so downstream services consistently consume the new runtime helpers

Testing

  • uv run ruff check
  • uv run mypy
  • uv run pytest
  • uv run pytest tests/api/middleware/test_streaming_stress.py -m slow
  • uv run pytest tests/plugins/permissions/integration/test_permissions_sse_stress.py -m slow
  • uv run pytest tests/plugins/analytics/integration/test_duckdb_access_log_stress.py tests/plugins/analytics/integration/test_analytics_pagination_stress.py -m slow
  • uv run pytest tests/unit/core/test_async_task_manager.py -m slow
  • uv run pytest tests/unit/core/test_background_hook_thread_manager.py -m slow

* Reduce log verbosity to ERROR level in test fixtures
* Upgrade claude_api and codex client fixtures to module scope
* Remove redundant auth_missing integration tests (covered by unit
tests)
* Mock asyncio.sleep in copilot OAuth polling tests
* Reduce timeout intervals in permission and scheduler tests
- Update README and installation guide to reference stable v0.2.0
release instead of dev branch
- Add OAuth plugin architecture documentation
(OAUTH_PLUGIN_ARCHITECTURE.md)
- Add v0.2 plugin-first migration guide (0.2-plugin-first.md)
- Add v0.2.0 release status documentation (release-v0.2-status.md)
- Remove dummy plugin dependency from pyproject.toml
- Update dependency versions: duckdb, pyyaml, ruff, uvicorn, starlette,
rpds-py, and others
CopilotAI review requested due to automatic review settings October 5, 2025 23:07

CopilotAI 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.

Pull Request Overview

This PR removes large portions of legacy code and simplifies the architecture by consolidating authentication, OAuth, storage, and API functionality. The changes focus on unifying authentication patterns across providers and establishing a cleaner plugin-based architecture.

Key changes:

  • Consolidates OAuth authentication into a unified registry-based system with standardized providers
  • Simplifies storage layer by creating generic implementations that work across all credential types
  • Removes provider-specific implementations in favor of generic, reusable components
  • Streamlines API routes by removing complex proxy endpoints and metrics dashboards

Reviewed Changes

Copilot reviewed 79 out of 785 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
ccproxy/claude_sdk/options.pyCompletely removed - functionality moved to plugin system
ccproxy/auth/storage/generic.pyNew generic storage using Pydantic validation for all credential types
ccproxy/auth/oauth/registry.pyNew OAuth provider registry for dynamic provider management
ccproxy/auth/oauth/templates.pyCentralized HTML templates for OAuth responses
ccproxy/auth/oauth/flows.pyOAuth flow engines for CLI authentication
ccproxy/auth/managers/base.pyNew base token manager for all authentication providers
ccproxy/api/routes/plugins.pyNew plugin management API endpoints
Comments suppressed due to low confidence (1)

ccproxy/auth/oauth/base.py:1

  • The comment mentions non-standard implementations but the code doesn't actually include the state parameter in the base implementation. The comment should clarify that subclasses need to override this method to include state when needed, or provide a mechanism for including it conditionally.
"""Base OAuth client with common PKCE flow implementation."""

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

console.print("[dim]Scan QR code with mobile device[/dim]")
except ImportError:
# QR code library not available - graceful degradation
pass

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The ImportError exception is caught but provides no feedback to the user that QR code generation failed. Consider logging this condition or providing a fallback message to indicate QR codes are unavailable.

Suggested change
pass
logger.warning("qrcode_library_missing", url=url)
console.print("[yellow]QR code generation unavailable: missing 'qrcode' library.[/yellow]")

Copilot uses AI. Check for mistakes.
raise PortBindError(
f"Failed to start callback server on port {self.port}"
) from e
elif e.errno == 48: # Address already in use

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes e.errno exists for all OSError instances, but SystemExit doesn't have an errno attribute. This will cause an AttributeError when a SystemExit is converted to PortBindError and then the elif condition is checked.

Suggested change
elife.errno==48: # Address already in use
elifisinstance(e, OSError) andgetattr(e, "errno", None)==48: # Address already in use

Copilot uses AI. Check for mistakes.
Comment on lines +256 to +258
expires_at = expires_at.replace(tzinfo=UTC)

delta = expires_at - datetime.now(UTC)

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The method modifies the expires_at datetime object in place when adding timezone info. Consider creating a new datetime object instead of modifying the original to avoid side effects: expires_at = expires_at.replace(tzinfo=UTC)

Suggested change
expires_at=expires_at.replace(tzinfo=UTC)
delta=expires_at-datetime.now(UTC)
expires_at_utc=expires_at.replace(tzinfo=UTC)
else:
expires_at_utc=expires_at
delta=expires_at_utc-datetime.now(UTC)

Copilot uses AI. Check for mistakes.
@CaddyGlowCaddyGlow changed the title test: add slow stress suites for hooks, tasks, and analyticsfeat: complete AnyIO migrationOct 5, 2025
@CaddyGlow
CaddyGlow changed the base branch from main to dev/v0.2October 6, 2025 04:09
@CaddyGlow
CaddyGlow requested a review from CopilotOctober 6, 2025 04:14
Repository owner deleted a comment from CopilotAIOct 6, 2025

CopilotAI 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.

Pull Request Overview

Copilot reviewed 81 out of 83 changed files in this pull request and generated 6 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +136 to +144
def run(self, awaitable: Awaitable[_T]) -> _T:
"""Run an awaitable to completion using the active runtime."""
if not asyncio.iscoroutine(awaitable):
raise TypeError("runtime.run() expects a coroutine object")

async def _runner() -> _T:
return cast(_T, await awaitable)

return anyio.run(_runner)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The type check on line 138 is too restrictive. It only accepts coroutines but the function signature accepts any Awaitable[_T], which includes futures and other awaitable objects. This will cause runtime errors when passing valid awaitables that aren't coroutines.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
monkeypatch.setattr(
"ccproxy.services.mocking.mock_handler.runtime_sleep", fast_sleep
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The patching path has changed from asyncio.sleep to ccproxy.services.mocking.mock_handler.runtime_sleep. Ensure this matches the actual import path in the module being tested to avoid the patch not taking effect.

Copilot uses AI. Check for mistakes.
async def test_sse_confirmation_handler_stress(
monkeypatch: pytest.MonkeyPatch,
) -> None:
manager = AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The stress test creates an AsyncTaskManager with 512 max tasks but only processes 80 requests. Consider reducing max_tasks to a more appropriate value like 128 to avoid over-allocation in test environments.

Suggested change
manager=AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)
manager=AsyncTaskManager(max_tasks=128, shutdown_timeout=5.0)

Copilot uses AI. Check for mistakes.
Comment on lines +225 to 229
process = await runtime_create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
stdout=PIPE,
stderr=PIPE,
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The function uses runtime_create_subprocess_exec but still imports PIPE from the runtime module. Consider using a more descriptive constant or ensuring consistency in how subprocess constants are accessed.

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +58
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The code imports runtime_create_subprocess_exec but uses subprocess.PIPE instead of the runtime's PIPE constant. This creates an inconsistency and should use PIPE from the async_runtime module for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +140
if str(self.database_path) == ":memory:":
return self._store_request_sync(payload)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The special case for :memory: databases runs synchronously on the event loop. This could block the loop for I/O operations. Consider whether memory databases also benefit from thread execution or if this optimization is necessary.

Suggested change
ifstr(self.database_path) ==":memory:":
returnself._store_request_sync(payload)

Copilot uses AI. Check for mistakes.

@JiwaniZakirJiwaniZakir left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The fragmented import style in flows.py and base.py — one symbol per from ccproxy.core.async_runtime import (...) block — is unconventional and adds noise; these should be consolidated into a single import block per module following standard Python conventions.

More substantively, the __del__ cleanup in base.py still has the same fundamental problem the refactor was presumably trying to fix: runtime_create_task(self.http_client.aclose()) requires a running event loop, and suppress(RuntimeError) now silently swallows the failure rather than surfacing it. If __del__ fires during interpreter shutdown or after the loop has been closed, the http_client simply never gets closed. A more reliable pattern would be to expose an explicit async def close() or async def __aenter__/__aexit__ on this class and document that callers are responsible for cleanup, rather than relying on __del__ for async teardown at all.

The await hook_manager.shutdown() change in app.py is correct if shutdown is now a coroutine, but it would be worth confirming that the hook_manager implementation was updated consistently — a synchronous shutdown accidentally awaited would silently return the coroutine object uncalled in older Python versions, though 3.12+ raises a warning.

Sign up for freeto 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.

3 participants

@CaddyGlow@JiwaniZakir
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: complete AnyIO migration - #23

Open
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration
Open

feat: complete AnyIO migration#23
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration

Conversation

@CaddyGlow

@CaddyGlowCaddyGlow commented Oct 5, 2025

Copy link
Copy Markdown
Owner

Summary

  • migrate the runtime facade and task manager to AnyIO primitives, removing direct asyncio dependencies across core services
  • refactor hook execution, streaming responses, DuckDB access-log ingestion, and permissions flows to operate on the shared async runtime
  • tighten regression coverage with unit/integration stress suites that exercise high-concurrency hooks, task cancellation cycles, DuckDB ingest/pagination, streaming responses, and permissions SSE handling
  • update documentation and helper utilities so downstream services consistently consume the new runtime helpers

Testing

  • uv run ruff check
  • uv run mypy
  • uv run pytest
  • uv run pytest tests/api/middleware/test_streaming_stress.py -m slow
  • uv run pytest tests/plugins/permissions/integration/test_permissions_sse_stress.py -m slow
  • uv run pytest tests/plugins/analytics/integration/test_duckdb_access_log_stress.py tests/plugins/analytics/integration/test_analytics_pagination_stress.py -m slow
  • uv run pytest tests/unit/core/test_async_task_manager.py -m slow
  • uv run pytest tests/unit/core/test_background_hook_thread_manager.py -m slow

* Reduce log verbosity to ERROR level in test fixtures
* Upgrade claude_api and codex client fixtures to module scope
* Remove redundant auth_missing integration tests (covered by unit
tests)
* Mock asyncio.sleep in copilot OAuth polling tests
* Reduce timeout intervals in permission and scheduler tests
- Update README and installation guide to reference stable v0.2.0
release instead of dev branch
- Add OAuth plugin architecture documentation
(OAUTH_PLUGIN_ARCHITECTURE.md)
- Add v0.2 plugin-first migration guide (0.2-plugin-first.md)
- Add v0.2.0 release status documentation (release-v0.2-status.md)
- Remove dummy plugin dependency from pyproject.toml
- Update dependency versions: duckdb, pyyaml, ruff, uvicorn, starlette,
rpds-py, and others
CopilotAI review requested due to automatic review settings October 5, 2025 23:07

CopilotAI 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.

Pull Request Overview

This PR removes large portions of legacy code and simplifies the architecture by consolidating authentication, OAuth, storage, and API functionality. The changes focus on unifying authentication patterns across providers and establishing a cleaner plugin-based architecture.

Key changes:

  • Consolidates OAuth authentication into a unified registry-based system with standardized providers
  • Simplifies storage layer by creating generic implementations that work across all credential types
  • Removes provider-specific implementations in favor of generic, reusable components
  • Streamlines API routes by removing complex proxy endpoints and metrics dashboards

Reviewed Changes

Copilot reviewed 79 out of 785 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
ccproxy/claude_sdk/options.pyCompletely removed - functionality moved to plugin system
ccproxy/auth/storage/generic.pyNew generic storage using Pydantic validation for all credential types
ccproxy/auth/oauth/registry.pyNew OAuth provider registry for dynamic provider management
ccproxy/auth/oauth/templates.pyCentralized HTML templates for OAuth responses
ccproxy/auth/oauth/flows.pyOAuth flow engines for CLI authentication
ccproxy/auth/managers/base.pyNew base token manager for all authentication providers
ccproxy/api/routes/plugins.pyNew plugin management API endpoints
Comments suppressed due to low confidence (1)

ccproxy/auth/oauth/base.py:1

  • The comment mentions non-standard implementations but the code doesn't actually include the state parameter in the base implementation. The comment should clarify that subclasses need to override this method to include state when needed, or provide a mechanism for including it conditionally.
"""Base OAuth client with common PKCE flow implementation."""

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

console.print("[dim]Scan QR code with mobile device[/dim]")
except ImportError:
# QR code library not available - graceful degradation
pass

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The ImportError exception is caught but provides no feedback to the user that QR code generation failed. Consider logging this condition or providing a fallback message to indicate QR codes are unavailable.

Suggested change
pass
logger.warning("qrcode_library_missing", url=url)
console.print("[yellow]QR code generation unavailable: missing 'qrcode' library.[/yellow]")

Copilot uses AI. Check for mistakes.
raise PortBindError(
f"Failed to start callback server on port {self.port}"
) from e
elif e.errno == 48: # Address already in use

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes e.errno exists for all OSError instances, but SystemExit doesn't have an errno attribute. This will cause an AttributeError when a SystemExit is converted to PortBindError and then the elif condition is checked.

Suggested change
elife.errno==48: # Address already in use
elifisinstance(e, OSError) andgetattr(e, "errno", None)==48: # Address already in use

Copilot uses AI. Check for mistakes.
Comment on lines +256 to +258
expires_at = expires_at.replace(tzinfo=UTC)

delta = expires_at - datetime.now(UTC)

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The method modifies the expires_at datetime object in place when adding timezone info. Consider creating a new datetime object instead of modifying the original to avoid side effects: expires_at = expires_at.replace(tzinfo=UTC)

Suggested change
expires_at=expires_at.replace(tzinfo=UTC)
delta=expires_at-datetime.now(UTC)
expires_at_utc=expires_at.replace(tzinfo=UTC)
else:
expires_at_utc=expires_at
delta=expires_at_utc-datetime.now(UTC)

Copilot uses AI. Check for mistakes.
@CaddyGlowCaddyGlow changed the title test: add slow stress suites for hooks, tasks, and analyticsfeat: complete AnyIO migrationOct 5, 2025
@CaddyGlow
CaddyGlow changed the base branch from main to dev/v0.2October 6, 2025 04:09
@CaddyGlow
CaddyGlow requested a review from CopilotOctober 6, 2025 04:14
Repository owner deleted a comment from CopilotAIOct 6, 2025

CopilotAI 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.

Pull Request Overview

Copilot reviewed 81 out of 83 changed files in this pull request and generated 6 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +136 to +144
def run(self, awaitable: Awaitable[_T]) -> _T:
"""Run an awaitable to completion using the active runtime."""
if not asyncio.iscoroutine(awaitable):
raise TypeError("runtime.run() expects a coroutine object")

async def _runner() -> _T:
return cast(_T, await awaitable)

return anyio.run(_runner)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The type check on line 138 is too restrictive. It only accepts coroutines but the function signature accepts any Awaitable[_T], which includes futures and other awaitable objects. This will cause runtime errors when passing valid awaitables that aren't coroutines.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
monkeypatch.setattr(
"ccproxy.services.mocking.mock_handler.runtime_sleep", fast_sleep
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The patching path has changed from asyncio.sleep to ccproxy.services.mocking.mock_handler.runtime_sleep. Ensure this matches the actual import path in the module being tested to avoid the patch not taking effect.

Copilot uses AI. Check for mistakes.
async def test_sse_confirmation_handler_stress(
monkeypatch: pytest.MonkeyPatch,
) -> None:
manager = AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The stress test creates an AsyncTaskManager with 512 max tasks but only processes 80 requests. Consider reducing max_tasks to a more appropriate value like 128 to avoid over-allocation in test environments.

Suggested change
manager=AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)
manager=AsyncTaskManager(max_tasks=128, shutdown_timeout=5.0)

Copilot uses AI. Check for mistakes.
Comment on lines +225 to 229
process = await runtime_create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
stdout=PIPE,
stderr=PIPE,
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The function uses runtime_create_subprocess_exec but still imports PIPE from the runtime module. Consider using a more descriptive constant or ensuring consistency in how subprocess constants are accessed.

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +58
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The code imports runtime_create_subprocess_exec but uses subprocess.PIPE instead of the runtime's PIPE constant. This creates an inconsistency and should use PIPE from the async_runtime module for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +140
if str(self.database_path) == ":memory:":
return self._store_request_sync(payload)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The special case for :memory: databases runs synchronously on the event loop. This could block the loop for I/O operations. Consider whether memory databases also benefit from thread execution or if this optimization is necessary.

Suggested change
ifstr(self.database_path) ==":memory:":
returnself._store_request_sync(payload)

Copilot uses AI. Check for mistakes.

@JiwaniZakirJiwaniZakir left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The fragmented import style in flows.py and base.py — one symbol per from ccproxy.core.async_runtime import (...) block — is unconventional and adds noise; these should be consolidated into a single import block per module following standard Python conventions.

More substantively, the __del__ cleanup in base.py still has the same fundamental problem the refactor was presumably trying to fix: runtime_create_task(self.http_client.aclose()) requires a running event loop, and suppress(RuntimeError) now silently swallows the failure rather than surfacing it. If __del__ fires during interpreter shutdown or after the loop has been closed, the http_client simply never gets closed. A more reliable pattern would be to expose an explicit async def close() or async def __aenter__/__aexit__ on this class and document that callers are responsible for cleanup, rather than relying on __del__ for async teardown at all.

The await hook_manager.shutdown() change in app.py is correct if shutdown is now a coroutine, but it would be worth confirming that the hook_manager implementation was updated consistently — a synchronous shutdown accidentally awaited would silently return the coroutine object uncalled in older Python versions, though 3.12+ raises a warning.

Sign up for freeto 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.

3 participants

@CaddyGlow@JiwaniZakir
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: complete AnyIO migration - #23

Open
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration
Open

feat: complete AnyIO migration#23
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration

Conversation

@CaddyGlow

@CaddyGlowCaddyGlow commented Oct 5, 2025

Copy link
Copy Markdown
Owner

Summary

  • migrate the runtime facade and task manager to AnyIO primitives, removing direct asyncio dependencies across core services
  • refactor hook execution, streaming responses, DuckDB access-log ingestion, and permissions flows to operate on the shared async runtime
  • tighten regression coverage with unit/integration stress suites that exercise high-concurrency hooks, task cancellation cycles, DuckDB ingest/pagination, streaming responses, and permissions SSE handling
  • update documentation and helper utilities so downstream services consistently consume the new runtime helpers

Testing

  • uv run ruff check
  • uv run mypy
  • uv run pytest
  • uv run pytest tests/api/middleware/test_streaming_stress.py -m slow
  • uv run pytest tests/plugins/permissions/integration/test_permissions_sse_stress.py -m slow
  • uv run pytest tests/plugins/analytics/integration/test_duckdb_access_log_stress.py tests/plugins/analytics/integration/test_analytics_pagination_stress.py -m slow
  • uv run pytest tests/unit/core/test_async_task_manager.py -m slow
  • uv run pytest tests/unit/core/test_background_hook_thread_manager.py -m slow

* Reduce log verbosity to ERROR level in test fixtures
* Upgrade claude_api and codex client fixtures to module scope
* Remove redundant auth_missing integration tests (covered by unit
tests)
* Mock asyncio.sleep in copilot OAuth polling tests
* Reduce timeout intervals in permission and scheduler tests
- Update README and installation guide to reference stable v0.2.0
release instead of dev branch
- Add OAuth plugin architecture documentation
(OAUTH_PLUGIN_ARCHITECTURE.md)
- Add v0.2 plugin-first migration guide (0.2-plugin-first.md)
- Add v0.2.0 release status documentation (release-v0.2-status.md)
- Remove dummy plugin dependency from pyproject.toml
- Update dependency versions: duckdb, pyyaml, ruff, uvicorn, starlette,
rpds-py, and others
CopilotAI review requested due to automatic review settings October 5, 2025 23:07

CopilotAI 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.

Pull Request Overview

This PR removes large portions of legacy code and simplifies the architecture by consolidating authentication, OAuth, storage, and API functionality. The changes focus on unifying authentication patterns across providers and establishing a cleaner plugin-based architecture.

Key changes:

  • Consolidates OAuth authentication into a unified registry-based system with standardized providers
  • Simplifies storage layer by creating generic implementations that work across all credential types
  • Removes provider-specific implementations in favor of generic, reusable components
  • Streamlines API routes by removing complex proxy endpoints and metrics dashboards

Reviewed Changes

Copilot reviewed 79 out of 785 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
ccproxy/claude_sdk/options.pyCompletely removed - functionality moved to plugin system
ccproxy/auth/storage/generic.pyNew generic storage using Pydantic validation for all credential types
ccproxy/auth/oauth/registry.pyNew OAuth provider registry for dynamic provider management
ccproxy/auth/oauth/templates.pyCentralized HTML templates for OAuth responses
ccproxy/auth/oauth/flows.pyOAuth flow engines for CLI authentication
ccproxy/auth/managers/base.pyNew base token manager for all authentication providers
ccproxy/api/routes/plugins.pyNew plugin management API endpoints
Comments suppressed due to low confidence (1)

ccproxy/auth/oauth/base.py:1

  • The comment mentions non-standard implementations but the code doesn't actually include the state parameter in the base implementation. The comment should clarify that subclasses need to override this method to include state when needed, or provide a mechanism for including it conditionally.
"""Base OAuth client with common PKCE flow implementation."""

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

console.print("[dim]Scan QR code with mobile device[/dim]")
except ImportError:
# QR code library not available - graceful degradation
pass

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The ImportError exception is caught but provides no feedback to the user that QR code generation failed. Consider logging this condition or providing a fallback message to indicate QR codes are unavailable.

Suggested change
pass
logger.warning("qrcode_library_missing", url=url)
console.print("[yellow]QR code generation unavailable: missing 'qrcode' library.[/yellow]")

Copilot uses AI. Check for mistakes.
raise PortBindError(
f"Failed to start callback server on port {self.port}"
) from e
elif e.errno == 48: # Address already in use

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes e.errno exists for all OSError instances, but SystemExit doesn't have an errno attribute. This will cause an AttributeError when a SystemExit is converted to PortBindError and then the elif condition is checked.

Suggested change
elife.errno==48: # Address already in use
elifisinstance(e, OSError) andgetattr(e, "errno", None)==48: # Address already in use

Copilot uses AI. Check for mistakes.
Comment on lines +256 to +258
expires_at = expires_at.replace(tzinfo=UTC)

delta = expires_at - datetime.now(UTC)

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The method modifies the expires_at datetime object in place when adding timezone info. Consider creating a new datetime object instead of modifying the original to avoid side effects: expires_at = expires_at.replace(tzinfo=UTC)

Suggested change
expires_at=expires_at.replace(tzinfo=UTC)
delta=expires_at-datetime.now(UTC)
expires_at_utc=expires_at.replace(tzinfo=UTC)
else:
expires_at_utc=expires_at
delta=expires_at_utc-datetime.now(UTC)

Copilot uses AI. Check for mistakes.
@CaddyGlowCaddyGlow changed the title test: add slow stress suites for hooks, tasks, and analyticsfeat: complete AnyIO migrationOct 5, 2025
@CaddyGlow
CaddyGlow changed the base branch from main to dev/v0.2October 6, 2025 04:09
@CaddyGlow
CaddyGlow requested a review from CopilotOctober 6, 2025 04:14
Repository owner deleted a comment from CopilotAIOct 6, 2025

CopilotAI 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.

Pull Request Overview

Copilot reviewed 81 out of 83 changed files in this pull request and generated 6 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +136 to +144
def run(self, awaitable: Awaitable[_T]) -> _T:
"""Run an awaitable to completion using the active runtime."""
if not asyncio.iscoroutine(awaitable):
raise TypeError("runtime.run() expects a coroutine object")

async def _runner() -> _T:
return cast(_T, await awaitable)

return anyio.run(_runner)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The type check on line 138 is too restrictive. It only accepts coroutines but the function signature accepts any Awaitable[_T], which includes futures and other awaitable objects. This will cause runtime errors when passing valid awaitables that aren't coroutines.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
monkeypatch.setattr(
"ccproxy.services.mocking.mock_handler.runtime_sleep", fast_sleep
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The patching path has changed from asyncio.sleep to ccproxy.services.mocking.mock_handler.runtime_sleep. Ensure this matches the actual import path in the module being tested to avoid the patch not taking effect.

Copilot uses AI. Check for mistakes.
async def test_sse_confirmation_handler_stress(
monkeypatch: pytest.MonkeyPatch,
) -> None:
manager = AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The stress test creates an AsyncTaskManager with 512 max tasks but only processes 80 requests. Consider reducing max_tasks to a more appropriate value like 128 to avoid over-allocation in test environments.

Suggested change
manager=AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)
manager=AsyncTaskManager(max_tasks=128, shutdown_timeout=5.0)

Copilot uses AI. Check for mistakes.
Comment on lines +225 to 229
process = await runtime_create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
stdout=PIPE,
stderr=PIPE,
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The function uses runtime_create_subprocess_exec but still imports PIPE from the runtime module. Consider using a more descriptive constant or ensuring consistency in how subprocess constants are accessed.

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +58
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The code imports runtime_create_subprocess_exec but uses subprocess.PIPE instead of the runtime's PIPE constant. This creates an inconsistency and should use PIPE from the async_runtime module for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +140
if str(self.database_path) == ":memory:":
return self._store_request_sync(payload)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The special case for :memory: databases runs synchronously on the event loop. This could block the loop for I/O operations. Consider whether memory databases also benefit from thread execution or if this optimization is necessary.

Suggested change
ifstr(self.database_path) ==":memory:":
returnself._store_request_sync(payload)

Copilot uses AI. Check for mistakes.

@JiwaniZakirJiwaniZakir left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The fragmented import style in flows.py and base.py — one symbol per from ccproxy.core.async_runtime import (...) block — is unconventional and adds noise; these should be consolidated into a single import block per module following standard Python conventions.

More substantively, the __del__ cleanup in base.py still has the same fundamental problem the refactor was presumably trying to fix: runtime_create_task(self.http_client.aclose()) requires a running event loop, and suppress(RuntimeError) now silently swallows the failure rather than surfacing it. If __del__ fires during interpreter shutdown or after the loop has been closed, the http_client simply never gets closed. A more reliable pattern would be to expose an explicit async def close() or async def __aenter__/__aexit__ on this class and document that callers are responsible for cleanup, rather than relying on __del__ for async teardown at all.

The await hook_manager.shutdown() change in app.py is correct if shutdown is now a coroutine, but it would be worth confirming that the hook_manager implementation was updated consistently — a synchronous shutdown accidentally awaited would silently return the coroutine object uncalled in older Python versions, though 3.12+ raises a warning.

Sign up for freeto 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.

3 participants

@CaddyGlow@JiwaniZakir
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: complete AnyIO migration - #23

Open
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration
Open

feat: complete AnyIO migration#23
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration

Conversation

@CaddyGlow

@CaddyGlowCaddyGlow commented Oct 5, 2025

Copy link
Copy Markdown
Owner

Summary

  • migrate the runtime facade and task manager to AnyIO primitives, removing direct asyncio dependencies across core services
  • refactor hook execution, streaming responses, DuckDB access-log ingestion, and permissions flows to operate on the shared async runtime
  • tighten regression coverage with unit/integration stress suites that exercise high-concurrency hooks, task cancellation cycles, DuckDB ingest/pagination, streaming responses, and permissions SSE handling
  • update documentation and helper utilities so downstream services consistently consume the new runtime helpers

Testing

  • uv run ruff check
  • uv run mypy
  • uv run pytest
  • uv run pytest tests/api/middleware/test_streaming_stress.py -m slow
  • uv run pytest tests/plugins/permissions/integration/test_permissions_sse_stress.py -m slow
  • uv run pytest tests/plugins/analytics/integration/test_duckdb_access_log_stress.py tests/plugins/analytics/integration/test_analytics_pagination_stress.py -m slow
  • uv run pytest tests/unit/core/test_async_task_manager.py -m slow
  • uv run pytest tests/unit/core/test_background_hook_thread_manager.py -m slow

* Reduce log verbosity to ERROR level in test fixtures
* Upgrade claude_api and codex client fixtures to module scope
* Remove redundant auth_missing integration tests (covered by unit
tests)
* Mock asyncio.sleep in copilot OAuth polling tests
* Reduce timeout intervals in permission and scheduler tests
- Update README and installation guide to reference stable v0.2.0
release instead of dev branch
- Add OAuth plugin architecture documentation
(OAUTH_PLUGIN_ARCHITECTURE.md)
- Add v0.2 plugin-first migration guide (0.2-plugin-first.md)
- Add v0.2.0 release status documentation (release-v0.2-status.md)
- Remove dummy plugin dependency from pyproject.toml
- Update dependency versions: duckdb, pyyaml, ruff, uvicorn, starlette,
rpds-py, and others
CopilotAI review requested due to automatic review settings October 5, 2025 23:07

CopilotAI 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.

Pull Request Overview

This PR removes large portions of legacy code and simplifies the architecture by consolidating authentication, OAuth, storage, and API functionality. The changes focus on unifying authentication patterns across providers and establishing a cleaner plugin-based architecture.

Key changes:

  • Consolidates OAuth authentication into a unified registry-based system with standardized providers
  • Simplifies storage layer by creating generic implementations that work across all credential types
  • Removes provider-specific implementations in favor of generic, reusable components
  • Streamlines API routes by removing complex proxy endpoints and metrics dashboards

Reviewed Changes

Copilot reviewed 79 out of 785 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
ccproxy/claude_sdk/options.pyCompletely removed - functionality moved to plugin system
ccproxy/auth/storage/generic.pyNew generic storage using Pydantic validation for all credential types
ccproxy/auth/oauth/registry.pyNew OAuth provider registry for dynamic provider management
ccproxy/auth/oauth/templates.pyCentralized HTML templates for OAuth responses
ccproxy/auth/oauth/flows.pyOAuth flow engines for CLI authentication
ccproxy/auth/managers/base.pyNew base token manager for all authentication providers
ccproxy/api/routes/plugins.pyNew plugin management API endpoints
Comments suppressed due to low confidence (1)

ccproxy/auth/oauth/base.py:1

  • The comment mentions non-standard implementations but the code doesn't actually include the state parameter in the base implementation. The comment should clarify that subclasses need to override this method to include state when needed, or provide a mechanism for including it conditionally.
"""Base OAuth client with common PKCE flow implementation."""

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

console.print("[dim]Scan QR code with mobile device[/dim]")
except ImportError:
# QR code library not available - graceful degradation
pass

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The ImportError exception is caught but provides no feedback to the user that QR code generation failed. Consider logging this condition or providing a fallback message to indicate QR codes are unavailable.

Suggested change
pass
logger.warning("qrcode_library_missing", url=url)
console.print("[yellow]QR code generation unavailable: missing 'qrcode' library.[/yellow]")

Copilot uses AI. Check for mistakes.
raise PortBindError(
f"Failed to start callback server on port {self.port}"
) from e
elif e.errno == 48: # Address already in use

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes e.errno exists for all OSError instances, but SystemExit doesn't have an errno attribute. This will cause an AttributeError when a SystemExit is converted to PortBindError and then the elif condition is checked.

Suggested change
elife.errno==48: # Address already in use
elifisinstance(e, OSError) andgetattr(e, "errno", None)==48: # Address already in use

Copilot uses AI. Check for mistakes.
Comment on lines +256 to +258
expires_at = expires_at.replace(tzinfo=UTC)

delta = expires_at - datetime.now(UTC)

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The method modifies the expires_at datetime object in place when adding timezone info. Consider creating a new datetime object instead of modifying the original to avoid side effects: expires_at = expires_at.replace(tzinfo=UTC)

Suggested change
expires_at=expires_at.replace(tzinfo=UTC)
delta=expires_at-datetime.now(UTC)
expires_at_utc=expires_at.replace(tzinfo=UTC)
else:
expires_at_utc=expires_at
delta=expires_at_utc-datetime.now(UTC)

Copilot uses AI. Check for mistakes.
@CaddyGlowCaddyGlow changed the title test: add slow stress suites for hooks, tasks, and analyticsfeat: complete AnyIO migrationOct 5, 2025
@CaddyGlow
CaddyGlow changed the base branch from main to dev/v0.2October 6, 2025 04:09
@CaddyGlow
CaddyGlow requested a review from CopilotOctober 6, 2025 04:14
Repository owner deleted a comment from CopilotAIOct 6, 2025

CopilotAI 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.

Pull Request Overview

Copilot reviewed 81 out of 83 changed files in this pull request and generated 6 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +136 to +144
def run(self, awaitable: Awaitable[_T]) -> _T:
"""Run an awaitable to completion using the active runtime."""
if not asyncio.iscoroutine(awaitable):
raise TypeError("runtime.run() expects a coroutine object")

async def _runner() -> _T:
return cast(_T, await awaitable)

return anyio.run(_runner)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The type check on line 138 is too restrictive. It only accepts coroutines but the function signature accepts any Awaitable[_T], which includes futures and other awaitable objects. This will cause runtime errors when passing valid awaitables that aren't coroutines.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
monkeypatch.setattr(
"ccproxy.services.mocking.mock_handler.runtime_sleep", fast_sleep
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The patching path has changed from asyncio.sleep to ccproxy.services.mocking.mock_handler.runtime_sleep. Ensure this matches the actual import path in the module being tested to avoid the patch not taking effect.

Copilot uses AI. Check for mistakes.
async def test_sse_confirmation_handler_stress(
monkeypatch: pytest.MonkeyPatch,
) -> None:
manager = AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The stress test creates an AsyncTaskManager with 512 max tasks but only processes 80 requests. Consider reducing max_tasks to a more appropriate value like 128 to avoid over-allocation in test environments.

Suggested change
manager=AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)
manager=AsyncTaskManager(max_tasks=128, shutdown_timeout=5.0)

Copilot uses AI. Check for mistakes.
Comment on lines +225 to 229
process = await runtime_create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
stdout=PIPE,
stderr=PIPE,
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The function uses runtime_create_subprocess_exec but still imports PIPE from the runtime module. Consider using a more descriptive constant or ensuring consistency in how subprocess constants are accessed.

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +58
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The code imports runtime_create_subprocess_exec but uses subprocess.PIPE instead of the runtime's PIPE constant. This creates an inconsistency and should use PIPE from the async_runtime module for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +140
if str(self.database_path) == ":memory:":
return self._store_request_sync(payload)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The special case for :memory: databases runs synchronously on the event loop. This could block the loop for I/O operations. Consider whether memory databases also benefit from thread execution or if this optimization is necessary.

Suggested change
ifstr(self.database_path) ==":memory:":
returnself._store_request_sync(payload)

Copilot uses AI. Check for mistakes.

@JiwaniZakirJiwaniZakir left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The fragmented import style in flows.py and base.py — one symbol per from ccproxy.core.async_runtime import (...) block — is unconventional and adds noise; these should be consolidated into a single import block per module following standard Python conventions.

More substantively, the __del__ cleanup in base.py still has the same fundamental problem the refactor was presumably trying to fix: runtime_create_task(self.http_client.aclose()) requires a running event loop, and suppress(RuntimeError) now silently swallows the failure rather than surfacing it. If __del__ fires during interpreter shutdown or after the loop has been closed, the http_client simply never gets closed. A more reliable pattern would be to expose an explicit async def close() or async def __aenter__/__aexit__ on this class and document that callers are responsible for cleanup, rather than relying on __del__ for async teardown at all.

The await hook_manager.shutdown() change in app.py is correct if shutdown is now a coroutine, but it would be worth confirming that the hook_manager implementation was updated consistently — a synchronous shutdown accidentally awaited would silently return the coroutine object uncalled in older Python versions, though 3.12+ raises a warning.

Sign up for freeto 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.

3 participants

@CaddyGlow@JiwaniZakir
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: complete AnyIO migration - #23

Open
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration
Open

feat: complete AnyIO migration#23
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration

Conversation

@CaddyGlow

@CaddyGlowCaddyGlow commented Oct 5, 2025

Copy link
Copy Markdown
Owner

Summary

  • migrate the runtime facade and task manager to AnyIO primitives, removing direct asyncio dependencies across core services
  • refactor hook execution, streaming responses, DuckDB access-log ingestion, and permissions flows to operate on the shared async runtime
  • tighten regression coverage with unit/integration stress suites that exercise high-concurrency hooks, task cancellation cycles, DuckDB ingest/pagination, streaming responses, and permissions SSE handling
  • update documentation and helper utilities so downstream services consistently consume the new runtime helpers

Testing

  • uv run ruff check
  • uv run mypy
  • uv run pytest
  • uv run pytest tests/api/middleware/test_streaming_stress.py -m slow
  • uv run pytest tests/plugins/permissions/integration/test_permissions_sse_stress.py -m slow
  • uv run pytest tests/plugins/analytics/integration/test_duckdb_access_log_stress.py tests/plugins/analytics/integration/test_analytics_pagination_stress.py -m slow
  • uv run pytest tests/unit/core/test_async_task_manager.py -m slow
  • uv run pytest tests/unit/core/test_background_hook_thread_manager.py -m slow

* Reduce log verbosity to ERROR level in test fixtures
* Upgrade claude_api and codex client fixtures to module scope
* Remove redundant auth_missing integration tests (covered by unit
tests)
* Mock asyncio.sleep in copilot OAuth polling tests
* Reduce timeout intervals in permission and scheduler tests
- Update README and installation guide to reference stable v0.2.0
release instead of dev branch
- Add OAuth plugin architecture documentation
(OAUTH_PLUGIN_ARCHITECTURE.md)
- Add v0.2 plugin-first migration guide (0.2-plugin-first.md)
- Add v0.2.0 release status documentation (release-v0.2-status.md)
- Remove dummy plugin dependency from pyproject.toml
- Update dependency versions: duckdb, pyyaml, ruff, uvicorn, starlette,
rpds-py, and others
CopilotAI review requested due to automatic review settings October 5, 2025 23:07

CopilotAI 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.

Pull Request Overview

This PR removes large portions of legacy code and simplifies the architecture by consolidating authentication, OAuth, storage, and API functionality. The changes focus on unifying authentication patterns across providers and establishing a cleaner plugin-based architecture.

Key changes:

  • Consolidates OAuth authentication into a unified registry-based system with standardized providers
  • Simplifies storage layer by creating generic implementations that work across all credential types
  • Removes provider-specific implementations in favor of generic, reusable components
  • Streamlines API routes by removing complex proxy endpoints and metrics dashboards

Reviewed Changes

Copilot reviewed 79 out of 785 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
ccproxy/claude_sdk/options.pyCompletely removed - functionality moved to plugin system
ccproxy/auth/storage/generic.pyNew generic storage using Pydantic validation for all credential types
ccproxy/auth/oauth/registry.pyNew OAuth provider registry for dynamic provider management
ccproxy/auth/oauth/templates.pyCentralized HTML templates for OAuth responses
ccproxy/auth/oauth/flows.pyOAuth flow engines for CLI authentication
ccproxy/auth/managers/base.pyNew base token manager for all authentication providers
ccproxy/api/routes/plugins.pyNew plugin management API endpoints
Comments suppressed due to low confidence (1)

ccproxy/auth/oauth/base.py:1

  • The comment mentions non-standard implementations but the code doesn't actually include the state parameter in the base implementation. The comment should clarify that subclasses need to override this method to include state when needed, or provide a mechanism for including it conditionally.
"""Base OAuth client with common PKCE flow implementation."""

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

console.print("[dim]Scan QR code with mobile device[/dim]")
except ImportError:
# QR code library not available - graceful degradation
pass

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The ImportError exception is caught but provides no feedback to the user that QR code generation failed. Consider logging this condition or providing a fallback message to indicate QR codes are unavailable.

Suggested change
pass
logger.warning("qrcode_library_missing", url=url)
console.print("[yellow]QR code generation unavailable: missing 'qrcode' library.[/yellow]")

Copilot uses AI. Check for mistakes.
raise PortBindError(
f"Failed to start callback server on port {self.port}"
) from e
elif e.errno == 48: # Address already in use

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes e.errno exists for all OSError instances, but SystemExit doesn't have an errno attribute. This will cause an AttributeError when a SystemExit is converted to PortBindError and then the elif condition is checked.

Suggested change
elife.errno==48: # Address already in use
elifisinstance(e, OSError) andgetattr(e, "errno", None)==48: # Address already in use

Copilot uses AI. Check for mistakes.
Comment on lines +256 to +258
expires_at = expires_at.replace(tzinfo=UTC)

delta = expires_at - datetime.now(UTC)

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The method modifies the expires_at datetime object in place when adding timezone info. Consider creating a new datetime object instead of modifying the original to avoid side effects: expires_at = expires_at.replace(tzinfo=UTC)

Suggested change
expires_at=expires_at.replace(tzinfo=UTC)
delta=expires_at-datetime.now(UTC)
expires_at_utc=expires_at.replace(tzinfo=UTC)
else:
expires_at_utc=expires_at
delta=expires_at_utc-datetime.now(UTC)

Copilot uses AI. Check for mistakes.
@CaddyGlowCaddyGlow changed the title test: add slow stress suites for hooks, tasks, and analyticsfeat: complete AnyIO migrationOct 5, 2025
@CaddyGlow
CaddyGlow changed the base branch from main to dev/v0.2October 6, 2025 04:09
@CaddyGlow
CaddyGlow requested a review from CopilotOctober 6, 2025 04:14
Repository owner deleted a comment from CopilotAIOct 6, 2025

CopilotAI 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.

Pull Request Overview

Copilot reviewed 81 out of 83 changed files in this pull request and generated 6 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +136 to +144
def run(self, awaitable: Awaitable[_T]) -> _T:
"""Run an awaitable to completion using the active runtime."""
if not asyncio.iscoroutine(awaitable):
raise TypeError("runtime.run() expects a coroutine object")

async def _runner() -> _T:
return cast(_T, await awaitable)

return anyio.run(_runner)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The type check on line 138 is too restrictive. It only accepts coroutines but the function signature accepts any Awaitable[_T], which includes futures and other awaitable objects. This will cause runtime errors when passing valid awaitables that aren't coroutines.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
monkeypatch.setattr(
"ccproxy.services.mocking.mock_handler.runtime_sleep", fast_sleep
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The patching path has changed from asyncio.sleep to ccproxy.services.mocking.mock_handler.runtime_sleep. Ensure this matches the actual import path in the module being tested to avoid the patch not taking effect.

Copilot uses AI. Check for mistakes.
async def test_sse_confirmation_handler_stress(
monkeypatch: pytest.MonkeyPatch,
) -> None:
manager = AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The stress test creates an AsyncTaskManager with 512 max tasks but only processes 80 requests. Consider reducing max_tasks to a more appropriate value like 128 to avoid over-allocation in test environments.

Suggested change
manager=AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)
manager=AsyncTaskManager(max_tasks=128, shutdown_timeout=5.0)

Copilot uses AI. Check for mistakes.
Comment on lines +225 to 229
process = await runtime_create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
stdout=PIPE,
stderr=PIPE,
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The function uses runtime_create_subprocess_exec but still imports PIPE from the runtime module. Consider using a more descriptive constant or ensuring consistency in how subprocess constants are accessed.

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +58
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The code imports runtime_create_subprocess_exec but uses subprocess.PIPE instead of the runtime's PIPE constant. This creates an inconsistency and should use PIPE from the async_runtime module for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +140
if str(self.database_path) == ":memory:":
return self._store_request_sync(payload)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The special case for :memory: databases runs synchronously on the event loop. This could block the loop for I/O operations. Consider whether memory databases also benefit from thread execution or if this optimization is necessary.

Suggested change
ifstr(self.database_path) ==":memory:":
returnself._store_request_sync(payload)

Copilot uses AI. Check for mistakes.

@JiwaniZakirJiwaniZakir left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The fragmented import style in flows.py and base.py — one symbol per from ccproxy.core.async_runtime import (...) block — is unconventional and adds noise; these should be consolidated into a single import block per module following standard Python conventions.

More substantively, the __del__ cleanup in base.py still has the same fundamental problem the refactor was presumably trying to fix: runtime_create_task(self.http_client.aclose()) requires a running event loop, and suppress(RuntimeError) now silently swallows the failure rather than surfacing it. If __del__ fires during interpreter shutdown or after the loop has been closed, the http_client simply never gets closed. A more reliable pattern would be to expose an explicit async def close() or async def __aenter__/__aexit__ on this class and document that callers are responsible for cleanup, rather than relying on __del__ for async teardown at all.

The await hook_manager.shutdown() change in app.py is correct if shutdown is now a coroutine, but it would be worth confirming that the hook_manager implementation was updated consistently — a synchronous shutdown accidentally awaited would silently return the coroutine object uncalled in older Python versions, though 3.12+ raises a warning.

Sign up for freeto 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.

3 participants

@CaddyGlow@JiwaniZakir
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: complete AnyIO migration - #23

Open
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration
Open

feat: complete AnyIO migration#23
CaddyGlow wants to merge 23 commits into
dev/v0.2from
feat/anyio-migration

Conversation

@CaddyGlow

@CaddyGlowCaddyGlow commented Oct 5, 2025

Copy link
Copy Markdown
Owner

Summary

  • migrate the runtime facade and task manager to AnyIO primitives, removing direct asyncio dependencies across core services
  • refactor hook execution, streaming responses, DuckDB access-log ingestion, and permissions flows to operate on the shared async runtime
  • tighten regression coverage with unit/integration stress suites that exercise high-concurrency hooks, task cancellation cycles, DuckDB ingest/pagination, streaming responses, and permissions SSE handling
  • update documentation and helper utilities so downstream services consistently consume the new runtime helpers

Testing

  • uv run ruff check
  • uv run mypy
  • uv run pytest
  • uv run pytest tests/api/middleware/test_streaming_stress.py -m slow
  • uv run pytest tests/plugins/permissions/integration/test_permissions_sse_stress.py -m slow
  • uv run pytest tests/plugins/analytics/integration/test_duckdb_access_log_stress.py tests/plugins/analytics/integration/test_analytics_pagination_stress.py -m slow
  • uv run pytest tests/unit/core/test_async_task_manager.py -m slow
  • uv run pytest tests/unit/core/test_background_hook_thread_manager.py -m slow

* Reduce log verbosity to ERROR level in test fixtures
* Upgrade claude_api and codex client fixtures to module scope
* Remove redundant auth_missing integration tests (covered by unit
tests)
* Mock asyncio.sleep in copilot OAuth polling tests
* Reduce timeout intervals in permission and scheduler tests
- Update README and installation guide to reference stable v0.2.0
release instead of dev branch
- Add OAuth plugin architecture documentation
(OAUTH_PLUGIN_ARCHITECTURE.md)
- Add v0.2 plugin-first migration guide (0.2-plugin-first.md)
- Add v0.2.0 release status documentation (release-v0.2-status.md)
- Remove dummy plugin dependency from pyproject.toml
- Update dependency versions: duckdb, pyyaml, ruff, uvicorn, starlette,
rpds-py, and others
CopilotAI review requested due to automatic review settings October 5, 2025 23:07

CopilotAI 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.

Pull Request Overview

This PR removes large portions of legacy code and simplifies the architecture by consolidating authentication, OAuth, storage, and API functionality. The changes focus on unifying authentication patterns across providers and establishing a cleaner plugin-based architecture.

Key changes:

  • Consolidates OAuth authentication into a unified registry-based system with standardized providers
  • Simplifies storage layer by creating generic implementations that work across all credential types
  • Removes provider-specific implementations in favor of generic, reusable components
  • Streamlines API routes by removing complex proxy endpoints and metrics dashboards

Reviewed Changes

Copilot reviewed 79 out of 785 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
ccproxy/claude_sdk/options.pyCompletely removed - functionality moved to plugin system
ccproxy/auth/storage/generic.pyNew generic storage using Pydantic validation for all credential types
ccproxy/auth/oauth/registry.pyNew OAuth provider registry for dynamic provider management
ccproxy/auth/oauth/templates.pyCentralized HTML templates for OAuth responses
ccproxy/auth/oauth/flows.pyOAuth flow engines for CLI authentication
ccproxy/auth/managers/base.pyNew base token manager for all authentication providers
ccproxy/api/routes/plugins.pyNew plugin management API endpoints
Comments suppressed due to low confidence (1)

ccproxy/auth/oauth/base.py:1

  • The comment mentions non-standard implementations but the code doesn't actually include the state parameter in the base implementation. The comment should clarify that subclasses need to override this method to include state when needed, or provide a mechanism for including it conditionally.
"""Base OAuth client with common PKCE flow implementation."""

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

console.print("[dim]Scan QR code with mobile device[/dim]")
except ImportError:
# QR code library not available - graceful degradation
pass

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The ImportError exception is caught but provides no feedback to the user that QR code generation failed. Consider logging this condition or providing a fallback message to indicate QR codes are unavailable.

Suggested change
pass
logger.warning("qrcode_library_missing", url=url)
console.print("[yellow]QR code generation unavailable: missing 'qrcode' library.[/yellow]")

Copilot uses AI. Check for mistakes.
raise PortBindError(
f"Failed to start callback server on port {self.port}"
) from e
elif e.errno == 48: # Address already in use

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes e.errno exists for all OSError instances, but SystemExit doesn't have an errno attribute. This will cause an AttributeError when a SystemExit is converted to PortBindError and then the elif condition is checked.

Suggested change
elife.errno==48: # Address already in use
elifisinstance(e, OSError) andgetattr(e, "errno", None)==48: # Address already in use

Copilot uses AI. Check for mistakes.
Comment on lines +256 to +258
expires_at = expires_at.replace(tzinfo=UTC)

delta = expires_at - datetime.now(UTC)

CopilotAIOct 5, 2025

Copy link

Choose a reason for hiding this comment

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

The method modifies the expires_at datetime object in place when adding timezone info. Consider creating a new datetime object instead of modifying the original to avoid side effects: expires_at = expires_at.replace(tzinfo=UTC)

Suggested change
expires_at=expires_at.replace(tzinfo=UTC)
delta=expires_at-datetime.now(UTC)
expires_at_utc=expires_at.replace(tzinfo=UTC)
else:
expires_at_utc=expires_at
delta=expires_at_utc-datetime.now(UTC)

Copilot uses AI. Check for mistakes.
@CaddyGlowCaddyGlow changed the title test: add slow stress suites for hooks, tasks, and analyticsfeat: complete AnyIO migrationOct 5, 2025
@CaddyGlow
CaddyGlow changed the base branch from main to dev/v0.2October 6, 2025 04:09
@CaddyGlow
CaddyGlow requested a review from CopilotOctober 6, 2025 04:14
Repository owner deleted a comment from CopilotAIOct 6, 2025

CopilotAI 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.

Pull Request Overview

Copilot reviewed 81 out of 83 changed files in this pull request and generated 6 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +136 to +144
def run(self, awaitable: Awaitable[_T]) -> _T:
"""Run an awaitable to completion using the active runtime."""
if not asyncio.iscoroutine(awaitable):
raise TypeError("runtime.run() expects a coroutine object")

async def _runner() -> _T:
return cast(_T, await awaitable)

return anyio.run(_runner)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The type check on line 138 is too restrictive. It only accepts coroutines but the function signature accepts any Awaitable[_T], which includes futures and other awaitable objects. This will cause runtime errors when passing valid awaitables that aren't coroutines.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
monkeypatch.setattr(
"ccproxy.services.mocking.mock_handler.runtime_sleep", fast_sleep
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The patching path has changed from asyncio.sleep to ccproxy.services.mocking.mock_handler.runtime_sleep. Ensure this matches the actual import path in the module being tested to avoid the patch not taking effect.

Copilot uses AI. Check for mistakes.
async def test_sse_confirmation_handler_stress(
monkeypatch: pytest.MonkeyPatch,
) -> None:
manager = AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The stress test creates an AsyncTaskManager with 512 max tasks but only processes 80 requests. Consider reducing max_tasks to a more appropriate value like 128 to avoid over-allocation in test environments.

Suggested change
manager=AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0)
manager=AsyncTaskManager(max_tasks=128, shutdown_timeout=5.0)

Copilot uses AI. Check for mistakes.
Comment on lines +225 to 229
process = await runtime_create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
stdout=PIPE,
stderr=PIPE,
)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The function uses runtime_create_subprocess_exec but still imports PIPE from the runtime module. Consider using a more descriptive constant or ensuring consistency in how subprocess constants are accessed.

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +58
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The code imports runtime_create_subprocess_exec but uses subprocess.PIPE instead of the runtime's PIPE constant. This creates an inconsistency and should use PIPE from the async_runtime module for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +140
if str(self.database_path) == ":memory:":
return self._store_request_sync(payload)

CopilotAIOct 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The special case for :memory: databases runs synchronously on the event loop. This could block the loop for I/O operations. Consider whether memory databases also benefit from thread execution or if this optimization is necessary.

Suggested change
ifstr(self.database_path) ==":memory:":
returnself._store_request_sync(payload)

Copilot uses AI. Check for mistakes.

@JiwaniZakirJiwaniZakir left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The fragmented import style in flows.py and base.py — one symbol per from ccproxy.core.async_runtime import (...) block — is unconventional and adds noise; these should be consolidated into a single import block per module following standard Python conventions.

More substantively, the __del__ cleanup in base.py still has the same fundamental problem the refactor was presumably trying to fix: runtime_create_task(self.http_client.aclose()) requires a running event loop, and suppress(RuntimeError) now silently swallows the failure rather than surfacing it. If __del__ fires during interpreter shutdown or after the loop has been closed, the http_client simply never gets closed. A more reliable pattern would be to expose an explicit async def close() or async def __aenter__/__aexit__ on this class and document that callers are responsible for cleanup, rather than relying on __del__ for async teardown at all.

The await hook_manager.shutdown() change in app.py is correct if shutdown is now a coroutine, but it would be worth confirming that the hook_manager implementation was updated consistently — a synchronous shutdown accidentally awaited would silently return the coroutine object uncalled in older Python versions, though 3.12+ raises a warning.

Sign up for freeto 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.

3 participants

@CaddyGlow@JiwaniZakir