Conversation
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
| return &GitHubDeviceAuthPollResult{Status: "error", Error: "expired_token", ErrorDesc: "device code expired"}, nil | |
| return &GitHubDeviceAuthPollResult{Status: "error", Error: "expired_code", ErrorDesc: "device code expired"}, nil |
| func (s *inMemoryGitHubDeviceSessionStore) Set(_ context.Context, id string, sess *GitHubDeviceSession, _ time.Duration) error { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
| s.sessions[id] = sess |
There was a problem hiding this comment.
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.
| 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) | |
| } |
There was a problem hiding this comment.
💡 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".
| if strings.HasSuffix(base, "/v1") { | ||
| base = strings.TrimSuffix(base, "/v1") | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <[email protected]>
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 的必填校验
|
Superseded by integration branch. |
Summary
Config
deploy/config.example.yaml.Tests
go test ./...(pass)pnpm -C frontend run typecheck(pass)pnpm -C frontend run lint:check(pass)pnpm -C frontend run test:run(pass; 6 files / 45 tests)