Skip to content

Add GitHub Copilot SDK provider#945

Open
warnes wants to merge 6 commits intomozilla-ai:mainfrom
Warnes-Innovations:main
Open

Add GitHub Copilot SDK provider#945
warnes wants to merge 6 commits intomozilla-ai:mainfrom
Warnes-Innovations:main

Conversation

@warnes
Copy link
Copy Markdown

@warnes warnes commented Mar 13, 2026

Description

Adds copilot_sdk — a new provider that communicates with GitHub Copilot models via
the github-copilot-sdk Python package,
which bundles the Copilot CLI binary for your platform and speaks to it over JSON-RPC.

The primary motivation is reasoning support: the Copilot CLI session protocol exposes
reasoning_effort (low / medium / high / xhigh) and streams reasoning deltas as
a distinct event type, which is not available through the GitHub Models OpenAI-compatible
REST endpoint and therefore cannot be surfaced by the existing openai provider.

A secondary benefit is tokenless auth: if the user is already logged in via
gh auth login, no API key or environment variable is needed — the provider falls back to
the CLI's own credentials automatically.

What's included

Area Files changed
Provider implementation src/any_llm/providers/copilot_sdk/copilot_sdk.py (new, 463 lines)
Package init + factory alias src/any_llm/providers/copilot_sdk/__init__.py (new)
Provider enum registration src/any_llm/constants.py (+1 line)
Optional dependency group pyproject.toml ([copilot_sdk] extras)
Unit tests tests/unit/providers/test_copilot_sdk_provider.py (new, 714 lines)
Test fixtures tests/conftest.py, tests/unit/test_provider.py (+minimal hooks)
Provider docs docs/src/content/docs/providers.md (table row + Provider Notes section)

Supported features

Feature Supported
Completion (non-streaming)
Completion (streaming)
Reasoning (reasoning_effort)
Image attachments (data: URIs)
Model listing
PDF attachments
Embeddings
Responses API
Batch

Authentication

Two modes, checked in order:

  1. TokenCOPILOT_GITHUB_TOKEN, GITHUB_TOKEN, or GH_TOKEN (or api_key argument).
  2. Logged-in CLI session — if no token is found, the provider uses gh auth login credentials; no environment variable required.

Notable implementation details

  • Lazy client init with asyncio.Lock double-checked locking to prevent concurrent coroutines from each spawning a separate CLI process on first use.
  • SESSION_ERROR raises RuntimeError rather than silently completing the stream with no content.
  • mimetypes.init() at module load to ensure guess_extension() is thread-safe from the first invocation (Python's lazy MIME-type initialisation is not thread-safe).
  • Image attachments are decoded from data: URIs to temporary files on disk, passed to the CLI as FileAttachment objects, and cleaned up after each request. HTTP image URLs are not fetched (skipped with a comment).
  • Copilot_sdkProvider alias in __init__.py is required so AnyLLM._create_provider() can find the class — the factory capitalises the provider key (copilot_sdkCopilot_sdk).

Installation

pip install any-llm-sdk[copilot_sdk]

github-copilot-sdk ships separate wheels per OS/arch (e.g. macosx_arm64, linux_x86_64).
pip selects the correct wheel automatically on supported platforms.

Quick usage example

from any_llm import AnyLLM

llm = AnyLLM.create("copilot_sdk")  # uses gh CLI session or COPILOT_GITHUB_TOKEN

# Reasoning
response = llm.completion(
    model="claude-sonnet-4-5",
    messages=[{"role": "user", "content": "Explain async generators in Python."}],
    reasoning_effort="high",
)
print(response.choices[0].message.content)

# Streaming
for chunk in llm.completion(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Hello!"}],
    stream=True,
):
    print(chunk.choices[0].delta.content or "", end="", flush=True)

PR Type

  • 🆕 New Feature

Relevant issues

Checklist

  • I understand the code I am submitting.
  • I have added unit tests that prove my fix/feature works
  • I have run this code locally and verified it fixes the issue.
  • New and existing tests pass locally
  • Documentation was updated where necessary
  • I have read and followed the contribution guidelines
  • AI Usage:
    • No AI was used.
    • AI was used for drafting/refactoring.
    • This is fully AI-generated.

AI Usage Information

  • AI Model used: Claude Sonnet 4.6

  • AI Developer Tool used: GitHub Copilot (VS Code) and Claude Code (VS Code plugin)

  • Any other info: The provider connects to the GitHub Copilot CLI via JSON-RPC, so using Copilot to write a Copilot provider felt appropriate. All logic was reviewed and verified manually; tests were executed locally against a real Copilot CLI session.

  • I am an AI Agent filling out this form (check box if true)

warnes and others added 5 commits March 12, 2026 13:38
- Add CopilotSdkProvider supporting completion and model listing via
  the github-copilot-sdk package (JSON-RPC to bundled Copilot CLI)
- Support three auth modes: explicit token, env vars
  (COPILOT_GITHUB_TOKEN / GITHUB_TOKEN / GH_TOKEN), or gh CLI session
- Add asyncio.Lock double-checked locking to prevent concurrent CLI
  process spawning on first use
- Register copilot_sdk in LLMProvider enum, pyproject.toml optional
  dependencies, and provider docs table (alphabetical order)
- Add 20 unit tests covering auth resolution, client reuse, completion,
  model listing, and edge cases (empty messages, cli_url path, errors)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Raise RuntimeError when SESSION_ERROR fires during streaming instead
  of silently completing the stream with no error
- Call mimetypes.init() at module load to ensure guess_extension() is
  thread-safe from the first invocation
- Add tests for SESSION_ERROR raises, image attachment forwarding, and
  update provider capability docs (streaming/reasoning/image now ✅)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Copilot_sdkProvider alias to __init__.py so AnyLLM._create_provider()
  can find the class (factory uses provider_key.capitalize() → "Copilot_sdk")
- Add "Provider Notes" section to providers.md with installation guide,
  authentication modes, configuration env vars, and usage examples
- Regenerate provider table via generate_provider_table.py; copilot_sdk
  row now shows correct COPILOT_CLI_URL in the Base column

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@njbrake
Copy link
Copy Markdown
Contributor

njbrake commented Mar 17, 2026

Thank you for the contribution! Here is the review from my agent:

  Issues Found

  1. BLOCKING — Provider key uses underscore, breaking naming convention

  Every existing provider uses a single lowercase word with no underscores: azureopenai, vertexaianthropic, llamacpp, lmstudio, etc. The factory in _create_provider() does
  provider_key.capitalize() + "Provider", which turns copilot_sdk into Copilot_sdkProvider — an awkward name that requires a class alias in __init__.py.

  Fix: Rename to copilotsdk everywhere (enum value, directory name, class name CopilotsdkProvider, pyproject extras, docs). This eliminates the alias and follows the
  established pattern.

  2. BLOCKING — Decorative section-separator comments

  Both copilot_sdk.py and test_copilot_sdk_provider.py use # ----------- banner comments throughout. From CLAUDE.md/AGENTS.md:

  Do not add decorative section-separator comments (e.g., # ----------- banners). Well-named test functions and natural file ordering are sufficient.

  These should all be removed.

  3. BLOCKING — .github/copilot-instructions.md should be a separate PR

  This file is unrelated to the provider feature. It's a project-level config file that duplicates content from AGENTS.md. Should be split into its own PR.

  4. Medium — Tests will fail when github-copilot-sdk is not installed

  Several tests import from copilot.generated.session_events import SessionEventType inline. If the SDK isn't installed, these tests will fail with ImportError rather than
  being skipped. Tests that depend on the optional SDK should either:
  - Add a module-level skip: pytest.importorskip("copilot")
  - Or guard inline imports with try/except + pytest.skip

  5. Medium — PermissionHandler.approve_all security concern

  _build_session_cfg uses PermissionHandler.approve_all which silently grants any permissions the Copilot CLI session requests (e.g., tool calls). This should at minimum be
  documented as a security consideration, and ideally be configurable or at least logged.

  6. Medium — getattr chain for error extraction violates style guide

  In _stream_from_session (line ~309):
  getattr(getattr(event, "data", None), "message", "Copilot session error")

  CLAUDE.md says: "Prefer direct attribute access (obj.field) over getattr(obj, "field") when the field is typed." If the event types from the SDK have typed fields, use
  event.data.message directly. If they're truly dynamic, at least add a comment explaining why.

  7. Minor — Non-unique completion IDs

  _build_chat_completion uses f"copilot-sdk-{int(time.time())}" which is only second-granularity. Two concurrent non-streaming completions in the same second get the same
  ID. Use time.time_ns() (already used in _build_chunk) or uuid4().

  8. Minor — _make_provider test helper bypasses __init__

  Using object.__new__(CopilotSdkProvider) to construct the provider is fragile — it will silently break if the base class __init__ changes. Consider using patch on
  _init_client and _verify_and_set_api_key instead, which would exercise the real constructor.

  Suggestions (non-blocking)

  - The _messages_to_prompt function flattens multi-turn conversations into a text transcript, which loses the structured turn information that models expect. If the Copilot
   SDK supports structured messages, that would be preferred over prompt flattening.
  - _extract_attachments silently skips HTTP image URLs. Consider raising NotImplementedError or logging a warning so users know their images were dropped.
  - The docstrings are thorough but could be trimmed — the module-level class docstring on CopilotSdkProvider is quite long and repeats the README content.

  Verdict

  Request Changes

  The provider implementation itself is well-structured and thoughtful — the double-checked locking pattern, session lifecycle management, and reasoning support are all done
   well. However, there are three blocking issues:

  1. The copilot_sdk naming convention breaks the established pattern and requires an alias hack. Rename to copilotsdk.
  2. Remove all # ----------- decorative banner comments per project guidelines.
  3. Split .github/copilot-instructions.md into its own PR.

  After those fixes plus the test skip guard (issue #4), this should be ready for another look.

@njbrake njbrake self-requested a review March 17, 2026 15:27
Copy link
Copy Markdown
Contributor

@njbrake njbrake left a comment

Choose a reason for hiding this comment

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

Needs a few updates, see previous comments 🙏

@github-actions
Copy link
Copy Markdown
Contributor

This PR is stale because it has been open 7 days with no activity. Remove stale label or comment or this will be closed in 3 days.

@github-actions github-actions bot added the Stale label Mar 28, 2026
@warnes
Copy link
Copy Markdown
Author

warnes commented Mar 28, 2026

I will fix these issues.

- rename provider key and enum from copilot_sdk to copilotsdk across code, tests, docs, and extras

- remove alias/banners and keep implementation style aligned with project conventions

- add optional SDK test guard and retain defensive event-field handling comments

- drop unrelated .github/copilot-instructions.md from this PR
@warnes
Copy link
Copy Markdown
Author

warnes commented Mar 28, 2026

Addressed review feedback in commit c20339f.

Summary of changes:

  • Renamed provider naming to match convention: copilot_sdk -> copilotsdk
    • Updated provider key, enum value, module path, docs, tests, and optional-dependency extra.
    • Removed the class alias workaround by using CopilotsdkProvider directly.
  • Removed decorative section-separator banner comments from provider and provider test module.
  • Removed unrelated .github/copilot-instructions.md from this PR.
  • Added optional dependency test guard in provider tests via pytest.importorskip("copilot").
  • Kept defensive access for dynamic SDK event payloads and documented why.
  • Switched non-streaming completion IDs to nanosecond precision (time.time_ns()).

Validation run:

  • uv run pytest -q tests/unit/providers/test_copilotsdk_provider.py tests/unit/test_provider.py
  • Result: 122 passed, 10 skipped

If you want, I can also split out any follow-up hardening (e.g., permission policy configurability) into a separate PR.

@warnes warnes requested a review from njbrake March 28, 2026 03:11
@github-actions github-actions bot removed the Stale label Mar 29, 2026
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