Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
3 changes: 2 additions & 1 deletion apps/backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ def _get_llm_api_key_with_fallback() -> str:
"openrouter": "openrouter",
"deepseek": "deepseek",
"ollama": "ollama",
"github_copilot": "github_copilot",
}

config_provider = provider_map.get(provider, provider)
Expand All @@ -121,7 +122,7 @@ class Settings(BaseSettings):

# LLM Configuration
llm_provider: Literal[
"openai", "anthropic", "openrouter", "gemini", "deepseek", "ollama"
"openai", "anthropic", "openrouter", "gemini", "deepseek", "ollama", "github_copilot"
] = "openai"
llm_model: str = "gpt-5-nano-2025-08-07"
llm_api_key: str = ""
Expand Down
66 changes: 58 additions & 8 deletions apps/backend/app/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ class LLMConfig(BaseModel):
api_base: str | None = None


def _is_github_copilot_authenticated() -> bool:
"""Check if GitHub Copilot has a valid OAuth token on disk.

LiteLLM stores the token at ~/.config/litellm/github_copilot/access-token.
If the file is missing, any LLM call would trigger the device-code OAuth
flow (requiring manual browser intervention), so we fail fast instead.
"""
from pathlib import Path
token_file = Path.home() / ".config" / "litellm" / "github_copilot" / "access-token"
return token_file.exists()


def _normalize_api_base(provider: str, api_base: str | None) -> str | None:
"""Normalize api_base for LiteLLM provider-specific expectations.

Expand Down Expand Up @@ -257,6 +269,7 @@ def get_model_name(config: LLMConfig) -> str:
"gemini": "gemini/",
"deepseek": "deepseek/",
"ollama": "ollama/",
"github_copilot": "github_copilot/",
}

prefix = provider_prefixes.get(config.provider, "")
Expand Down Expand Up @@ -313,30 +326,44 @@ async def check_llm_health(
if config is None:
config = get_llm_config()

# Check if API key is configured (except for Ollama)
if config.provider != "ollama" and not config.api_key:
# Check if API key is configured (except for Ollama and GitHub Copilot which use OAuth)
if config.provider not in ("ollama", "github_copilot") and not config.api_key:
return {
"healthy": False,
"provider": config.provider,
"model": config.model,
"error_code": "api_key_missing",
}

# GitHub Copilot: fail fast if not authenticated to avoid triggering device flow
if config.provider == "github_copilot" and not _is_github_copilot_authenticated():
return {
"healthy": False,
"provider": config.provider,
"model": config.model,
"error_code": "github_copilot_not_authenticated",
"message": "GitHub Copilot is not authenticated. Please authenticate via Settings first.",
}

model_name = get_model_name(config)

prompt = test_prompt or "Hi"

try:
# Make a minimal test call with timeout
# Pass API key directly to avoid race conditions with global os.environ
# For GitHub Copilot, LiteLLM manages OAuth tokens internally, so don't pass api_key
kwargs: dict[str, Any] = {
"model": model_name,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 16,
"api_key": config.api_key,
"api_base": _normalize_api_base(config.provider, config.api_base),
"timeout": LLM_TIMEOUT_HEALTH_CHECK,
}

# Only pass api_key for providers that use it (not OAuth-based providers)
if config.provider != "github_copilot":
kwargs["api_key"] = config.api_key
kwargs["api_base"] = _normalize_api_base(config.provider, config.api_base)
Comment on lines +379 to +382
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

Same issue as in the complete() function - api_base should not be passed to LiteLLM for GitHub Copilot. The normalization call on line 366 should also be skipped for github_copilot, similar to how api_key is handled.

Copilot uses AI. Check for mistakes.
reasoning_effort = _get_reasoning_effort(config.provider, model_name)
if reasoning_effort:
kwargs["reasoning_effort"] = reasoning_effort
Expand Down Expand Up @@ -414,21 +441,32 @@ async def complete(

model_name = get_model_name(config)

# GitHub Copilot: fail fast if not authenticated to avoid triggering device flow
if config.provider == "github_copilot" and not _is_github_copilot_authenticated():
raise ValueError(
"GitHub Copilot is not authenticated. "
"Please authenticate via Settings before using AI features."
)

messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})

try:
# Pass API key directly to avoid race conditions with global os.environ
# For GitHub Copilot, LiteLLM manages OAuth tokens internally, so don't pass api_key
kwargs: dict[str, Any] = {
"model": model_name,
"messages": messages,
"max_tokens": max_tokens,
"api_key": config.api_key,
"api_base": _normalize_api_base(config.provider, config.api_base),
"timeout": LLM_TIMEOUT_COMPLETION,
}

# Only pass api_key for providers that use it (not OAuth-based providers)
if config.provider != "github_copilot":
kwargs["api_key"] = config.api_key
kwargs["api_base"] = _normalize_api_base(config.provider, config.api_base)
Comment on lines +481 to +485
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The api_base parameter should not be passed to LiteLLM for GitHub Copilot provider, as GitHub Copilot uses OAuth and has a fixed API endpoint managed by LiteLLM. Currently, this code only skips api_key for github_copilot, but api_base is still being set on line 436 when it's provided in the config. This could cause issues if a user sets a custom API base for GitHub Copilot. The condition on line 467 should also check if api_base should be passed, similar to how api_key is handled.

Suggested change
# Only pass api_key for providers that use it (not OAuth-based providers)
if config.provider != "github_copilot":
kwargs["api_key"] = config.api_key
kwargs["api_base"] = _normalize_api_base(config.provider, config.api_base)
# Only pass api_key/api_base for providers that use them (not OAuth-based providers)
if config.provider != "github_copilot":
kwargs["api_key"] = config.api_key
normalized_api_base = _normalize_api_base(config.provider, config.api_base)
if normalized_api_base is not None:
kwargs["api_base"] = normalized_api_base

Copilot uses AI. Check for mistakes.
if _supports_temperature(config.provider, model_name):
kwargs["temperature"] = temperature
reasoning_effort = _get_reasoning_effort(config.provider, model_name)
Expand All @@ -452,6 +490,7 @@ async def complete(
def _supports_json_mode(provider: str, model: str) -> bool:
"""Check if the model supports JSON mode."""
# Models that support response_format={"type": "json_object"}
# Note: github_copilot does NOT support response_format parameter
json_mode_providers = ["openai", "anthropic", "gemini", "deepseek"]
if provider in json_mode_providers:
return True
Expand Down Expand Up @@ -627,6 +666,13 @@ async def complete_json(

model_name = get_model_name(config)

# GitHub Copilot: fail fast if not authenticated to avoid triggering device flow
if config.provider == "github_copilot" and not _is_github_copilot_authenticated():
raise ValueError(
"GitHub Copilot is not authenticated. "
"Please authenticate via Settings before using AI features."
)

# Build messages
json_system = (
system_prompt or ""
Expand All @@ -644,14 +690,18 @@ async def complete_json(
try:
# Build request kwargs
# Pass API key directly to avoid race conditions with global os.environ
# For GitHub Copilot, LiteLLM manages OAuth tokens internally, so don't pass api_key
kwargs: dict[str, Any] = {
"model": model_name,
"messages": messages,
"max_tokens": max_tokens,
"api_key": config.api_key,
"api_base": _normalize_api_base(config.provider, config.api_base),
"timeout": _calculate_timeout("json", max_tokens, config.provider),
}

# Only pass api_key for providers that use it (not OAuth-based providers)
if config.provider != "github_copilot":
kwargs["api_key"] = config.api_key
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

Same issue as in other functions - api_base should not be passed to LiteLLM for GitHub Copilot. The code should skip setting api_base for github_copilot provider.

Suggested change
kwargs["api_key"] = config.api_key
kwargs["api_key"] = config.api_key
# Only pass api_base for providers that support it (not GitHub Copilot)
if config.provider != "github_copilot":

Copilot uses AI. Check for mistakes.
kwargs["api_base"] = _normalize_api_base(config.provider, config.api_base)
if _supports_temperature(config.provider, model_name):
# LLM-002: Increase temperature on retry for variation
kwargs["temperature"] = _get_retry_temperature(attempt)
Expand Down
Loading
Loading