Skip to content

Initial c3-invoke project scaffold - #1

Merged
lasalasa merged 2 commits into
mainfrom
feature/InitialSetup
Mar 10, 2026
Merged

lasalasa merged 2 commits into
mainfrom
feature/InitialSetup

Conversation

@lasalasa

Copy link
Copy Markdown
Member

No description provided.

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.

Copilot AI 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.

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.

Comment on lines +8 to +17
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)

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread src/c3_invoke/output.py
from typing import Any


def parse_json_output(output: str) -> Any:

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
def parse_json_output(output: str) -> Any:
def parse_json_output(output: str | None) -> Any:

Copilot uses AI. Check for mistakes.
)
except subprocess.CalledProcessError as exc:
elapsed = time.monotonic() - start
fallback = self._handle_fallback(exc, request)

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

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

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

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +52
return subprocess.run(
fallback_cmd,
input=request.prompt,
capture_output=True,
text=True,
encoding="utf-8",
check=True,
cwd=request.cwd,
timeout=request.timeout,
)

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

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

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

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +27
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

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +27
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

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@lasalasa
lasalasa merged commit 2456c38 into main Mar 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants