Skip to content

feat: add GitHub Copilot token provider and GitHub device auth#1

Closed
alfadb wants to merge 60 commits intomainfrom
copilot
Closed

feat: add GitHub Copilot token provider and GitHub device auth#1
alfadb wants to merge 60 commits intomainfrom
copilot

Conversation

@alfadb
Copy link
Copy Markdown
Owner

@alfadb alfadb commented Feb 13, 2026

Summary

  • Add GitHub Copilot token provider + helpers for gateway usage, including model listing and platform-aware gateway routing.
  • Add GitHub device auth flow (service + device session store) and wire it into admin/gateway + admin UI device auth modal.
  • Add copilot/aggregator platform support across backend + admin UI (accounts/groups, error passthrough rules, platform badges/icons).
  • Harden upstream URL handling (validated URL helpers for OpenAI Responses / Anthropic Messages) and add gosec annotations/docs.
  • Fix gateway handler to guard against nil API key.

Config

  • Add Copilot + GitHub device auth settings to config and deploy/config.example.yaml.

Tests

  • backend: go test ./... (pass)
  • frontend: pnpm -C frontend run typecheck (pass)
  • frontend: pnpm -C frontend run lint:check (pass)
  • frontend: pnpm -C frontend run test:run (pass; 6 files / 45 tests)

alfadb and others added 14 commits February 13, 2026 21:32
Copilot AI review requested due to automatic review settings February 13, 2026 13:54
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds GitHub Copilot support to the application by implementing token provisioning via GitHub device authentication flow and integrating it with the existing gateway services.

Changes:

  • Added GitHub Copilot token provider that exchanges GitHub personal access tokens for Copilot bearer tokens
  • Implemented GitHub OAuth device authentication flow with session management for obtaining GitHub tokens
  • Extended OpenAI and Claude gateway services to detect and handle GitHub Copilot accounts with special headers and URL patterns

Reviewed changes

Copilot reviewed 38 out of 40 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
frontend/vitest.config.ts Fixed config handling to support function-based vite configs
frontend/src/i18n/locales/*.ts Added translations for GitHub device auth UI
frontend/src/components/admin/account/GitHubDeviceAuthModal.vue New modal component for GitHub device authorization flow
frontend/src/components/admin/account/AccountActionMenu.vue Added device auth action for GitHub Copilot accounts
frontend/src/views/admin/AccountsView.vue Integrated device auth modal and fixed auto-refresh type checking
frontend/src/api/admin/accounts.ts Added API client methods for device auth endpoints
frontend/src/tests/setup.ts Added File.prototype.text polyfill for jsdom compatibility
frontend/src/tests/integration/*.spec.ts Added flushPromises calls to fix test timing issues
deploy/config.example.yaml Added GitHub Copilot and GitHub API domains to allowlist
backend/internal/config/config.go Added GitHub domains to default allowlist
backend/internal/service/github_copilot_token_provider.go Token provider for exchanging GitHub tokens for Copilot tokens
backend/internal/service/github_device_auth_service.go Service implementing GitHub OAuth device flow
backend/internal/service/github_copilot_helpers.go Helper functions for detecting and handling Copilot accounts
backend/internal/service/github_device_session_store.go In-memory session store for device auth sessions
backend/internal/service/openai_responses_url.go URL helper for OpenAI/Copilot responses endpoint
backend/internal/service/anthropic_messages_url.go URL helper for Anthropic messages endpoint
backend/internal/service/openai_gateway_service.go Extended to handle GitHub Copilot accounts with token refresh on 401
backend/internal/service/gateway_service.go Extended to handle GitHub Copilot accounts in Claude gateway
backend/internal/service/account_test_service.go Updated test service to support Copilot token provider
backend/internal/service/token_cache_key.go Added cache key helper for Copilot tokens
backend/internal/service/wire.go Added new services to dependency injection
backend/internal/repository/github_device_session_store.go Redis-backed session store implementation
backend/internal/repository/wire.go Added session store to DI
backend/internal/handler/admin/account_handler.go Added endpoints for starting, polling, and cancelling device auth
backend/internal/server/routes/admin.go Added device auth routes
backend/cmd/server/wire_gen.go Generated wire code with new dependencies
.gitignore Added .sisyphus/ to gitignore
backend/go.mod, backend/go.sum Added google/subcommands and golang.org/x/tools dependencies
backend/internal/service/*_test.go Comprehensive unit tests for new services
backend/internal/repository/*_test.go Unit and integration tests for session store
backend/internal/handler/admin/account_github_device_auth_test.go Handler tests for device auth endpoints

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

expiresAt := time.Unix(sess.ExpiresAtUnix, 0)
if now.After(expiresAt) {
_ = s.store.Delete(ctx, sessionID)
return &GitHubDeviceAuthPollResult{Status: "error", Error: "expired_token", ErrorDesc: "device code expired"}, nil
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.

Error type suffix mismatch: The error type is "expired_token" but should be "expired_code" to accurately reflect that it's the device code that expired, not an access token. This improves clarity and follows the naming convention used in the GitHub OAuth device flow documentation.

Suggested change
return &GitHubDeviceAuthPollResult{Status: "error", Error: "expired_token", ErrorDesc: "device code expired"}, nil
return &GitHubDeviceAuthPollResult{Status: "error", Error: "expired_code", ErrorDesc: "device code expired"}, nil

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +50
func (s *inMemoryGitHubDeviceSessionStore) Set(_ context.Context, id string, sess *GitHubDeviceSession, _ time.Duration) error {
s.mu.Lock()
defer s.mu.Unlock()
s.sessions[id] = sess
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 in-memory implementation of GitHubDeviceSessionStore ignores the TTL parameter and never expires sessions. This can lead to memory leaks if sessions are not explicitly cancelled or if poll operations fail. Consider implementing automatic expiration using a background goroutine with time.After, or at minimum document that this implementation is only suitable for testing and that production should use the Redis-backed implementation.

Suggested change
func (s *inMemoryGitHubDeviceSessionStore) Set(_ context.Context, id string, sess *GitHubDeviceSession, _ time.Duration) error {
s.mu.Lock()
defer s.mu.Unlock()
s.sessions[id] = sess
func (s *inMemoryGitHubDeviceSessionStore) Set(_ context.Context, id string, sess *GitHubDeviceSession, ttl time.Duration) error {
s.mu.Lock()
s.sessions[id] = sess
s.mu.Unlock()
if ttl > 0 {
go func(id string, ttl time.Duration) {
timer := time.NewTimer(ttl)
defer timer.Stop()
<-timer.C
_ = s.Delete(context.Background(), id)
}(id, ttl)
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3747043ab7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +17 to +19
if strings.HasSuffix(base, "/v1") {
base = strings.TrimSuffix(base, "/v1")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve Copilot base path when appending /responses

When isGitHubCopilot is true, this strips any trailing /v1 unconditionally before appending /responses. That breaks path-prefixed deployments such as https://...githubcopilot.com/copilot/v1, which should resolve to /copilot/v1/responses but now become /copilot/responses and return 404. This is a regression for valid base URLs that intentionally include a routed /v1 segment.

Useful? React with 👍 / 👎.

@alfadb alfadb closed this Feb 13, 2026
@alfadb alfadb reopened this Feb 13, 2026
alfadb and others added 27 commits February 14, 2026 20:05
WHAT: 在 /v1/messages、/v1/responses、/v1beta 支持 {platform}/{model} 并进行跨协议转发
WHY: 让 Claude Code / Codex / Gemini CLI 可跨平台调用模型,并修复 Copilot chat-only 模型不支持 Responses API 的失败
HOW: 增加 namespace 解析与强制路由;实现 Responses<->Claude 与 GeminiNative<->Claude 转换;加入 Copilot 模型刷新(定时+手动)
WHAT: 为 Copilot APIKey 账号提供"刷新模型"操作
WHY: Copilot 官方模型列表可能变化,需要手动触发同步
HOW: 调用 /admin/accounts/:id/models/refresh 并提示结果
WHAT:
- Add Provider concept to distinguish API protocol (Platform) from actual service source
- Refactor ModelNamespace to support provider-prefixed model IDs
- Extend PricingService with provider-aware pricing lookup and ModelInfo struct
- Enhance GatewayService with multi-group query support (GetAvailableModelsByGroupIDs, GetAccessibleGroupIDs)
- Add ListSchedulableByGroupIDs and ListPublicGroupIDs to AccountRepo
- Refactor Models API to use new multi-group logic and integrate pricing info

WHY:
- Same model name (e.g., gpt-4) may have different context windows and pricing across providers
  (OpenAI, Azure, Copilot). Current Platform-only abstraction cannot differentiate them.
- Enables provider-specific routing, pricing, and context window resolution.

HOW:
1. Define Provider constants (openai/azure/copilot/anthropic/gemini/vertex/bedrock/openrouter/aggregator)
2. Add ProviderToPlatform mapping for automatic platform inference
3. Extend ModelNamespace with Provider field and inferFromModelName logic
4. Add GetModelPricingWithProvider, GetModelInfo, GetContextWindow to PricingService
5. Refactor Models endpoint to use GetAccessibleGroupIDs (merge public + allowed groups)
6. Support ListSchedulableByGroupIDs for efficient batch group queries
WHAT:
- Add GroupRepository.ListPublicGroupIDs stub
- Add AccountRepository.ListSchedulableByGroupIDs stub

WHY:
- Keep API contract tests compiling after repo interface changes

HOW:
- Implement minimal in-memory ID collection; return empty schedulable list stub
WHAT:
- Add AccountRepository.ListSchedulableByGroupIDs stubs
- Add GroupRepository.ListPublicGroupIDs stubs

WHY:
- Keep multiplatform gateway tests compiling after repo interface changes

HOW:
- Implement minimal mock methods returning empty lists / derived public group IDs
WHAT:
- Add AccountRepository.ListSchedulableByGroupIDs stub
- Add GroupRepository.ListPublicGroupIDs stub

WHY:
- Service-level unit tests use repo stubs that must satisfy updated interfaces

HOW:
- Implement stubs as panic-on-use (unexpected call) to preserve existing test intent
WHAT:
- Allow API keys without GroupID to pass auth middleware without billing/subscription checks

WHY:
- Groupless keys require resolving an effective group in the handler (model/platform dependent)
- Billing checks must run after the effective group is known

HOW:
- Populate API key/user context and set nil group context when GroupID is absent
- Add a unit test asserting groupless keys skip the precheck
WHAT:
- Resolve an effective API key (and group) before running CountTokens billing checks
- Return 503 when no accessible group can be resolved

WHY:
- Groupless API keys require model/provider-aware group resolution in the handler
- Billing/subscription validation must run against the resolved group

HOW:
- Call resolveEffectiveAPIKey(...) early in CountTokens and handle resolution errors
- Add a unit test covering the groupless key resolution path
WHAT:
- Treat subscription-type groups as subscription mode even when subscription is nil

WHY:
- Callers may rely on cached subscription state without passing a hydrated subscription object
- Eligibility checks should not silently fall back to balance mode for subscription groups

HOW:
- Determine subscription mode from group type alone
- Add a unit test covering subscription groups with nil subscription input
WHAT:
- When recording usage for subscription groups, fetch the active subscription if input.Subscription is nil

WHY:
- Some call paths have API key + group context but do not pass a hydrated subscription object
- Usage recording must still attribute cost to subscription usage (not user balance)

HOW:
- Call userSubRepo.GetActiveByUserIDAndGroupID for subscription-type groups
- Add a unit test asserting subscription usage is incremented and balance is not deducted
WHAT:
- Stop asserting http.CloseNotifier on the wrapped ResponseWriter

WHY:
- http.CloseNotifier is deprecated; callers may still expose CloseNotify without implementing the deprecated interface

HOW:
- Use an anonymous interface matching CloseNotify() instead
WHAT:
- Simplify the platform eligibility check for RefreshAvailableModels

WHY:
- The logic is equivalent but easier to read and less error-prone

HOW:
- Replace a negated OR condition with an AND condition
WHAT:
- Simplify cleanup of token-related keys when normalizing OpenAI request bodies

WHY:
- delete() is safe on missing keys; redundant presence checks add noise

HOW:
- Always delete the alternate token keys after mapping to max_output_tokens
WHAT:
- Simplify default base URL selection in testOpenAIAccountConnection

WHY:
- The logic is equivalent but clearer and easier to extend for more platforms

HOW:
- Replace nested if/else with a switch on account.Platform
WHAT:
- Remove redundant type conversion when mapping Gemini tool parameters

WHY:
- The parameters already satisfy the required type; extra wrapping adds noise

HOW:
- Assign fd.Parameters directly and keep the existing nil fallback behavior
WHAT:
- 将 ProviderCopilot/ProviderAggregator 映射到对应 Platform
- 更新 ModelNamespace 的单元测试期望

WHY:
- ParseModelNamespace 依赖 ProviderToPlatform 推导 ForcePlatform
- 之前将 copilot/aggregator 视为 openai 会导致跨平台路由与调度选择错误

HOW:
- 调整 backend/internal/domain/constants.go 的 ProviderToPlatform
- 补充/修正 backend/internal/service/model_namespace_test.go 覆盖
WHAT:
- 基于账号信息推断 provider(openai/azure/openrouter/copilot/aggregator...)
- 收集可用模型时输出 provider/model 形式,避免同名模型冲突

WHY:
- 同一模型名在不同 provider 下可能有不同上下文窗口、定价与路由策略
- /v1/models 与跨协议路由需要稳定、可区分的 model ID

HOW:
- 新增 inferProviderFromAccount()(platform + base_url hostname 推断)
- GatewayService.collectModelsFromAccounts 统一补全 provider 前缀
- 添加单元测试覆盖 OpenAI/Azure/Copilot 场景
WHAT:
- /v1/models 默认返回 openai/ 前缀的模型 ID,并对 provider/model 进行拆解
- 定价数据透传 source 字段到模型列表响应

WHY:
- 多 provider 同名模型需要在模型列表中可区分且可用于定价/上下文查询
- source 便于审计定价来源,排查价格/上下文窗口差异

HOW:
- PricingService 解析并返回 pricing 的 source
- openai.Model 与 service.ModelInfo 增加 source 字段
- GatewayHandler.Models 根据 namespaced model ID 选择正确 provider 做 GetModelInfo
- 添加单元测试覆盖 azure/copilot/aggregator 的 namespaced models 与 source
WHAT:
- 创建 API Key 时不再强制要求 group_id

WHY:
- groupless API key 已在 handler 层支持按请求模型解析有效分组
- UI 强制校验会阻止该能力,同时与已支持的 group 变更操作不一致

HOW:
- 移除 KeysView.vue 中对 formData.group_id 的必填校验
@alfadb
Copy link
Copy Markdown
Owner Author

alfadb commented Feb 16, 2026

Superseded by integration branch.

@alfadb alfadb closed this Feb 16, 2026
@alfadb alfadb deleted the copilot branch February 16, 2026 15:12
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