Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
17 changes: 17 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,23 @@ GEMINI_API_KEY=
# LLM_OPENROUTER_API_KEY=sk-or-xxx
# LLM_OPENROUTER_MODELS=~anthropic/claude-sonnet-latest,~openai/gpt-latest
# LITELLM_MODEL=openai/~anthropic/claude-sonnet-latest
#
# GitHub Copilot(OAuth 设备流,无需手动配置 API Key)
# 来源:https://docs.litellm.ai/docs/providers/github_copilot
# 需要付费 GitHub Copilot 订阅;首次调用时 LiteLLM 会在终端打印
# device code 与 GitHub 验证链接,授权后凭据自动保存到
# ~/.config/litellm/github_copilot/,后续无需再次登录。
# 零配置默认:未配置任何 LLM 源时自动启用 copilot 渠道(gpt-4o)。
# 关闭默认:LLM_DEFAULT_COPILOT=false
# LLM_CHANNELS=copilot
# LLM_COPILOT_PROTOCOL=github_copilot
# LLM_COPILOT_MODELS=gpt-4o,claude-sonnet-4
# LITELLM_MODEL=github_copilot/gpt-4o
# 可选:自定义凭据保存位置(参见 LiteLLM 文档)
# GITHUB_COPILOT_TOKEN_DIR=~/.config/litellm/github_copilot
# 兼容:若已自行通过其它方式获取 Copilot Token,可走 OpenAI-compatible 路径
# LLM_COPILOT_PROTOCOL=openai
# GITHUB_COPILOT_TOKEN=ghu_xxx

# 高级:模型路由 YAML 配置(可选,参考 docs/examples/litellm_config.example.yaml)
# LITELLM_CONFIG=./litellm_config.yaml
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/daily_analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,17 @@ jobs:
LLM_OLLAMA_ENABLED: ${{ vars.LLM_OLLAMA_ENABLED || secrets.LLM_OLLAMA_ENABLED }}
LLM_OLLAMA_EXTRA_HEADERS: ${{ vars.LLM_OLLAMA_EXTRA_HEADERS || secrets.LLM_OLLAMA_EXTRA_HEADERS }}

# GitHub Copilot 渠道(OAuth 设备流为交互式流程,不适用于无人值守 CI;
# 此处仅保留 env 占位,方便已有 GITHUB_COPILOT_TOKEN 的用户走 OpenAI 兼容路径。)
LLM_COPILOT_PROTOCOL: ${{ vars.LLM_COPILOT_PROTOCOL || secrets.LLM_COPILOT_PROTOCOL }}
LLM_COPILOT_BASE_URL: ${{ vars.LLM_COPILOT_BASE_URL || secrets.LLM_COPILOT_BASE_URL }}
LLM_COPILOT_API_KEY: ${{ secrets.LLM_COPILOT_API_KEY }}
LLM_COPILOT_API_KEYS: ${{ secrets.LLM_COPILOT_API_KEYS }}
LLM_COPILOT_MODELS: ${{ vars.LLM_COPILOT_MODELS || secrets.LLM_COPILOT_MODELS }}
LLM_COPILOT_ENABLED: ${{ vars.LLM_COPILOT_ENABLED || secrets.LLM_COPILOT_ENABLED }}
LLM_COPILOT_EXTRA_HEADERS: ${{ vars.LLM_COPILOT_EXTRA_HEADERS || secrets.LLM_COPILOT_EXTRA_HEADERS }}
GITHUB_COPILOT_TOKEN: ${{ secrets.GITHUB_COPILOT_TOKEN }}

# ==========================================
# 数据源
# ==========================================
Expand Down
11 changes: 9 additions & 2 deletions apps/dsa-web/src/components/settings/LLMChannelEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const PROTOCOL_OPTIONS: Array<{ value: ChannelProtocol; label: string }> = [
{ value: 'anthropic', label: 'Anthropic' },
{ value: 'vertex_ai', label: 'Vertex AI' },
{ value: 'ollama', label: 'Ollama' },
{ value: 'github_copilot', label: 'GitHub Copilot (OAuth)' },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve Copilot protocol when loading channel settings

Adding github_copilot to the selectable protocols here introduces a regression because the editor’s protocol parser (normalizeProtocol used by inferProtocol) still falls back unknown values to openai. In practice, when an existing config contains LLM_COPILOT_PROTOCOL=github_copilot, opening and saving the Settings page rewrites that channel as openai, which breaks the intended OAuth device-flow path and can make the channel fail without a manually provided token/key. Please add explicit github_copilot normalization (and related alias handling) so Copilot channels round-trip safely.

Useful? React with 👍 / 👎.

];

const KNOWN_MODEL_PREFIXES = new Set([
Expand Down Expand Up @@ -229,7 +230,7 @@ const ChannelRow: React.FC<ChannelRowProps> = ({
</span>
</Tooltip>
) : null}
{!hasKey && channel.protocol !== 'ollama' ? <Badge variant="warning">未填 Key</Badge> : null}
{!hasKey && channel.protocol !== 'ollama' && channel.protocol !== 'github_copilot' ? <Badge variant="warning">未填 Key</Badge> : null}
{testState?.status !== 'idle' ? (
<Badge variant={statusVariant}>
{testState?.status === 'success' ? '连接正常' : testState?.status === 'error' ? '连接失败' : '测试中'}
Expand Down Expand Up @@ -342,7 +343,13 @@ const ChannelRow: React.FC<ChannelRowProps> = ({
value={channel.apiKey}
disabled={busy}
onChange={(e) => onUpdate(index, 'apiKey', e.target.value)}
placeholder={channel.protocol === 'ollama' ? '本地 Ollama 可留空' : '支持多个 Key 逗号分隔'}
placeholder={
channel.protocol === 'ollama'
? '本地 Ollama 可留空'
: channel.protocol === 'github_copilot'
? '首次调用会触发 OAuth 设备流,留空即可'
: '支持多个 Key 逗号分隔'
}
/>

<div className="space-y-3 rounded-xl border border-[var(--settings-border)] bg-[var(--settings-surface-hover)] p-3">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('llmProviderTemplates', () => {
'anthropic',
'openai',
'ollama',
'copilot',
'custom',
]);
});
Expand Down Expand Up @@ -99,6 +100,7 @@ describe('llmProviderTemplates', () => {
anthropic: 'claude-sonnet-4-6,claude-opus-4-7',
vertex_ai: 'gemini-3.1-pro-preview',
ollama: 'llama3.2,qwen2.5',
github_copilot: 'gpt-4o,claude-sonnet-4',
});
});
});
24 changes: 23 additions & 1 deletion apps/dsa-web/src/components/settings/llmProviderTemplates.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
export type ChannelProtocol = 'openai' | 'deepseek' | 'gemini' | 'anthropic' | 'vertex_ai' | 'ollama';
export type ChannelProtocol =
| 'openai'
| 'deepseek'
| 'gemini'
| 'anthropic'
| 'vertex_ai'
| 'ollama'
| 'github_copilot';
export type LLMProviderCapability =
| 'openai-compatible'
| 'aggregator'
Expand Down Expand Up @@ -199,6 +206,20 @@ export const LLM_PROVIDER_TEMPLATES: LLMProviderTemplate[] = [
configHint: '需要本机、Docker 或 self-hosted runner 能访问 Ollama 服务。',
officialSources: [{ label: 'Ollama API', url: 'https://github.com/ollama/ollama/blob/main/docs/api.md' }],
},
{
channelId: 'copilot',
label: 'GitHub Copilot(OAuth 设备流,无需 API Key)',
protocol: 'github_copilot',
baseUrl: '',
placeholderModels: 'gpt-4o,claude-sonnet-4,gemini-2.5-pro',
capabilities: ['official-api'],
configHint:
'需要付费 GitHub Copilot 订阅。首次调用时 LiteLLM 会在终端打印 device code 与 GitHub 验证链接,授权后凭据自动保存到 ~/.config/litellm/github_copilot/,之后无需再次登录。',
officialSources: [
{ label: 'GitHub Copilot Docs', url: 'https://docs.github.com/en/copilot' },
{ label: 'LiteLLM GitHub Copilot Provider', url: 'https://docs.litellm.ai/docs/providers/github_copilot' },
],
},
{
channelId: 'custom',
label: '自定义渠道',
Expand Down Expand Up @@ -232,4 +253,5 @@ export const MODEL_PLACEHOLDERS_BY_PROTOCOL: Record<ChannelProtocol, string> = {
anthropic: 'claude-sonnet-4-6,claude-opus-4-7',
vertex_ai: 'gemini-3.1-pro-preview',
ollama: 'llama3.2,qwen2.5',
github_copilot: 'gpt-4o,claude-sonnet-4',
};
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- [改进] 补齐通知渠道 P0 基线、Actions 映射与 `--check-notify` 只读诊断,完善 AstrBot 配置入口和通知回归快照。
- [chore] 清理仓库根目录:移除误入库的 `.codex`、`review.md` 跟踪记录,将 smoke 测试入口迁移到 `scripts/`、环境检查脚本迁移为 `scripts/check_env.py`,并将 LiteLLM YAML 示例迁移到 `docs/examples/`。
- [新功能] Web 设置页新增通知渠道一键测试,支持临时配置、耗时与脱敏 attempts 展示。
- [新功能] LLM 渠道新增 GitHub Copilot 预设:基于 LiteLLM 原生 `github_copilot/` provider,首次调用通过 OAuth 设备流自动完成授权,凭据本地持久化,无需手动配置 API Key(需付费 GitHub Copilot 订阅)。
- [改进] 未配置任何 LLM 源时,`main.py` / `server.py` 默认启用 GitHub Copilot 渠道,首次调用触发 OAuth 设备流;可通过 `LLM_DEFAULT_COPILOT=false` 关闭。

## [3.15.0] - 2026-05-05

Expand Down
2 changes: 2 additions & 0 deletions docs/llm-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ OpenAI-compatible Base URL 只填到服务商兼容入口,不额外拼接 `/ch
| 火山方舟 / 豆包 | `volcengine` | `openai` | `https://ark.cn-beijing.volces.com/api/v3` | `doubao-seed-1-6-251015,doubao-seed-1-6-thinking-251015` |
| 硅基流动 / SiliconFlow | `siliconflow` | `openai` | `https://api.siliconflow.cn/v1` | `deepseek-ai/DeepSeek-V3.2,Qwen/Qwen3-235B-A22B-Thinking-2507` |
| OpenRouter | `openrouter` | `openai` | `https://openrouter.ai/api/v1` | `~anthropic/claude-sonnet-latest,~openai/gpt-latest` |
| GitHub Copilot | `copilot` | `github_copilot` | 留空(LiteLLM 自动注入) | `gpt-4o,claude-sonnet-4,gemini-2.5-pro` |
| Ollama | `ollama` | `ollama` | `http://127.0.0.1:11434` | `llama3.2,qwen2.5` |

## 官方来源与兼容性
Expand All @@ -85,6 +86,7 @@ OpenAI-compatible Base URL 只填到服务商兼容入口,不额外拼接 `/ch
| 火山方舟 / 豆包 | [在线推理(常规)](https://www.volcengine.com/docs/82379/2121998)、[模型列表](https://www.volcengine.com/docs/82379/1949118) | 官方示例使用 `https://ark.cn-beijing.volces.com/api/v3` 与 `doubao-seed-1-6-251015`;如使用 Coding Plan,请改用其专用 Base URL 和模型名,不要套用本表的在线推理模板。 |
| SiliconFlow | [模型列表](https://docs.siliconflow.cn/quickstart/models)、[获取模型列表 API](https://docs.siliconflow.cn/cn/api-reference/models/get-model-list) | 平台模型实时更新且 `/models` 需要 API Key;模板只给常见新模型示例,保存前建议在 Web 设置页点击「获取模型」确认账号可见性。 |
| OpenRouter | [Models API](https://openrouter.ai/docs/api/api-reference/models/get-models) | OpenRouter 支持 `~anthropic/claude-sonnet-latest`、`~openai/gpt-latest` 等 latest router alias;2026-05-03 的一次手动 live smoke 以 Claude Sonnet latest 作为默认示例通过,GPT latest 保留为可按账号权限切换的备选。 |
| GitHub Copilot | [Copilot 文档](https://docs.github.com/en/copilot)、[LiteLLM GitHub Copilot Provider](https://docs.litellm.ai/docs/providers/github_copilot) | 需要付费 GitHub Copilot 订阅。`copilot` 渠道默认走 LiteLLM 原生 `github_copilot/` provider:首次调用时会在终端打印 device code 与 GitHub 验证链接,授权后凭据自动保存到 `~/.config/litellm/github_copilot/`,后续无需重复登录,也不需要手动维护 `LLM_COPILOT_API_KEY`。运行时模型形如 `github_copilot/gpt-4o`。可选:通过 `GITHUB_COPILOT_TOKEN_DIR` 自定义凭据目录;如已有现成 Copilot Token,可改为 `LLM_COPILOT_PROTOCOL=openai` + `GITHUB_COPILOT_TOKEN` 的兼容模式(此时模型形如 `openai/gpt-4o`)。注意:CI / 无人值守环境无法完成交互式 OAuth,建议仅在本地或桌面端使用 OAuth 设备流,CI 中改用其他渠道。 |
| LiteLLM | [OpenAI-Compatible Endpoints](https://docs.litellm.ai/docs/providers/openai_compatible) | OpenAI-compatible 端点需要把运行时模型写成 `openai/<model>`,Base URL 只填到服务商兼容入口,不额外拼接 `/chat/completions`。 |

本页预设只保证配置形状与当前依赖的 OpenAI-compatible 路由规则一致;实际连通性仍取决于服务商账号权限、地域、额度和模型开通状态。当前 LiteLLM 版本约束为 `litellm>=1.80.10,!=1.82.7,!=1.82.8,<2.0.0`(见 `requirements.txt`),保留历史最低版本、显式排除 PyPI 事故版本,并避免未来大版本自动进入。
Expand Down
79 changes: 78 additions & 1 deletion src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,39 @@ def __str__(self) -> str: # noqa: D105


_MANAGED_LITELLM_KEY_PROVIDERS = {"gemini", "vertex_ai", "anthropic", "openai", "deepseek"}
SUPPORTED_LLM_CHANNEL_PROTOCOLS = ("openai", "anthropic", "gemini", "vertex_ai", "deepseek", "ollama")
SUPPORTED_LLM_CHANNEL_PROTOCOLS = (
"openai",
"anthropic",
"gemini",
"vertex_ai",
"deepseek",
"ollama",
"github_copilot",
)
_FALSEY_ENV_VALUES = {"0", "false", "no", "off"}
# Fallback defaults used when ANSPIRE_API_KEYS is reused as legacy OpenAI-compatible source.
# These are compatibility examples; actual availability should be validated by Anspire console/model entitlement.
ANSPIRE_LLM_BASE_URL_DEFAULT = "https://open-gateway.anspire.cn/v6"
ANSPIRE_LLM_MODEL_DEFAULT = "Doubao-Seed-2.0-lite"
# GitHub Copilot endpoints.
# - The OpenAI-compatible API endpoint at https://api.githubcopilot.com is
# only used as the legacy fallback when a user manually pre-provisions a
# GITHUB_COPILOT_TOKEN; in that mode the runtime model name remains
# ``openai/<model>``.
# - The default copilot channel uses LiteLLM's native ``github_copilot``
# provider, which triggers an OAuth device-flow on first use and stores
# refreshed tokens under ``~/.config/litellm/github_copilot/`` (see
# https://docs.litellm.ai/docs/providers/github_copilot). No API key is
# required up-front for that path.
# Reference: https://docs.github.com/en/copilot/using-github-copilot/asking-github-copilot-questions-in-your-ide
GITHUB_COPILOT_BASE_URL_DEFAULT = "https://api.githubcopilot.com"
GITHUB_COPILOT_INTEGRATION_ID = "vscode-chat"
# Default model surfaced by the copilot channel when LLM_COPILOT_MODELS is
# unset. ``gpt-4o`` is the most universally accessible Copilot-managed
# model name; users can override via LLM_COPILOT_MODELS to e.g.
# ``gpt-4.1`` / ``claude-sonnet-4`` / ``gemini-2.5-pro`` once their
# Copilot account has access.
GITHUB_COPILOT_DEFAULT_MODEL = "gpt-4o"
# Kimi K2.6 is consumed through Moonshot's OpenAI-compatible API in this
# repository. Official references:
# - https://platform.kimi.ai/docs/guide/kimi-k2-6-quickstart
Expand Down Expand Up @@ -200,6 +227,9 @@ def canonicalize_llm_channel_protocol(value: Optional[str]) -> str:
"google": "gemini",
"vertex": "vertex_ai",
"vertexai": "vertex_ai",
"copilot": "github_copilot",
"github": "github_copilot",
"githubcopilot": "github_copilot",
}
return aliases.get(candidate, candidate)

Expand Down Expand Up @@ -245,6 +275,9 @@ def channel_allows_empty_api_key(protocol: Optional[str], base_url: Optional[str
resolved_protocol = resolve_llm_channel_protocol(protocol, base_url=base_url)
if resolved_protocol == "ollama":
return True
# github_copilot uses LiteLLM's built-in OAuth device flow; no static API key.
if resolved_protocol == "github_copilot":
return True
parsed = urlparse(base_url or "")
return parsed.hostname in {"127.0.0.1", "localhost", "0.0.0.0"}

Expand Down Expand Up @@ -1194,6 +1227,26 @@ def _load_from_env(cls) -> 'Config':
if llm_model_list:
llm_models_source = "legacy_env"

# Priority 4: zero-config default → GitHub Copilot OAuth device flow.
# Only kicks in when nothing else is configured. The first LLM call
# will print a device code in the terminal that runs main.py / serve;
# credentials are persisted to ~/.config/litellm/github_copilot/ by
# LiteLLM and reused on subsequent runs. Requires a paid GitHub
# Copilot subscription. Disable by setting LLM_DEFAULT_COPILOT=false
# or by configuring any other LLM source above.
if not llm_model_list and parse_env_bool(
os.getenv('LLM_DEFAULT_COPILOT'), default=True
):
llm_channels = cls._parse_llm_channels('copilot')
llm_model_list = cls._channels_to_model_list(llm_channels)
if llm_model_list:
llm_models_source = "default_copilot"
logger.info(
"No LLM source configured; defaulting to GitHub Copilot "
"channel (OAuth device flow). Set LLM_DEFAULT_COPILOT=false "
"to opt out."
)

if (
inferred_legacy_deepseek_model
and llm_models_source == "legacy_env"
Expand Down Expand Up @@ -1714,6 +1767,21 @@ def _parse_llm_channels(cls, channels_str: str) -> List[Dict[str, Any]]:
protocol_raw = os.getenv(f'LLM_{ch_upper}_PROTOCOL', '').strip()
if ch_lower == "anspire" and not protocol_raw:
protocol_raw = "openai"
if ch_lower == "copilot" and not protocol_raw:
# Default to LiteLLM native github_copilot provider so the
# OAuth device flow handles credentials automatically; users
# can still set LLM_COPILOT_PROTOCOL=openai + GITHUB_COPILOT_TOKEN
# for the legacy pre-provisioned token path.
protocol_raw = "github_copilot"
# Only fall back to the OpenAI-compatible Copilot endpoint when
# the user explicitly opts into the openai protocol; the native
# github_copilot provider injects its own api_base.
if (
ch_lower == "copilot"
and not base_url
and canonicalize_llm_channel_protocol(protocol_raw) == "openai"
):
base_url = GITHUB_COPILOT_BASE_URL_DEFAULT
enabled_raw = os.getenv(f'LLM_{ch_upper}_ENABLED')
if ch_lower == "anspire" and (enabled_raw is None or not enabled_raw.strip()):
enabled_raw = os.getenv('ANSPIRE_LLM_ENABLED')
Expand All @@ -1729,6 +1797,10 @@ def _parse_llm_channels(cls, channels_str: str) -> List[Dict[str, Any]]:
if not api_keys and ch_lower == "anspire":
anspire_keys_raw = os.getenv('ANSPIRE_API_KEYS', '')
api_keys = [k.strip() for k in anspire_keys_raw.split(',') if k.strip()]
if not api_keys and ch_lower == "copilot":
_copilot_token = os.getenv('GITHUB_COPILOT_TOKEN', '').strip()
if _copilot_token:
api_keys = [_copilot_token]

# Models
models_raw = os.getenv(f'LLM_{ch_upper}_MODELS', '')
Expand All @@ -1739,6 +1811,8 @@ def _parse_llm_channels(cls, channels_str: str) -> List[Dict[str, Any]]:
).strip()
if anspire_model:
raw_models = [anspire_model]
if not raw_models and ch_lower == "copilot":
raw_models = [GITHUB_COPILOT_DEFAULT_MODEL]
protocol = resolve_llm_channel_protocol(protocol_raw, base_url=base_url, models=raw_models, channel_name=ch_name)
models = [normalize_llm_channel_model(m, protocol, base_url) for m in raw_models]

Expand Down Expand Up @@ -1804,6 +1878,9 @@ def _channels_to_model_list(cls, channels: List[Dict[str, Any]]) -> List[Dict[st
headers = dict(ch.get('extra_headers') or {})
if ch['base_url'] and 'aihubmix.com' in ch['base_url']:
headers.setdefault('APP-Code', 'GPIJ3886')
# Auto-inject GitHub Copilot required integration header
if ch['base_url'] and 'githubcopilot.com' in ch['base_url']:
headers.setdefault('Copilot-Integration-Id', GITHUB_COPILOT_INTEGRATION_ID)
if headers:
litellm_params['extra_headers'] = headers

Expand Down
Empty file added test_out.txt
Empty file.
10 changes: 9 additions & 1 deletion tests/test_daily_analysis_workflow_llm_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,16 @@
"anthropic",
"openai",
"ollama",
"copilot",
}

# Channels whose runtime authentication does not rely on a static API key
# (Ollama runs locally without auth, GitHub Copilot uses LiteLLM's OAuth
# device flow). They are still wired through the channel env conventions
# but ``LLM_<CHANNEL>_API_KEY=`` is intentionally absent from the example
# `.env`.
CHANNELS_WITHOUT_API_KEY_IN_EXAMPLE = {"ollama", "copilot"}


def _extract_provider_templates() -> dict[str, str]:
content = TEMPLATE_PATH.read_text(encoding="utf-8")
Expand Down Expand Up @@ -103,7 +111,7 @@ def test_env_example_includes_provider_template_channel_examples() -> None:
assert f"LLM_CHANNELS={channel}" in env_example
assert f"LLM_{upper}_MODELS=" in env_example

if channel != "ollama":
if channel not in CHANNELS_WITHOUT_API_KEY_IN_EXAMPLE:
assert f"LLM_{upper}_API_KEY=" in env_example
if base_url:
assert f"LLM_{upper}_BASE_URL=" in env_example
Expand Down
Loading