Repository navigation
Initial c3-invoke project scaffold - #1
Conversation
Add initial project scaffold for c3-invoke. Introduces the core library (public API, types, output parsing, batch runner) and provider implementations for Gemini, Claude, and Codex (BaseProvider + provider modules). Adds an optional FastAPI HTTP server with routers and a console entrypoint, a pyproject.toml with packaging and extras, README, and comprehensive tests for providers, output parsing, and batching. Also include GitHub Actions workflows for CI and publishing to PyPI/TestPyPI.
There was a problem hiding this comment.
Pull request overview
Introduces the initial scaffold for c3-invoke, a Python package that provides a unified interface for invoking AI CLI tools (Gemini/Claude/Codex), plus an optional FastAPI HTTP server and CI/publish automation.
Changes:
- Added core library modules: provider abstractions, CLI execution, batch execution, and JSON output parsing.
- Added optional FastAPI server (app factory, routers, CLI entrypoint) exposing health/providers/prompt/batch endpoints.
- Added pytest test suite, packaging metadata (pyproject), README documentation, and GitHub Actions CI/publish workflows.
Reviewed changes
Copilot reviewed 24 out of 26 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_providers.py | Unit tests for provider metadata/command building and Gemini fallback/timeout paths. |
| tests/test_pool.py | Tests for parallel batch execution ordering and error handling. |
| tests/test_output.py | Tests for parsing JSON from plain text and fenced/embedded outputs. |
| tests/test_init.py | Tests for public API exports and provider registry behavior. |
| tests/init.py | Marks tests as a package (empty). |
| src/c3_invoke/types.py | Adds dataclass types for requests/responses/provider metadata and OutputFormat enum. |
| src/c3_invoke/output.py | Adds JSON parsing helper for CLI output. |
| src/c3_invoke/pool.py | Adds threadpool-based run_batch implementation. |
| src/c3_invoke/providers/base.py | Adds BaseProvider with binary resolution and subprocess execution/error handling. |
| src/c3_invoke/providers/gemini.py | Adds Gemini provider + fallback behavior. |
| src/c3_invoke/providers/claude.py | Adds Claude provider. |
| src/c3_invoke/providers/codex.py | Adds Codex provider. |
| src/c3_invoke/providers/init.py | Exposes provider classes via package exports. |
| src/c3_invoke/init.py | Defines public API surface and provider registry (get_provider, list_providers). |
| src/c3_invoke/server/app.py | FastAPI app factory wiring routers. |
| src/c3_invoke/server/main.py | CLI entrypoint to run uvicorn with the app factory. |
| src/c3_invoke/server/init.py | Declares product metadata for entry-point discovery. |
| src/c3_invoke/server/routers/health.py | Adds /health endpoint reporting available providers. |
| src/c3_invoke/server/routers/providers.py | Adds providers listing and per-provider test endpoint. |
| src/c3_invoke/server/routers/prompt.py | Adds /prompt endpoint to run a single prompt. |
| src/c3_invoke/server/routers/batch.py | Adds /batch endpoint for parallel prompt execution. |
| src/c3_invoke/server/routers/init.py | Router package marker (empty). |
| pyproject.toml | Packaging metadata, optional deps (server/dev), entry points, pytest config. |
| README.md | Usage/docs for library and HTTP server plus architecture overview. |
| .github/workflows/ci.yml | CI running tests + build verification on PRs/pushes. |
| .github/workflows/publish.yml | Release workflow to test/build and publish to TestPyPI then PyPI. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| def create_app() -> FastAPI: | ||
| app = FastAPI( | ||
| title="C3 Invoke", | ||
| description="Unified HTTP API for AI CLI providers", | ||
| version="0.1.0", | ||
| ) | ||
| app.include_router(health.router) | ||
| app.include_router(providers.router) | ||
| app.include_router(prompt.router) | ||
| app.include_router(batch.router) |
There was a problem hiding this comment.
The new FastAPI server surface (app factory + routers) is not covered by tests. Since the project already runs pytest in CI and includes httpx in dev deps, add at least a minimal test that boots create_app() and exercises one or two key endpoints (e.g., /health, /providers, and a /prompt error path) to prevent accidental breaking changes.
| from typing import Any | ||
|
|
||
|
|
||
| def parse_json_output(output: str) -> Any: |
There was a problem hiding this comment.
parse_json_output is annotated as taking output: str, but the function (and its tests) allow None and treat it as empty input. This will cause type-checking/IDE confusion. Update the signature to accept str | None (or Any) so the type contract matches runtime behavior.
| def parse_json_output(output: str) -> Any: | |
| def parse_json_output(output: str | None) -> Any: |
| ) | ||
| except subprocess.CalledProcessError as exc: | ||
| elapsed = time.monotonic() - start | ||
| fallback = self._handle_fallback(exc, request) |
There was a problem hiding this comment.
BaseProvider.run() calls _handle_fallback() inside the except CalledProcessError block without guarding against exceptions from the fallback itself. If a provider's fallback path raises (e.g., another CalledProcessError due to check=True), run() will raise instead of returning a PromptResponse error, breaking the method's contract. Wrap the _handle_fallback() call in its own try/except and treat fallback failures as None (or convert them into an error response).
| fallback = self._handle_fallback(exc, request) | |
| try: | |
| fallback = self._handle_fallback(exc, request) | |
| except Exception: | |
| # If fallback handling fails, treat it as no fallback and | |
| # continue with the original error. | |
| fallback = None |
| return subprocess.run( | ||
| fallback_cmd, | ||
| input=request.prompt, | ||
| capture_output=True, | ||
| text=True, | ||
| encoding="utf-8", | ||
| check=True, | ||
| cwd=request.cwd, | ||
| timeout=request.timeout, | ||
| ) |
There was a problem hiding this comment.
GeminiProvider._handle_fallback() runs a new subprocess with check=True. If that fallback attempt fails (non-zero exit) or times out, it will raise and escape BaseProvider.run()'s error handling, causing .run() to raise unexpectedly. Make the fallback path non-throwing (catch exceptions and return None, or reuse _execute with explicit error handling).
| return subprocess.run( | |
| fallback_cmd, | |
| input=request.prompt, | |
| capture_output=True, | |
| text=True, | |
| encoding="utf-8", | |
| check=True, | |
| cwd=request.cwd, | |
| timeout=request.timeout, | |
| ) | |
| try: | |
| return subprocess.run( | |
| fallback_cmd, | |
| input=request.prompt, | |
| capture_output=True, | |
| text=True, | |
| encoding="utf-8", | |
| check=True, | |
| cwd=request.cwd, | |
| timeout=request.timeout, | |
| ) | |
| except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError): | |
| # If the fallback command also fails or times out, give up on fallback | |
| # and let the caller handle the original exception. | |
| return None |
| def build_command(self, request: PromptRequest) -> list[str]: | ||
| binary = self.info().binary | ||
| if not binary: | ||
| binary = "gemini" | ||
| cmd = [binary, "-p", "", "-o", "text", "--allowed-mcp-server-names", ""] | ||
| cmd.extend(request.extra_flags) | ||
| return cmd |
There was a problem hiding this comment.
build_command() hardcodes the output format to text (and always includes -o text), ignoring request.output_format. Since PromptRequest/HTTP API expose output_format, callers can request JSON/markdown but the provider will still return text. Map request.output_format to the Gemini CLI flags (or reject non-text formats explicitly).
| def build_command(self, request: PromptRequest) -> list[str]: | ||
| binary = self.info().binary | ||
| if not binary: | ||
| binary = "claude" | ||
| cmd = [binary, "--print", "--output-format", "text"] | ||
| if request.model: | ||
| cmd.extend(["--model", request.model]) | ||
| cmd.extend(request.extra_flags) | ||
| return cmd |
There was a problem hiding this comment.
build_command() hardcodes --output-format text, ignoring request.output_format. Since OutputFormat is part of the public API, callers can set it but it has no effect. Use request.output_format.value (or raise/return an error for unsupported formats) so the option is either honored or clearly rejected.
No description provided.