mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 21:27:41 +02:00
Subscription access (WIP): - 9Router integration: auto-detects if running, routes through user's subscriptions - Fallback routing: no API key → check 9Router → use subscription - Model ID mapping for 9Router (cc/ prefix for Claude Code models) - GitHub Copilot OAuth device flow (copilot_auth.py, providers/copilot.py) - Copilot auth endpoints: start-auth, poll-auth, models, disconnect Settings restructure: - 4 tabs: General, Models, Usage, Commands - Models tab: Subscriptions section (9Router + Copilot) + API Keys section - Per-provider "CONNECTED" badges - Subscription token fields removed (Anthropic banned OAuth in third-party apps) - 9Router auto-detection with "Check Connection" button Provider routing: - create_provider() checks subscription tokens → API keys → 9Router fallback - validate_credentials() allows through if 9Router is running - 9Router model ID mapping (sonnet → cc/claude-sonnet-4-6) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
122 lines
4.7 KiB
Python
122 lines
4.7 KiB
Python
"""GitHub Copilot provider — routes through Copilot's OpenAI-compatible API.
|
|
|
|
Uses the user's GitHub Copilot subscription to access Claude, GPT, and other models.
|
|
Extends OpenAICompatProvider since Copilot's API speaks the OpenAI format.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
from typing import Any, AsyncIterator
|
|
|
|
from openai import AsyncOpenAI
|
|
|
|
from backend.apps.agents.providers.base import (
|
|
BaseProvider, ProviderMessage, StreamEvent, ToolSchema, ModelResponse,
|
|
)
|
|
from backend.apps.agents.providers.openai_compat import OpenAICompatProvider
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
COPILOT_API_BASE = "https://api.githubcopilot.com"
|
|
|
|
|
|
class CopilotProvider(OpenAICompatProvider):
|
|
"""Provider that routes through GitHub Copilot's API."""
|
|
|
|
def __init__(self, copilot_token: str):
|
|
# Initialize OpenAI client pointing at Copilot's API
|
|
self.client = AsyncOpenAI(
|
|
api_key=copilot_token,
|
|
base_url=COPILOT_API_BASE,
|
|
)
|
|
# Store token for header injection
|
|
self._copilot_token = copilot_token
|
|
|
|
def get_model_id(self, short_name: str) -> str:
|
|
# Copilot uses same model IDs — pass through
|
|
return short_name
|
|
|
|
async def stream_message(
|
|
self,
|
|
model: str,
|
|
system: str | None,
|
|
messages: list[ProviderMessage],
|
|
tools: list[ToolSchema],
|
|
max_tokens: int = 8192,
|
|
) -> AsyncIterator[StreamEvent]:
|
|
"""Stream with Copilot-specific headers."""
|
|
kwargs: dict[str, Any] = {
|
|
"model": self.get_model_id(model),
|
|
"max_tokens": max_tokens,
|
|
"messages": self._build_messages(system, messages),
|
|
"stream": True,
|
|
"extra_headers": {
|
|
"copilot-integration-id": "vscode-chat",
|
|
},
|
|
}
|
|
if tools:
|
|
kwargs["tools"] = [self.clean_tool_schema(t) for t in tools]
|
|
|
|
stream = await self.client.chat.completions.create(**kwargs)
|
|
|
|
# Reuse parent's stream parsing logic
|
|
text_started = False
|
|
text_index = 0
|
|
tool_indices: dict[int, dict] = {}
|
|
next_block_index = 0
|
|
|
|
from uuid import uuid4
|
|
|
|
async for chunk in stream:
|
|
if not chunk.choices:
|
|
continue
|
|
|
|
delta = chunk.choices[0].delta
|
|
finish_reason = chunk.choices[0].finish_reason
|
|
|
|
if delta.content is not None:
|
|
if not text_started:
|
|
text_started = True
|
|
text_index = next_block_index
|
|
next_block_index += 1
|
|
yield StreamEvent(type="content_block_start", index=text_index, block_type="text")
|
|
yield StreamEvent(type="content_block_delta", index=text_index, delta_type="text_delta", text=delta.content)
|
|
|
|
if delta.tool_calls:
|
|
for tc_delta in delta.tool_calls:
|
|
tc_idx = tc_delta.index
|
|
if tc_idx not in tool_indices:
|
|
if text_started:
|
|
yield StreamEvent(type="content_block_stop", index=text_index)
|
|
text_started = False
|
|
block_idx = next_block_index
|
|
next_block_index += 1
|
|
tool_indices[tc_idx] = {
|
|
"block_index": block_idx,
|
|
"id": tc_delta.id or uuid4().hex,
|
|
"name": tc_delta.function.name if tc_delta.function else "",
|
|
"json_buf": "",
|
|
}
|
|
yield StreamEvent(
|
|
type="content_block_start", index=block_idx, block_type="tool_use",
|
|
tool_name=tool_indices[tc_idx]["name"], tool_id=tool_indices[tc_idx]["id"],
|
|
)
|
|
info = tool_indices[tc_idx]
|
|
if tc_delta.function and tc_delta.function.name:
|
|
info["name"] = tc_delta.function.name
|
|
if tc_delta.function and tc_delta.function.arguments:
|
|
info["json_buf"] += tc_delta.function.arguments
|
|
yield StreamEvent(
|
|
type="content_block_delta", index=info["block_index"],
|
|
delta_type="input_json_delta", text=tc_delta.function.arguments,
|
|
)
|
|
|
|
if finish_reason is not None:
|
|
if text_started:
|
|
yield StreamEvent(type="content_block_stop", index=text_index)
|
|
for info in tool_indices.values():
|
|
yield StreamEvent(type="content_block_stop", index=info["block_index"])
|
|
yield StreamEvent(type="message_stop")
|