Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .apm/skills/benchmark-qed-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ Ask in **a single `ask_user` form** (split into two if the workflow is autoq, si

| Field | Type | Options / examples | Notes |
|-------|------|--------------------|-------|
| `llm_provider` | enum | `openai.chat`, `azure.openai.chat`, `azure.inference.chat` | See provider table in [references/config-reference.md](references/config-reference.md). |
| `llm_provider` | enum | `openai.chat`, `azure.openai.chat`, `azure.inference.chat`, `copilot.chat` | See provider table in [references/config-reference.md](references/config-reference.md). |
| `model` | string | `gpt-4.1`, `gpt-4o`, `o3-mini`, an Azure deployment name | For Azure providers this is the **deployment name**, not the base model id. |
| `auth_type` | enum | `api_key` (default), `azure_managed_identity` | `azure_managed_identity` is only valid for `azure.*` providers. |
| `api_key_env_var` | string | `OPENAI_API_KEY` (default), `AZURE_OPENAI_API_KEY`, … | Only ask when `auth_type=api_key`. The skill writes `${VAR_NAME}` into YAML and adds the variable to `.env`. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ significance_correction: holm
| Azure OpenAI Embedding | `azure.openai.embedding` | Azure-hosted embeddings |
| Azure Inference Chat | `azure.inference.chat` | Azure AI Inference chat |
| Azure Inference Embedding | `azure.inference.embedding` | Azure AI Inference embeddings |
| GitHub Copilot CLI | `copilot.chat` | Chat models via the Copilot CLI (`copilot login`; no `api_key`/`auth_type`; no embeddings, so pair with another embedding provider; `temperature`/`seed` ignored). Optional `init_args`: `cli_path`, `github_token`, `timeout`, `reasoning_effort`, `validate_model`, `delete_sessions`. |

## Custom LLM Providers

Expand Down
4 changes: 4 additions & 0 deletions .semversioner/next-release/minor-20261023000000000000.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "minor",
"description": "Add copilot.chat LLM provider to use models through the GitHub Copilot CLI"
}
2 changes: 1 addition & 1 deletion benchmark_qed/cli/init_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class ConfigType(StrEnum):
model: gpt-4.1
auth_type: api_key # or azure_managed_identity
api_key: ${OPENAI_API_KEY} # remove this if using azure_managed_identity
llm_provider: openai.chat # or azure.openai.chat | azure.inference.chat
llm_provider: openai.chat # or azure.openai.chat | azure.inference.chat | copilot.chat
concurrent_requests: 4 # The number of concurrent requests to send to the model.
# init_args:
# Additional initialization arguments for the LLM can be added here.
Expand Down
20 changes: 20 additions & 0 deletions benchmark_qed/cli/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class ProviderResult:
("openai.chat", "OpenAI", "OpenAI API (default)"),
("azure.openai.chat", "Azure OpenAI", "Azure-hosted OpenAI models"),
("azure.inference.chat", "Azure Inference", "Azure AI Inference endpoint"),
("copilot.chat", "GitHub Copilot CLI", "Models available via GitHub Copilot"),
]

EMBEDDING_PROVIDERS: list[tuple[str, str, str]] = [
Expand All @@ -106,6 +107,7 @@ class ProviderResult:
]

DEFAULT_CHAT_MODEL = "gpt-4.1"
DEFAULT_COPILOT_CHAT_MODEL = "gpt-6-luna"
DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large"
DEFAULT_API_VERSION = "2024-12-01-preview"

Expand Down Expand Up @@ -255,6 +257,18 @@ def prompt_provider(
[(val, label) for val, label, _desc in providers],
)

if provider_value == "copilot.chat":
# The Copilot CLI handles authentication via the user's GitHub login.
model = typer.prompt(
"Model name",
default=default_model or DEFAULT_COPILOT_CHAT_MODEL,
)
return ProviderResult(
llm_provider=provider_value,
model=model,
auth_type="copilot_cli",
)

# Auth type
auth_type = select_option("Authentication type", AUTH_TYPES)

Expand All @@ -276,6 +290,12 @@ def prompt_embedding_provider(
chat_result: ProviderResult,
) -> ProviderResult:
"""Ask whether to reuse the chat provider for embeddings, or configure separately."""
if chat_result.llm_provider == "copilot.chat":
rich_print(
"[yellow]GitHub Copilot CLI does not provide embeddings; "
"select a separate embedding provider.[/yellow]"
)
return prompt_provider("embedding")
if typer.confirm("Use the same provider for embeddings?", default=True):
# Derive the embedding provider from the chat provider
mapping = {
Expand Down
15 changes: 10 additions & 5 deletions benchmark_qed/cli/yaml_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,17 @@ def _render_llm_section(provider_dict: dict[str, Any], indent: int = 2) -> str:
provider_dict = dataclasses.asdict(provider_dict)

pad = " " * indent
lines: list[str] = []
lines: list[str] = [f"{pad}model: {provider_dict['model']}"]

lines.extend([
f"{pad}model: {provider_dict['model']}",
f"{pad}auth_type: {provider_dict['auth_type']}",
])
if provider_dict["llm_provider"] == "copilot.chat":
# Copilot CLI authenticates via the user's GitHub login; no API key.
lines.extend([
f"{pad}llm_provider: {provider_dict['llm_provider']}",
f"{pad}concurrent_requests: 4",
])
return "\n".join(lines)

lines.append(f"{pad}auth_type: {provider_dict['auth_type']}")

if provider_dict["auth_type"] == "api_key":
lines.append(f"{pad}api_key: ${{OPENAI_API_KEY}}")
Expand Down
66 changes: 52 additions & 14 deletions benchmark_qed/config/llm_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ class LLMProvider(StrEnum):
AzureOpenAIEmbedding = "azure.openai.embedding"
AzureInferenceChat = "azure.inference.chat"
AzureInferenceEmbedding = "azure.inference.embedding"
CopilotChat = "copilot.chat"


# graphrag-llm completion strategy name used for the GitHub Copilot CLI provider.
COPILOT_COMPLETION_TYPE = "copilot_cli"


class ModelType(StrEnum):
Expand Down Expand Up @@ -121,9 +126,17 @@ class LLMConfig(BaseModel):
description="List of custom LLM providers to register.",
)

@property
def is_copilot(self) -> bool:
"""Whether this config targets the GitHub Copilot CLI provider."""
return str(self.llm_provider) == LLMProvider.CopilotChat

@model_validator(mode="after")
def check_api_key(self) -> Self:
"""Check if the API key is set."""
# The Copilot CLI authenticates with the user's GitHub login or token.
if self.is_copilot:
return self
if self.auth_type == "api_key" and (
self.api_key is None or self.api_key.get_secret_value().strip() == ""
):
Expand All @@ -138,8 +151,14 @@ def to_model_config(self) -> ModelConfig:
``azure``, ``azure_ai``). Custom providers are passed through with
their registered name as the ``ModelConfig.type`` so that
``register_completion`` / ``register_embedding`` callers can resolve
them.
them. The GitHub Copilot CLI provider is mapped to the
``copilot_cli`` completion type registered by
:class:`benchmark_qed.llm.factory.ModelFactory`; its ``init_args`` are
forwarded to :class:`benchmark_qed.llm.copilot_completion.CopilotCLICompletion`.
"""
if self.is_copilot:
return self._to_copilot_model_config()

provider_key = str(self.llm_provider)
is_builtin = provider_key in _PROVIDER_TO_MODEL_PROVIDER
model_provider = _PROVIDER_TO_MODEL_PROVIDER.get(provider_key, provider_key)
Expand All @@ -161,17 +180,6 @@ def to_model_config(self) -> ModelConfig:

azure_deployment_name = self.model if model_provider == "azure" else None

rate_limit: RateLimitConfig | None = None
if self.concurrent_requests and self.concurrent_requests > 0:
rate_limit = RateLimitConfig(
type=RateLimitType.SlidingWindow,
period_in_seconds=1,
requests_per_period=max(1, self.concurrent_requests),
)

# Track usage by enabling a metrics processor with no writer (silent).
metrics = MetricsConfig(writer=None)

config_kwargs: dict[str, Any] = {
"model_provider": model_provider,
"model": self.model,
Expand All @@ -182,8 +190,9 @@ def to_model_config(self) -> ModelConfig:
"azure_deployment_name": azure_deployment_name,
"call_args": dict(self.call_args),
"retry": self.retry,
"rate_limit": rate_limit,
"metrics": metrics,
"rate_limit": self._rate_limit_config(),
# Track usage by enabling a metrics processor with no writer (silent).
"metrics": MetricsConfig(writer=None),
**init_args,
}

Expand All @@ -194,3 +203,32 @@ def to_model_config(self) -> ModelConfig:
config_kwargs["type"] = provider_key

return ModelConfig(**config_kwargs)

def _rate_limit_config(self) -> RateLimitConfig | None:
if self.concurrent_requests and self.concurrent_requests > 0:
return RateLimitConfig(
type=RateLimitType.SlidingWindow,
period_in_seconds=1,
requests_per_period=max(1, self.concurrent_requests),
)
return None

def _to_copilot_model_config(self) -> ModelConfig:
"""Build a ``ModelConfig`` for the GitHub Copilot CLI provider.

Authentication is handled by the Copilot CLI (logged-in user or a
``github_token`` in ``init_args``), so ``api_key`` is not forwarded.
``init_args`` become extra ``ModelConfig`` fields, which graphrag-llm
passes through to the completion constructor.
"""
return ModelConfig(
type=COPILOT_COMPLETION_TYPE,
model_provider="copilot",
model=self.model,
auth_method=AuthMethod.ApiKey,
call_args=dict(self.call_args),
retry=self.retry,
rate_limit=self._rate_limit_config(),
metrics=MetricsConfig(writer=None),
**self.init_args,
)
Loading
Loading