Skip to content

Commit 913d7aa

Browse files
committed
copilot: support Copilot authentication and service
Fixes-by: Rolf Fredheim <rolf@markolo.eu> (agent-assisted) Co-authored-by: GitHub Copilot (claude-opus-4.6) Fixes: #2
1 parent a8ad904 commit 913d7aa

11 files changed

Lines changed: 821 additions & 10 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Run the cloud setup script:
7575

7676
Or configure manually:
7777
- Press `,` in the app → configure API key
78-
- Supports OpenAI, Anthropic, OpenRouter
78+
- Supports OpenAI, Anthropic, OpenRouter, Copilot
7979
- Keys stored in `~/.config/meeting-notes/config.yaml`
8080

8181
**Option B: Local AI (Free, private, but slower)**
@@ -187,7 +187,7 @@ python run.py --dev
187187
### Settings
188188

189189
Press `,` to configure:
190-
- AI provider (OpenAI, Anthropic, OpenRouter, Ollama, none)
190+
- AI provider (OpenAI, Anthropic, OpenRouter, Copilot, Ollama, none)
191191
- API keys
192192
- Whisper model (tiny/base/small/medium/large)
193193
- Recording mode (mic/system/combined)

meeting_notes/ai_summarizer.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,3 +459,138 @@ def summarize(self, transcript: str, user_notes: str = "") -> MeetingSummary:
459459
logger.error(f"All {max_retries} attempts failed for OpenRouter API call")
460460
logger.error(error_msg, exc_info=True)
461461
raise
462+
463+
464+
class CopilotSummarizer(BaseSummarizer):
465+
"""Summarizer using GitHub Copilot API (included with Copilot subscription)."""
466+
467+
MODELS = {
468+
"mini": {
469+
"id": "gpt-4o-mini",
470+
"name": "GPT-4o Mini (Copilot)",
471+
},
472+
"standard": {
473+
"id": "gpt-4o",
474+
"name": "GPT-4o (Copilot)",
475+
},
476+
}
477+
478+
def __init__(self, api_key: Optional[str] = None, model: str = "mini"):
479+
"""
480+
Initialize Copilot summarizer.
481+
482+
Args:
483+
api_key: GitHub OAuth access token (from device flow authentication).
484+
model: Model tier - "mini" (gpt-4o-mini) or "standard" (gpt-4o).
485+
"""
486+
if not api_key:
487+
raise ValueError(
488+
"GitHub Copilot token required. "
489+
"Authenticate via Settings > GitHub Copilot > Authenticate with GitHub."
490+
)
491+
492+
if model not in self.MODELS:
493+
raise ValueError(
494+
f"Invalid model: {model}. Choose from: {list(self.MODELS.keys())}"
495+
)
496+
497+
self.model_config = self.MODELS[model]
498+
self.model = self.model_config["id"]
499+
500+
try:
501+
from .copilot_auth import COPILOT_API_BASE, CopilotTokenManager
502+
503+
self._token_manager = CopilotTokenManager(api_key)
504+
self.copilot_api_base = COPILOT_API_BASE
505+
except Exception as e:
506+
raise RuntimeError(f"Failed to initialize Copilot auth: {e}")
507+
508+
try:
509+
from openai import OpenAI
510+
511+
self._openai_class = OpenAI
512+
except ImportError:
513+
raise ImportError("openai package not installed. Run: pip install openai")
514+
515+
self._client = None
516+
self._current_token = None
517+
518+
def _get_client(self):
519+
"""Get an OpenAI client using a short-lived Copilot session token."""
520+
from meeting_notes import __version__
521+
522+
token = self._token_manager.get_token()
523+
524+
if token != self._current_token:
525+
self._client = self._openai_class(
526+
api_key=token,
527+
base_url=self.copilot_api_base,
528+
default_headers={
529+
"Openai-Intent": "conversation-edits",
530+
"User-Agent": f"meeting-notes/{__version__}",
531+
},
532+
)
533+
self._current_token = token
534+
535+
return self._client
536+
537+
def summarize(self, transcript: str, user_notes: str = "") -> MeetingSummary:
538+
"""Generate summary using GitHub Copilot with retry logic."""
539+
logger.info(f"Generating AI summary with {self.model_config['name']}...")
540+
logger.info(f"Transcript: {len(transcript.split())} words")
541+
print(f"Generating AI summary with {self.model_config['name']}...")
542+
print(f"Transcript: {len(transcript.split())} words")
543+
544+
max_retries = 2
545+
retry_delay = 2 # seconds
546+
547+
for attempt in range(max_retries):
548+
try:
549+
client = self._get_client()
550+
551+
response = client.chat.completions.create(
552+
model=self.model,
553+
messages=[
554+
{
555+
"role": "user",
556+
"content": self._build_prompt(
557+
transcript, user_notes=user_notes
558+
),
559+
}
560+
],
561+
temperature=0.3,
562+
)
563+
564+
# Log token usage if available (no cost tracking - included in subscription)
565+
if response.usage:
566+
total_tokens = (
567+
response.usage.prompt_tokens + response.usage.completion_tokens
568+
)
569+
logger.info(
570+
f"Summary generated ({total_tokens} tokens, included in Copilot subscription)"
571+
)
572+
print(
573+
f"Summary generated ({total_tokens} tokens, included in Copilot subscription)"
574+
)
575+
else:
576+
logger.info("Summary generated (Copilot)")
577+
print("Summary generated (Copilot)")
578+
579+
return self._parse_response(response.choices[0].message.content)
580+
581+
except Exception as e:
582+
error_msg = f"Attempt {attempt + 1}/{max_retries} failed: {type(e).__name__}: {e}"
583+
584+
if attempt < max_retries - 1:
585+
# If it's a token error, force a refresh before retrying
586+
self._current_token = None
587+
self._token_manager.invalidate()
588+
logger.warning(error_msg + f" - Retrying in {retry_delay}s...")
589+
print(f"Warning: {error_msg} - Retrying in {retry_delay}s...")
590+
time.sleep(retry_delay)
591+
else:
592+
logger.error(
593+
f"All {max_retries} attempts failed for Copilot API call"
594+
)
595+
logger.error(error_msg, exc_info=True)
596+
raise

meeting_notes/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,8 @@ def __init__(self, dev_mode: bool = False):
695695
api_key = self.config.anthropic_api_key or os.getenv("ANTHROPIC_API_KEY")
696696
elif self.config.ai_provider == "openrouter":
697697
api_key = self.config.openrouter_api_key or os.getenv("OPENROUTER_API_KEY")
698+
elif self.config.ai_provider == "copilot":
699+
api_key = self.config.github_copilot_token or os.getenv("GITHUB_COPILOT_TOKEN")
698700

699701
self.note_maker = NoteMaker(
700702
output_dir=self.config.notes_dir,
@@ -1601,6 +1603,8 @@ def handle_settings_closed(self, new_config: Optional[AppConfig]) -> None:
16011603
api_key = self.config.anthropic_api_key or os.getenv("ANTHROPIC_API_KEY")
16021604
elif self.config.ai_provider == "openrouter":
16031605
api_key = self.config.openrouter_api_key or os.getenv("OPENROUTER_API_KEY")
1606+
elif self.config.ai_provider == "copilot":
1607+
api_key = self.config.github_copilot_token or os.getenv("GITHUB_COPILOT_TOKEN")
16041608

16051609
self.note_maker = NoteMaker(
16061610
output_dir=self.config.notes_dir,

meeting_notes/config.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
class AppConfig:
1616
"""Application configuration."""
1717
# AI Summarization
18-
ai_provider: str = "anthropic" # "openai", "anthropic", "openrouter", "local", or "none"
18+
ai_provider: str = "anthropic" # "openai", "anthropic", "openrouter", "copilot", "local", or "none"
1919
ai_model: str = "haiku" # Model tier (varies by provider)
2020

2121
# API Keys (or set environment variables)
2222
openai_api_key: str = "" # OPENAI_API_KEY
2323
anthropic_api_key: str = "" # ANTHROPIC_API_KEY
2424
openrouter_api_key: str = "" # OPENROUTER_API_KEY
25+
github_copilot_token: str = "" # OAuth token from GitHub Device Flow
26+
github_copilot_client_id: str = "" # GITHUB_COPILOT_CLIENT_ID
2527

2628
# Legacy (kept for backwards compatibility)
2729
ollama_model: str = "llama3.2:3b"
@@ -56,6 +58,8 @@ def to_safe_dict(self) -> Dict[str, Any]:
5658
data['anthropic_api_key'] = self._redact_key(data['anthropic_api_key'])
5759
if data.get('openrouter_api_key'):
5860
data['openrouter_api_key'] = self._redact_key(data['openrouter_api_key'])
61+
if data.get('github_copilot_token'):
62+
data['github_copilot_token'] = self._redact_key(data['github_copilot_token'])
5963
return data
6064

6165
@classmethod
@@ -140,7 +144,7 @@ def validate_config(config: AppConfig) -> tuple[bool, Optional[str]]:
140144
(is_valid, error_message)
141145
"""
142146
# Validate AI provider
143-
valid_providers = ["openai", "anthropic", "openrouter", "local", "none"]
147+
valid_providers = ["openai", "anthropic", "openrouter", "copilot", "local", "none"]
144148
if config.ai_provider not in valid_providers:
145149
return False, f"Invalid ai_provider: {config.ai_provider}. Must be one of {valid_providers}"
146150

@@ -179,6 +183,19 @@ def validate_config(config: AppConfig) -> tuple[bool, Optional[str]]:
179183
valid_models = ["cheap", "balanced", "premium"]
180184
if config.ai_model not in valid_models:
181185
return False, f"Invalid ai_model for OpenRouter: {config.ai_model}. Must be one of {valid_models}"
186+
elif config.ai_provider == "copilot":
187+
token = config.github_copilot_token or os.getenv("GITHUB_COPILOT_TOKEN")
188+
if not token:
189+
return False, (
190+
"ai_provider is 'copilot' but not authenticated.\n"
191+
"Set GITHUB_COPILOT_TOKEN environment variable or authenticate in Settings"
192+
)
193+
valid_models = ["mini", "standard"]
194+
if config.ai_model not in valid_models:
195+
return (
196+
False,
197+
f"Invalid ai_model for Copilot: {config.ai_model}. Must be one of {valid_models}",
198+
)
182199

183200
# Validate whisper model
184201
valid_whisper = ["tiny", "base", "small", "medium", "large"]

0 commit comments

Comments
 (0)