copilot-python 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- copilot_python-0.1.0/LICENSE +21 -0
- copilot_python-0.1.0/PKG-INFO +239 -0
- copilot_python-0.1.0/README.md +200 -0
- copilot_python-0.1.0/pyproject.toml +35 -0
- copilot_python-0.1.0/setup.cfg +4 -0
- copilot_python-0.1.0/src/copilot_python/__init__.py +27 -0
- copilot_python-0.1.0/src/copilot_python/__main__.py +5 -0
- copilot_python-0.1.0/src/copilot_python/cli.py +3 -0
- copilot_python-0.1.0/src/copilot_python/client.py +1 -0
- copilot_python-0.1.0/src/copilot_python.egg-info/PKG-INFO +239 -0
- copilot_python-0.1.0/src/copilot_python.egg-info/SOURCES.txt +18 -0
- copilot_python-0.1.0/src/copilot_python.egg-info/dependency_links.txt +1 -0
- copilot_python-0.1.0/src/copilot_python.egg-info/entry_points.txt +3 -0
- copilot_python-0.1.0/src/copilot_python.egg-info/requires.txt +1 -0
- copilot_python-0.1.0/src/copilot_python.egg-info/top_level.txt +2 -0
- copilot_python-0.1.0/src/copilot_wrapper/__init__.py +27 -0
- copilot_python-0.1.0/src/copilot_wrapper/__main__.py +5 -0
- copilot_python-0.1.0/src/copilot_wrapper/cli.py +73 -0
- copilot_python-0.1.0/src/copilot_wrapper/client.py +311 -0
- copilot_python-0.1.0/tests/test_client.py +132 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 copilot-python contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: copilot-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python wrapper for GitHub Copilot and GitHub Models using the official openai SDK.
|
|
5
|
+
Author: copilot-python contributors
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 copilot-python contributors
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Keywords: github,copilot,cli,wrapper,python
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
34
|
+
Requires-Python: >=3.9
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Requires-Dist: openai>=1.30.0
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# copilot-python
|
|
41
|
+
|
|
42
|
+
一個使用官方 `openai` 套件實作的 GitHub Copilot / GitHub Models 包裝庫。
|
|
43
|
+
|
|
44
|
+
## 功能
|
|
45
|
+
|
|
46
|
+
- 直接使用官方 `openai.OpenAI`
|
|
47
|
+
- 支援 GitHub OAuth Device Flow 取得 Token
|
|
48
|
+
- 支援手動提供 Fine-grained PAT / API Key
|
|
49
|
+
- 支援 Windows / macOS / Linux
|
|
50
|
+
|
|
51
|
+
## 前置需求
|
|
52
|
+
|
|
53
|
+
若使用 GitHub Models,常見端點為:
|
|
54
|
+
|
|
55
|
+
- `https://models.github.ai/inference`
|
|
56
|
+
- 或你的 OpenAI 相容代理端點
|
|
57
|
+
|
|
58
|
+
**內建 OAuth Client ID**:本庫已內建公開的 GitHub OAuth App client_id,可直接使用 OAuth Device Flow,無需額外設定。
|
|
59
|
+
|
|
60
|
+
## 安裝本庫
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install .
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
或直接安裝依賴:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install openai
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 快速開始
|
|
73
|
+
|
|
74
|
+
### 1. 手動提供 API Key / Token
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from openai import OpenAI
|
|
78
|
+
|
|
79
|
+
client = OpenAI(
|
|
80
|
+
base_url="https://models.github.ai/inference",
|
|
81
|
+
api_key="github_pat_xxxxxxxxx",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
response = client.chat.completions.create(
|
|
85
|
+
messages=[
|
|
86
|
+
{"role": "system", "content": "你是一個有用的助手。"},
|
|
87
|
+
{"role": "user", "content": "請解釋什麼是量子糾纏。"},
|
|
88
|
+
],
|
|
89
|
+
model="gpt-4o",
|
|
90
|
+
temperature=1,
|
|
91
|
+
max_tokens=4096,
|
|
92
|
+
top_p=1,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
print(response.choices[0].message.content)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. 使用 CLI 命令取得 Token(推薦)
|
|
99
|
+
|
|
100
|
+
最簡單的方式是使用命令列工具:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
copilot-python login
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
這會開啟瀏覽器進行 GitHub 授權,並輸出 access token。
|
|
107
|
+
|
|
108
|
+
### 3. 程式碼中使用 OAuth Device Flow
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from openai import OpenAI
|
|
112
|
+
from copilot_python import login_device_flow
|
|
113
|
+
|
|
114
|
+
# 使用內建 client_id,無需額外提供
|
|
115
|
+
token = login_device_flow()
|
|
116
|
+
|
|
117
|
+
client = OpenAI(
|
|
118
|
+
base_url="https://models.github.ai/inference",
|
|
119
|
+
api_key=token,
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 4. 使用 `CopilotClient`
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from copilot_python import CopilotClient
|
|
127
|
+
|
|
128
|
+
client = CopilotClient(api_key="github_pat_xxxxxxxxx")
|
|
129
|
+
|
|
130
|
+
reply = client.ask(
|
|
131
|
+
"如何寫一個 HTTP 伺服器?",
|
|
132
|
+
model="gpt-4o",
|
|
133
|
+
system_prompt="你是一個有用的助手。",
|
|
134
|
+
)
|
|
135
|
+
print(reply.choices[0].message.content)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 建議 Token 類型
|
|
139
|
+
|
|
140
|
+
常見情境:
|
|
141
|
+
|
|
142
|
+
- OAuth token:`gho_...`
|
|
143
|
+
- Fine-grained PAT:`github_pat_...`
|
|
144
|
+
- GitHub App user-to-server token:`ghu_...`
|
|
145
|
+
- 視服務而定,可能需要 `models` 或 `Copilot Requests` 權限
|
|
146
|
+
|
|
147
|
+
## 命令列工具
|
|
148
|
+
|
|
149
|
+
### 取得 Token
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
copilot-python login
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
這會:
|
|
156
|
+
1. 顯示 GitHub 裝置授權連結
|
|
157
|
+
2. 自動開啟瀏覽器(可用 `--no-open-browser` 停用)
|
|
158
|
+
3. 等待授權完成
|
|
159
|
+
4. 輸出 access token 到 stdout
|
|
160
|
+
|
|
161
|
+
完整選項:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
copilot-python login [--client-id CLIENT_ID] [--scope SCOPE] [--no-open-browser]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 主要 API
|
|
168
|
+
|
|
169
|
+
### `login_device_flow()`
|
|
170
|
+
|
|
171
|
+
程式碼中走 GitHub OAuth Device Flow,回傳 access token。
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from copilot_python import login_device_flow
|
|
175
|
+
|
|
176
|
+
# 使用內建 client_id
|
|
177
|
+
token = login_device_flow()
|
|
178
|
+
|
|
179
|
+
# 或自訂 client_id 和 scope
|
|
180
|
+
token = login_device_flow(
|
|
181
|
+
client_id="你的 client_id",
|
|
182
|
+
scope="repo user",
|
|
183
|
+
)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### `create_client()`
|
|
187
|
+
|
|
188
|
+
建立已填入 `base_url` 與 `api_key` 的官方 `OpenAI` client。
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from copilot_python import create_client
|
|
192
|
+
|
|
193
|
+
client = create_client(base_url="https://models.github.ai/inference", api_key="...")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `ask_copilot()`
|
|
197
|
+
|
|
198
|
+
最方便的單次呼叫入口,回傳官方 SDK 的 completion 物件。
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
ask_copilot(
|
|
202
|
+
prompt,
|
|
203
|
+
*,
|
|
204
|
+
model,
|
|
205
|
+
api_key=None,
|
|
206
|
+
token=None,
|
|
207
|
+
base_url="https://models.github.ai/inference",
|
|
208
|
+
system_prompt=None,
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `CopilotClient.ask()`
|
|
213
|
+
|
|
214
|
+
適合重複呼叫時使用。
|
|
215
|
+
|
|
216
|
+
## 例外
|
|
217
|
+
|
|
218
|
+
- `CopilotError`
|
|
219
|
+
- `CopilotAuthenticationError`
|
|
220
|
+
- `CopilotHTTPError`
|
|
221
|
+
- `CopilotTimeoutError`
|
|
222
|
+
|
|
223
|
+
## 注意事項
|
|
224
|
+
|
|
225
|
+
1. 這個專案現在是 **基於官方 `openai` SDK** 呼叫 OpenAI 相容聊天 API。
|
|
226
|
+
2. 已內建 GitHub OAuth App client_id,可直接使用 `copilot-python login` 或 `login_device_flow()`。
|
|
227
|
+
3. `base_url` 可指向 GitHub Models 或其他 OpenAI 相容端點。
|
|
228
|
+
4. 若手動提供 PAT,請確認 Token 權限符合目標服務需求。
|
|
229
|
+
5. OAuth Device Flow 的 `scope` 參數取決於你使用的 GitHub OAuth App,內建的 client_id 支援標準 OAuth scopes(如 `repo`, `user` 等)。
|
|
230
|
+
|
|
231
|
+
## 相容性
|
|
232
|
+
|
|
233
|
+
舊命令 `copilot-wrapper login` 和舊導入 `from copilot_wrapper import ...` 仍保留向後相容。
|
|
234
|
+
|
|
235
|
+
## 測試
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
python -m unittest discover -s tests -v
|
|
239
|
+
```
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# copilot-python
|
|
2
|
+
|
|
3
|
+
一個使用官方 `openai` 套件實作的 GitHub Copilot / GitHub Models 包裝庫。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- 直接使用官方 `openai.OpenAI`
|
|
8
|
+
- 支援 GitHub OAuth Device Flow 取得 Token
|
|
9
|
+
- 支援手動提供 Fine-grained PAT / API Key
|
|
10
|
+
- 支援 Windows / macOS / Linux
|
|
11
|
+
|
|
12
|
+
## 前置需求
|
|
13
|
+
|
|
14
|
+
若使用 GitHub Models,常見端點為:
|
|
15
|
+
|
|
16
|
+
- `https://models.github.ai/inference`
|
|
17
|
+
- 或你的 OpenAI 相容代理端點
|
|
18
|
+
|
|
19
|
+
**內建 OAuth Client ID**:本庫已內建公開的 GitHub OAuth App client_id,可直接使用 OAuth Device Flow,無需額外設定。
|
|
20
|
+
|
|
21
|
+
## 安裝本庫
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
或直接安裝依賴:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install openai
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 快速開始
|
|
34
|
+
|
|
35
|
+
### 1. 手動提供 API Key / Token
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from openai import OpenAI
|
|
39
|
+
|
|
40
|
+
client = OpenAI(
|
|
41
|
+
base_url="https://models.github.ai/inference",
|
|
42
|
+
api_key="github_pat_xxxxxxxxx",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
response = client.chat.completions.create(
|
|
46
|
+
messages=[
|
|
47
|
+
{"role": "system", "content": "你是一個有用的助手。"},
|
|
48
|
+
{"role": "user", "content": "請解釋什麼是量子糾纏。"},
|
|
49
|
+
],
|
|
50
|
+
model="gpt-4o",
|
|
51
|
+
temperature=1,
|
|
52
|
+
max_tokens=4096,
|
|
53
|
+
top_p=1,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
print(response.choices[0].message.content)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. 使用 CLI 命令取得 Token(推薦)
|
|
60
|
+
|
|
61
|
+
最簡單的方式是使用命令列工具:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
copilot-python login
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
這會開啟瀏覽器進行 GitHub 授權,並輸出 access token。
|
|
68
|
+
|
|
69
|
+
### 3. 程式碼中使用 OAuth Device Flow
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from openai import OpenAI
|
|
73
|
+
from copilot_python import login_device_flow
|
|
74
|
+
|
|
75
|
+
# 使用內建 client_id,無需額外提供
|
|
76
|
+
token = login_device_flow()
|
|
77
|
+
|
|
78
|
+
client = OpenAI(
|
|
79
|
+
base_url="https://models.github.ai/inference",
|
|
80
|
+
api_key=token,
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 4. 使用 `CopilotClient`
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from copilot_python import CopilotClient
|
|
88
|
+
|
|
89
|
+
client = CopilotClient(api_key="github_pat_xxxxxxxxx")
|
|
90
|
+
|
|
91
|
+
reply = client.ask(
|
|
92
|
+
"如何寫一個 HTTP 伺服器?",
|
|
93
|
+
model="gpt-4o",
|
|
94
|
+
system_prompt="你是一個有用的助手。",
|
|
95
|
+
)
|
|
96
|
+
print(reply.choices[0].message.content)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 建議 Token 類型
|
|
100
|
+
|
|
101
|
+
常見情境:
|
|
102
|
+
|
|
103
|
+
- OAuth token:`gho_...`
|
|
104
|
+
- Fine-grained PAT:`github_pat_...`
|
|
105
|
+
- GitHub App user-to-server token:`ghu_...`
|
|
106
|
+
- 視服務而定,可能需要 `models` 或 `Copilot Requests` 權限
|
|
107
|
+
|
|
108
|
+
## 命令列工具
|
|
109
|
+
|
|
110
|
+
### 取得 Token
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
copilot-python login
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
這會:
|
|
117
|
+
1. 顯示 GitHub 裝置授權連結
|
|
118
|
+
2. 自動開啟瀏覽器(可用 `--no-open-browser` 停用)
|
|
119
|
+
3. 等待授權完成
|
|
120
|
+
4. 輸出 access token 到 stdout
|
|
121
|
+
|
|
122
|
+
完整選項:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
copilot-python login [--client-id CLIENT_ID] [--scope SCOPE] [--no-open-browser]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## 主要 API
|
|
129
|
+
|
|
130
|
+
### `login_device_flow()`
|
|
131
|
+
|
|
132
|
+
程式碼中走 GitHub OAuth Device Flow,回傳 access token。
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from copilot_python import login_device_flow
|
|
136
|
+
|
|
137
|
+
# 使用內建 client_id
|
|
138
|
+
token = login_device_flow()
|
|
139
|
+
|
|
140
|
+
# 或自訂 client_id 和 scope
|
|
141
|
+
token = login_device_flow(
|
|
142
|
+
client_id="你的 client_id",
|
|
143
|
+
scope="repo user",
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `create_client()`
|
|
148
|
+
|
|
149
|
+
建立已填入 `base_url` 與 `api_key` 的官方 `OpenAI` client。
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from copilot_python import create_client
|
|
153
|
+
|
|
154
|
+
client = create_client(base_url="https://models.github.ai/inference", api_key="...")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### `ask_copilot()`
|
|
158
|
+
|
|
159
|
+
最方便的單次呼叫入口,回傳官方 SDK 的 completion 物件。
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
ask_copilot(
|
|
163
|
+
prompt,
|
|
164
|
+
*,
|
|
165
|
+
model,
|
|
166
|
+
api_key=None,
|
|
167
|
+
token=None,
|
|
168
|
+
base_url="https://models.github.ai/inference",
|
|
169
|
+
system_prompt=None,
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `CopilotClient.ask()`
|
|
174
|
+
|
|
175
|
+
適合重複呼叫時使用。
|
|
176
|
+
|
|
177
|
+
## 例外
|
|
178
|
+
|
|
179
|
+
- `CopilotError`
|
|
180
|
+
- `CopilotAuthenticationError`
|
|
181
|
+
- `CopilotHTTPError`
|
|
182
|
+
- `CopilotTimeoutError`
|
|
183
|
+
|
|
184
|
+
## 注意事項
|
|
185
|
+
|
|
186
|
+
1. 這個專案現在是 **基於官方 `openai` SDK** 呼叫 OpenAI 相容聊天 API。
|
|
187
|
+
2. 已內建 GitHub OAuth App client_id,可直接使用 `copilot-python login` 或 `login_device_flow()`。
|
|
188
|
+
3. `base_url` 可指向 GitHub Models 或其他 OpenAI 相容端點。
|
|
189
|
+
4. 若手動提供 PAT,請確認 Token 權限符合目標服務需求。
|
|
190
|
+
5. OAuth Device Flow 的 `scope` 參數取決於你使用的 GitHub OAuth App,內建的 client_id 支援標準 OAuth scopes(如 `repo`, `user` 等)。
|
|
191
|
+
|
|
192
|
+
## 相容性
|
|
193
|
+
|
|
194
|
+
舊命令 `copilot-wrapper login` 和舊導入 `from copilot_wrapper import ...` 仍保留向後相容。
|
|
195
|
+
|
|
196
|
+
## 測試
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
python -m unittest discover -s tests -v
|
|
200
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "copilot-python"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A Python wrapper for GitHub Copilot and GitHub Models using the official openai SDK."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {file = "LICENSE"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "copilot-python contributors" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"openai>=1.30.0"
|
|
17
|
+
]
|
|
18
|
+
keywords = ["github", "copilot", "cli", "wrapper", "python"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
copilot-python = "copilot_python.cli:main"
|
|
29
|
+
copilot-wrapper = "copilot_wrapper.cli:main"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools]
|
|
32
|
+
package-dir = {"" = "src"}
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from copilot_wrapper import (
|
|
2
|
+
CopilotAuthenticationError,
|
|
3
|
+
CopilotClient,
|
|
4
|
+
CopilotError,
|
|
5
|
+
CopilotTimeoutError,
|
|
6
|
+
DEFAULT_GITHUB_OAUTH_CLIENT_ID,
|
|
7
|
+
DeviceFlowInfo,
|
|
8
|
+
GitHubOAuthDeviceFlow,
|
|
9
|
+
OpenAI,
|
|
10
|
+
ask_copilot,
|
|
11
|
+
create_client,
|
|
12
|
+
login_device_flow,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"CopilotAuthenticationError",
|
|
17
|
+
"CopilotClient",
|
|
18
|
+
"CopilotError",
|
|
19
|
+
"CopilotTimeoutError",
|
|
20
|
+
"DEFAULT_GITHUB_OAUTH_CLIENT_ID",
|
|
21
|
+
"DeviceFlowInfo",
|
|
22
|
+
"GitHubOAuthDeviceFlow",
|
|
23
|
+
"OpenAI",
|
|
24
|
+
"ask_copilot",
|
|
25
|
+
"create_client",
|
|
26
|
+
"login_device_flow",
|
|
27
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from copilot_wrapper.client import *
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: copilot-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python wrapper for GitHub Copilot and GitHub Models using the official openai SDK.
|
|
5
|
+
Author: copilot-python contributors
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 copilot-python contributors
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Keywords: github,copilot,cli,wrapper,python
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
34
|
+
Requires-Python: >=3.9
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Requires-Dist: openai>=1.30.0
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# copilot-python
|
|
41
|
+
|
|
42
|
+
一個使用官方 `openai` 套件實作的 GitHub Copilot / GitHub Models 包裝庫。
|
|
43
|
+
|
|
44
|
+
## 功能
|
|
45
|
+
|
|
46
|
+
- 直接使用官方 `openai.OpenAI`
|
|
47
|
+
- 支援 GitHub OAuth Device Flow 取得 Token
|
|
48
|
+
- 支援手動提供 Fine-grained PAT / API Key
|
|
49
|
+
- 支援 Windows / macOS / Linux
|
|
50
|
+
|
|
51
|
+
## 前置需求
|
|
52
|
+
|
|
53
|
+
若使用 GitHub Models,常見端點為:
|
|
54
|
+
|
|
55
|
+
- `https://models.github.ai/inference`
|
|
56
|
+
- 或你的 OpenAI 相容代理端點
|
|
57
|
+
|
|
58
|
+
**內建 OAuth Client ID**:本庫已內建公開的 GitHub OAuth App client_id,可直接使用 OAuth Device Flow,無需額外設定。
|
|
59
|
+
|
|
60
|
+
## 安裝本庫
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install .
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
或直接安裝依賴:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install openai
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 快速開始
|
|
73
|
+
|
|
74
|
+
### 1. 手動提供 API Key / Token
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from openai import OpenAI
|
|
78
|
+
|
|
79
|
+
client = OpenAI(
|
|
80
|
+
base_url="https://models.github.ai/inference",
|
|
81
|
+
api_key="github_pat_xxxxxxxxx",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
response = client.chat.completions.create(
|
|
85
|
+
messages=[
|
|
86
|
+
{"role": "system", "content": "你是一個有用的助手。"},
|
|
87
|
+
{"role": "user", "content": "請解釋什麼是量子糾纏。"},
|
|
88
|
+
],
|
|
89
|
+
model="gpt-4o",
|
|
90
|
+
temperature=1,
|
|
91
|
+
max_tokens=4096,
|
|
92
|
+
top_p=1,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
print(response.choices[0].message.content)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. 使用 CLI 命令取得 Token(推薦)
|
|
99
|
+
|
|
100
|
+
最簡單的方式是使用命令列工具:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
copilot-python login
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
這會開啟瀏覽器進行 GitHub 授權,並輸出 access token。
|
|
107
|
+
|
|
108
|
+
### 3. 程式碼中使用 OAuth Device Flow
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from openai import OpenAI
|
|
112
|
+
from copilot_python import login_device_flow
|
|
113
|
+
|
|
114
|
+
# 使用內建 client_id,無需額外提供
|
|
115
|
+
token = login_device_flow()
|
|
116
|
+
|
|
117
|
+
client = OpenAI(
|
|
118
|
+
base_url="https://models.github.ai/inference",
|
|
119
|
+
api_key=token,
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 4. 使用 `CopilotClient`
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from copilot_python import CopilotClient
|
|
127
|
+
|
|
128
|
+
client = CopilotClient(api_key="github_pat_xxxxxxxxx")
|
|
129
|
+
|
|
130
|
+
reply = client.ask(
|
|
131
|
+
"如何寫一個 HTTP 伺服器?",
|
|
132
|
+
model="gpt-4o",
|
|
133
|
+
system_prompt="你是一個有用的助手。",
|
|
134
|
+
)
|
|
135
|
+
print(reply.choices[0].message.content)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 建議 Token 類型
|
|
139
|
+
|
|
140
|
+
常見情境:
|
|
141
|
+
|
|
142
|
+
- OAuth token:`gho_...`
|
|
143
|
+
- Fine-grained PAT:`github_pat_...`
|
|
144
|
+
- GitHub App user-to-server token:`ghu_...`
|
|
145
|
+
- 視服務而定,可能需要 `models` 或 `Copilot Requests` 權限
|
|
146
|
+
|
|
147
|
+
## 命令列工具
|
|
148
|
+
|
|
149
|
+
### 取得 Token
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
copilot-python login
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
這會:
|
|
156
|
+
1. 顯示 GitHub 裝置授權連結
|
|
157
|
+
2. 自動開啟瀏覽器(可用 `--no-open-browser` 停用)
|
|
158
|
+
3. 等待授權完成
|
|
159
|
+
4. 輸出 access token 到 stdout
|
|
160
|
+
|
|
161
|
+
完整選項:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
copilot-python login [--client-id CLIENT_ID] [--scope SCOPE] [--no-open-browser]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 主要 API
|
|
168
|
+
|
|
169
|
+
### `login_device_flow()`
|
|
170
|
+
|
|
171
|
+
程式碼中走 GitHub OAuth Device Flow,回傳 access token。
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from copilot_python import login_device_flow
|
|
175
|
+
|
|
176
|
+
# 使用內建 client_id
|
|
177
|
+
token = login_device_flow()
|
|
178
|
+
|
|
179
|
+
# 或自訂 client_id 和 scope
|
|
180
|
+
token = login_device_flow(
|
|
181
|
+
client_id="你的 client_id",
|
|
182
|
+
scope="repo user",
|
|
183
|
+
)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### `create_client()`
|
|
187
|
+
|
|
188
|
+
建立已填入 `base_url` 與 `api_key` 的官方 `OpenAI` client。
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from copilot_python import create_client
|
|
192
|
+
|
|
193
|
+
client = create_client(base_url="https://models.github.ai/inference", api_key="...")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `ask_copilot()`
|
|
197
|
+
|
|
198
|
+
最方便的單次呼叫入口,回傳官方 SDK 的 completion 物件。
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
ask_copilot(
|
|
202
|
+
prompt,
|
|
203
|
+
*,
|
|
204
|
+
model,
|
|
205
|
+
api_key=None,
|
|
206
|
+
token=None,
|
|
207
|
+
base_url="https://models.github.ai/inference",
|
|
208
|
+
system_prompt=None,
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `CopilotClient.ask()`
|
|
213
|
+
|
|
214
|
+
適合重複呼叫時使用。
|
|
215
|
+
|
|
216
|
+
## 例外
|
|
217
|
+
|
|
218
|
+
- `CopilotError`
|
|
219
|
+
- `CopilotAuthenticationError`
|
|
220
|
+
- `CopilotHTTPError`
|
|
221
|
+
- `CopilotTimeoutError`
|
|
222
|
+
|
|
223
|
+
## 注意事項
|
|
224
|
+
|
|
225
|
+
1. 這個專案現在是 **基於官方 `openai` SDK** 呼叫 OpenAI 相容聊天 API。
|
|
226
|
+
2. 已內建 GitHub OAuth App client_id,可直接使用 `copilot-python login` 或 `login_device_flow()`。
|
|
227
|
+
3. `base_url` 可指向 GitHub Models 或其他 OpenAI 相容端點。
|
|
228
|
+
4. 若手動提供 PAT,請確認 Token 權限符合目標服務需求。
|
|
229
|
+
5. OAuth Device Flow 的 `scope` 參數取決於你使用的 GitHub OAuth App,內建的 client_id 支援標準 OAuth scopes(如 `repo`, `user` 等)。
|
|
230
|
+
|
|
231
|
+
## 相容性
|
|
232
|
+
|
|
233
|
+
舊命令 `copilot-wrapper login` 和舊導入 `from copilot_wrapper import ...` 仍保留向後相容。
|
|
234
|
+
|
|
235
|
+
## 測試
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
python -m unittest discover -s tests -v
|
|
239
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/copilot_python/__init__.py
|
|
5
|
+
src/copilot_python/__main__.py
|
|
6
|
+
src/copilot_python/cli.py
|
|
7
|
+
src/copilot_python/client.py
|
|
8
|
+
src/copilot_python.egg-info/PKG-INFO
|
|
9
|
+
src/copilot_python.egg-info/SOURCES.txt
|
|
10
|
+
src/copilot_python.egg-info/dependency_links.txt
|
|
11
|
+
src/copilot_python.egg-info/entry_points.txt
|
|
12
|
+
src/copilot_python.egg-info/requires.txt
|
|
13
|
+
src/copilot_python.egg-info/top_level.txt
|
|
14
|
+
src/copilot_wrapper/__init__.py
|
|
15
|
+
src/copilot_wrapper/__main__.py
|
|
16
|
+
src/copilot_wrapper/cli.py
|
|
17
|
+
src/copilot_wrapper/client.py
|
|
18
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openai>=1.30.0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .client import (
|
|
2
|
+
CopilotClient,
|
|
3
|
+
CopilotError,
|
|
4
|
+
CopilotAuthenticationError,
|
|
5
|
+
DEFAULT_GITHUB_OAUTH_CLIENT_ID,
|
|
6
|
+
DeviceFlowInfo,
|
|
7
|
+
GitHubOAuthDeviceFlow,
|
|
8
|
+
OpenAI,
|
|
9
|
+
CopilotTimeoutError,
|
|
10
|
+
ask_copilot,
|
|
11
|
+
create_client,
|
|
12
|
+
login_device_flow,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"CopilotClient",
|
|
17
|
+
"CopilotError",
|
|
18
|
+
"CopilotAuthenticationError",
|
|
19
|
+
"DEFAULT_GITHUB_OAUTH_CLIENT_ID",
|
|
20
|
+
"DeviceFlowInfo",
|
|
21
|
+
"GitHubOAuthDeviceFlow",
|
|
22
|
+
"OpenAI",
|
|
23
|
+
"CopilotTimeoutError",
|
|
24
|
+
"ask_copilot",
|
|
25
|
+
"create_client",
|
|
26
|
+
"login_device_flow",
|
|
27
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from .client import DEFAULT_GITHUB_OAUTH_CLIENT_ID, GitHubOAuthDeviceFlow
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
11
|
+
parser = argparse.ArgumentParser()
|
|
12
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
13
|
+
|
|
14
|
+
login_parser = subparsers.add_parser("login", help="Run GitHub OAuth Device Flow and print the token.")
|
|
15
|
+
login_parser.add_argument(
|
|
16
|
+
"--client-id",
|
|
17
|
+
default=os.environ.get("GITHUB_OAUTH_CLIENT_ID") or DEFAULT_GITHUB_OAUTH_CLIENT_ID,
|
|
18
|
+
help="GitHub OAuth App client ID. Defaults to the built-in public client ID, or can be overridden via GITHUB_OAUTH_CLIENT_ID.",
|
|
19
|
+
)
|
|
20
|
+
login_parser.add_argument(
|
|
21
|
+
"--scope",
|
|
22
|
+
default=None,
|
|
23
|
+
help="Optional GitHub OAuth scope, e.g. read:user.",
|
|
24
|
+
)
|
|
25
|
+
login_parser.add_argument(
|
|
26
|
+
"--timeout",
|
|
27
|
+
type=float,
|
|
28
|
+
default=30.0,
|
|
29
|
+
help="HTTP request timeout in seconds.",
|
|
30
|
+
)
|
|
31
|
+
login_parser.add_argument(
|
|
32
|
+
"--no-open-browser",
|
|
33
|
+
action="store_true",
|
|
34
|
+
help="Do not open the browser automatically.",
|
|
35
|
+
)
|
|
36
|
+
return parser
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main(argv: list[str] | None = None) -> int:
|
|
40
|
+
parser = build_parser()
|
|
41
|
+
args = parser.parse_args(argv)
|
|
42
|
+
|
|
43
|
+
if args.command == "login":
|
|
44
|
+
return _run_login(args)
|
|
45
|
+
|
|
46
|
+
parser.error("Unknown command.")
|
|
47
|
+
return 2
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _run_login(args: argparse.Namespace) -> int:
|
|
51
|
+
flow = GitHubOAuthDeviceFlow(
|
|
52
|
+
client_id=args.client_id,
|
|
53
|
+
scope=args.scope,
|
|
54
|
+
timeout=args.timeout,
|
|
55
|
+
)
|
|
56
|
+
device_flow = flow.start()
|
|
57
|
+
|
|
58
|
+
print(f"Verification URL: {device_flow.verification_uri}", file=sys.stderr)
|
|
59
|
+
print(f"User Code: {device_flow.user_code}", file=sys.stderr)
|
|
60
|
+
print("完成授權後,token 會輸出到 stdout。", file=sys.stderr)
|
|
61
|
+
|
|
62
|
+
if not args.no_open_browser:
|
|
63
|
+
import webbrowser
|
|
64
|
+
|
|
65
|
+
webbrowser.open(device_flow.verification_uri)
|
|
66
|
+
|
|
67
|
+
token = flow.poll(device_flow)
|
|
68
|
+
print(token)
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.parse
|
|
8
|
+
import urllib.request
|
|
9
|
+
import webbrowser
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Mapping, Optional
|
|
12
|
+
|
|
13
|
+
from openai import OpenAI as _OpenAI
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DEFAULT_BASE_URL = "https://models.github.ai/inference"
|
|
17
|
+
DEFAULT_GITHUB_OAUTH_CLIENT_ID = "Ov23liZUzHJhxMU267Fs"
|
|
18
|
+
DEVICE_CODE_URL = "https://github.com/login/device/code"
|
|
19
|
+
ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
|
|
20
|
+
DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
|
|
21
|
+
|
|
22
|
+
OpenAI = _OpenAI
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CopilotError(RuntimeError):
|
|
26
|
+
"""Base exception for this package."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CopilotAuthenticationError(CopilotError):
|
|
30
|
+
"""Raised when authentication is missing or rejected."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CopilotTimeoutError(CopilotError):
|
|
34
|
+
"""Raised when a network request times out."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class DeviceFlowInfo:
|
|
39
|
+
device_code: str
|
|
40
|
+
user_code: str
|
|
41
|
+
verification_uri: str
|
|
42
|
+
expires_in: int
|
|
43
|
+
interval: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_client(
|
|
47
|
+
*,
|
|
48
|
+
api_key: Optional[str] = None,
|
|
49
|
+
token: Optional[str] = None,
|
|
50
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
51
|
+
timeout: float = 180.0,
|
|
52
|
+
**kwargs: Any,
|
|
53
|
+
) -> OpenAI:
|
|
54
|
+
resolved_api_key = api_key or token or _get_token_from_environment()
|
|
55
|
+
if not resolved_api_key:
|
|
56
|
+
raise CopilotAuthenticationError(
|
|
57
|
+
"未提供 `api_key`。請手動傳入,或設定 `COPILOT_GITHUB_TOKEN`、`GITHUB_TOKEN`、`GH_TOKEN`。"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return OpenAI(
|
|
61
|
+
base_url=base_url,
|
|
62
|
+
api_key=resolved_api_key,
|
|
63
|
+
timeout=timeout,
|
|
64
|
+
**kwargs,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class CopilotClient:
|
|
69
|
+
"""Thin wrapper around the official `openai.OpenAI` client."""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
api_key: Optional[str] = None,
|
|
75
|
+
token: Optional[str] = None,
|
|
76
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
77
|
+
timeout: float = 180.0,
|
|
78
|
+
**kwargs: Any,
|
|
79
|
+
) -> None:
|
|
80
|
+
self.client = create_client(
|
|
81
|
+
api_key=api_key,
|
|
82
|
+
token=token,
|
|
83
|
+
base_url=base_url,
|
|
84
|
+
timeout=timeout,
|
|
85
|
+
**kwargs,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def chat(self) -> Any:
|
|
90
|
+
return self.client.chat
|
|
91
|
+
|
|
92
|
+
def ask(
|
|
93
|
+
self,
|
|
94
|
+
prompt: str,
|
|
95
|
+
*,
|
|
96
|
+
model: str,
|
|
97
|
+
system_prompt: Optional[str] = None,
|
|
98
|
+
temperature: Optional[float] = None,
|
|
99
|
+
max_tokens: Optional[int] = None,
|
|
100
|
+
top_p: Optional[float] = None,
|
|
101
|
+
**extra_body: Any,
|
|
102
|
+
) -> Any:
|
|
103
|
+
if not isinstance(prompt, str) or not prompt.strip():
|
|
104
|
+
raise ValueError("`prompt` 必須是非空字串。")
|
|
105
|
+
|
|
106
|
+
messages = []
|
|
107
|
+
if system_prompt:
|
|
108
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
109
|
+
messages.append({"role": "user", "content": prompt})
|
|
110
|
+
|
|
111
|
+
return self.client.chat.completions.create(
|
|
112
|
+
messages=messages,
|
|
113
|
+
model=model,
|
|
114
|
+
temperature=temperature,
|
|
115
|
+
max_tokens=max_tokens,
|
|
116
|
+
top_p=top_p,
|
|
117
|
+
**extra_body,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class GitHubOAuthDeviceFlow:
|
|
122
|
+
"""Implements GitHub OAuth Device Flow using only the standard library."""
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
*,
|
|
127
|
+
client_id: str = DEFAULT_GITHUB_OAUTH_CLIENT_ID,
|
|
128
|
+
scope: Optional[str] = None,
|
|
129
|
+
timeout: float = 30.0,
|
|
130
|
+
) -> None:
|
|
131
|
+
if not client_id:
|
|
132
|
+
raise ValueError("`client_id` 不能為空。")
|
|
133
|
+
self.client_id = client_id
|
|
134
|
+
self.scope = scope
|
|
135
|
+
self.timeout = timeout
|
|
136
|
+
|
|
137
|
+
def start(self) -> DeviceFlowInfo:
|
|
138
|
+
payload = {"client_id": self.client_id}
|
|
139
|
+
if self.scope:
|
|
140
|
+
payload["scope"] = self.scope
|
|
141
|
+
|
|
142
|
+
request = urllib.request.Request(
|
|
143
|
+
DEVICE_CODE_URL,
|
|
144
|
+
data=urllib.parse.urlencode(payload).encode("utf-8"),
|
|
145
|
+
headers={
|
|
146
|
+
"Accept": "application/json",
|
|
147
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
148
|
+
},
|
|
149
|
+
method="POST",
|
|
150
|
+
)
|
|
151
|
+
data = _request_json(request, timeout=self.timeout)
|
|
152
|
+
return DeviceFlowInfo(
|
|
153
|
+
device_code=str(data["device_code"]),
|
|
154
|
+
user_code=str(data["user_code"]),
|
|
155
|
+
verification_uri=str(data["verification_uri"]),
|
|
156
|
+
expires_in=int(data["expires_in"]),
|
|
157
|
+
interval=int(data.get("interval", 5)),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def poll(self, device_flow: DeviceFlowInfo) -> str:
|
|
161
|
+
deadline = time.time() + device_flow.expires_in
|
|
162
|
+
interval = device_flow.interval
|
|
163
|
+
|
|
164
|
+
while time.time() < deadline:
|
|
165
|
+
payload = {
|
|
166
|
+
"client_id": self.client_id,
|
|
167
|
+
"device_code": device_flow.device_code,
|
|
168
|
+
"grant_type": DEVICE_GRANT_TYPE,
|
|
169
|
+
}
|
|
170
|
+
request = urllib.request.Request(
|
|
171
|
+
ACCESS_TOKEN_URL,
|
|
172
|
+
data=urllib.parse.urlencode(payload).encode("utf-8"),
|
|
173
|
+
headers={
|
|
174
|
+
"Accept": "application/json",
|
|
175
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
176
|
+
},
|
|
177
|
+
method="POST",
|
|
178
|
+
)
|
|
179
|
+
response = _request_json(request, timeout=self.timeout)
|
|
180
|
+
|
|
181
|
+
access_token = response.get("access_token")
|
|
182
|
+
if access_token:
|
|
183
|
+
return str(access_token)
|
|
184
|
+
|
|
185
|
+
error = str(response.get("error", ""))
|
|
186
|
+
if error == "authorization_pending":
|
|
187
|
+
time.sleep(interval)
|
|
188
|
+
continue
|
|
189
|
+
if error == "slow_down":
|
|
190
|
+
interval = int(response.get("interval", interval + 5))
|
|
191
|
+
time.sleep(interval)
|
|
192
|
+
continue
|
|
193
|
+
if error in {"expired_token", "token_expired"}:
|
|
194
|
+
raise CopilotAuthenticationError("裝置授權碼已過期,請重新發起 OAuth Device Flow。")
|
|
195
|
+
if error == "access_denied":
|
|
196
|
+
raise CopilotAuthenticationError("使用者已拒絕授權。")
|
|
197
|
+
if error:
|
|
198
|
+
raise CopilotAuthenticationError(f"OAuth Device Flow 失敗:{error}")
|
|
199
|
+
|
|
200
|
+
time.sleep(interval)
|
|
201
|
+
|
|
202
|
+
raise CopilotTimeoutError("等待 OAuth Device Flow 授權逾時。")
|
|
203
|
+
|
|
204
|
+
def authorize(self, *, open_browser: bool = True, print_instructions: bool = True) -> str:
|
|
205
|
+
device_flow = self.start()
|
|
206
|
+
if print_instructions:
|
|
207
|
+
print(
|
|
208
|
+
f"請前往 {device_flow.verification_uri} 並輸入驗證碼 {device_flow.user_code} 完成授權。"
|
|
209
|
+
)
|
|
210
|
+
if open_browser:
|
|
211
|
+
webbrowser.open(device_flow.verification_uri)
|
|
212
|
+
return self.poll(device_flow)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def login_device_flow(
|
|
216
|
+
*,
|
|
217
|
+
client_id: str = DEFAULT_GITHUB_OAUTH_CLIENT_ID,
|
|
218
|
+
scope: Optional[str] = None,
|
|
219
|
+
timeout: float = 30.0,
|
|
220
|
+
open_browser: bool = True,
|
|
221
|
+
print_instructions: bool = True,
|
|
222
|
+
) -> str:
|
|
223
|
+
flow = GitHubOAuthDeviceFlow(client_id=client_id, scope=scope, timeout=timeout)
|
|
224
|
+
return flow.authorize(open_browser=open_browser, print_instructions=print_instructions)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def ask_copilot(
|
|
228
|
+
prompt: str,
|
|
229
|
+
*,
|
|
230
|
+
model: str,
|
|
231
|
+
api_key: Optional[str] = None,
|
|
232
|
+
token: Optional[str] = None,
|
|
233
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
234
|
+
system_prompt: Optional[str] = None,
|
|
235
|
+
temperature: Optional[float] = None,
|
|
236
|
+
max_tokens: Optional[int] = None,
|
|
237
|
+
top_p: Optional[float] = None,
|
|
238
|
+
timeout: float = 180.0,
|
|
239
|
+
default_headers: Optional[Mapping[str, str]] = None,
|
|
240
|
+
**extra_body: Any,
|
|
241
|
+
) -> Any:
|
|
242
|
+
client = CopilotClient(
|
|
243
|
+
api_key=api_key or token,
|
|
244
|
+
base_url=base_url,
|
|
245
|
+
timeout=timeout,
|
|
246
|
+
default_headers=default_headers,
|
|
247
|
+
**extra_body.pop("client_options", {}),
|
|
248
|
+
)
|
|
249
|
+
return client.ask(
|
|
250
|
+
prompt,
|
|
251
|
+
model=model,
|
|
252
|
+
system_prompt=system_prompt,
|
|
253
|
+
temperature=temperature,
|
|
254
|
+
max_tokens=max_tokens,
|
|
255
|
+
top_p=top_p,
|
|
256
|
+
**extra_body,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _get_token_from_environment() -> Optional[str]:
|
|
261
|
+
for name in ("COPILOT_GITHUB_TOKEN", "GITHUB_TOKEN", "GH_TOKEN"):
|
|
262
|
+
value = os.environ.get(name)
|
|
263
|
+
if value:
|
|
264
|
+
return value
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _request_json(request: urllib.request.Request, *, timeout: float) -> dict[str, Any]:
|
|
269
|
+
try:
|
|
270
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
271
|
+
raw = response.read().decode("utf-8")
|
|
272
|
+
except TimeoutError as exc:
|
|
273
|
+
raise CopilotTimeoutError("網路請求逾時。") from exc
|
|
274
|
+
except urllib.error.HTTPError as exc:
|
|
275
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
276
|
+
payload = _safe_load_json(body)
|
|
277
|
+
message = _extract_error_message(payload) or body or f"HTTP {exc.code}"
|
|
278
|
+
raise CopilotAuthenticationError(message) from exc
|
|
279
|
+
except urllib.error.URLError as exc:
|
|
280
|
+
raise CopilotError(f"網路連線失敗:{exc.reason}") from exc
|
|
281
|
+
|
|
282
|
+
return _safe_load_json(raw)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _safe_load_json(raw: str) -> dict[str, Any]:
|
|
286
|
+
if not raw.strip():
|
|
287
|
+
return {}
|
|
288
|
+
try:
|
|
289
|
+
data = json.loads(raw)
|
|
290
|
+
except json.JSONDecodeError as exc:
|
|
291
|
+
raise CopilotError("伺服器回應不是有效的 JSON。") from exc
|
|
292
|
+
if isinstance(data, dict):
|
|
293
|
+
return data
|
|
294
|
+
raise CopilotError("伺服器回應不是有效的 JSON 物件。")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _extract_error_message(payload: dict[str, Any]) -> str:
|
|
298
|
+
error = payload.get("error")
|
|
299
|
+
if isinstance(error, dict):
|
|
300
|
+
message = error.get("message")
|
|
301
|
+
if isinstance(message, str):
|
|
302
|
+
return message
|
|
303
|
+
if isinstance(error, str):
|
|
304
|
+
return error
|
|
305
|
+
message = payload.get("message")
|
|
306
|
+
if isinstance(message, str):
|
|
307
|
+
return message
|
|
308
|
+
error_description = payload.get("error_description")
|
|
309
|
+
if isinstance(error_description, str):
|
|
310
|
+
return error_description
|
|
311
|
+
return ""
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest import mock
|
|
3
|
+
|
|
4
|
+
from copilot_python import (
|
|
5
|
+
CopilotClient,
|
|
6
|
+
GitHubOAuthDeviceFlow,
|
|
7
|
+
ask_copilot,
|
|
8
|
+
create_client,
|
|
9
|
+
)
|
|
10
|
+
from copilot_python.cli import main
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _FakeHTTPResponse:
|
|
14
|
+
def __init__(self, payload):
|
|
15
|
+
self.payload = payload
|
|
16
|
+
|
|
17
|
+
def read(self):
|
|
18
|
+
return self.payload.encode("utf-8")
|
|
19
|
+
|
|
20
|
+
def __enter__(self):
|
|
21
|
+
return self
|
|
22
|
+
|
|
23
|
+
def __exit__(self, exc_type, exc, tb):
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CopilotClientTests(unittest.TestCase):
|
|
28
|
+
@mock.patch("copilot_wrapper.client.OpenAI")
|
|
29
|
+
def test_create_client(self, mock_openai):
|
|
30
|
+
instance = mock.Mock()
|
|
31
|
+
mock_openai.return_value = instance
|
|
32
|
+
|
|
33
|
+
client = create_client(api_key="token")
|
|
34
|
+
|
|
35
|
+
self.assertIs(client, instance)
|
|
36
|
+
mock_openai.assert_called_once()
|
|
37
|
+
|
|
38
|
+
@mock.patch("copilot_wrapper.client.OpenAI")
|
|
39
|
+
def test_ask_copilot_uses_helper_api(self, mock_openai):
|
|
40
|
+
completion = mock.Mock()
|
|
41
|
+
completion.choices = [mock.Mock(message=mock.Mock(content="done"))]
|
|
42
|
+
instance = mock.Mock()
|
|
43
|
+
instance.chat.completions.create.return_value = completion
|
|
44
|
+
mock_openai.return_value = instance
|
|
45
|
+
|
|
46
|
+
response = ask_copilot("hello", model="gpt-4o", api_key="token")
|
|
47
|
+
|
|
48
|
+
self.assertEqual(response.choices[0].message.content, "done")
|
|
49
|
+
instance.chat.completions.create.assert_called_once()
|
|
50
|
+
|
|
51
|
+
@mock.patch("copilot_wrapper.client.OpenAI")
|
|
52
|
+
def test_copilot_client_ask_adds_system_message(self, mock_openai):
|
|
53
|
+
completion = mock.Mock()
|
|
54
|
+
completion.choices = [mock.Mock(message=mock.Mock(content="ok"))]
|
|
55
|
+
instance = mock.Mock()
|
|
56
|
+
instance.chat.completions.create.return_value = completion
|
|
57
|
+
mock_openai.return_value = instance
|
|
58
|
+
|
|
59
|
+
client = CopilotClient(api_key="token")
|
|
60
|
+
response = client.ask("hello", model="gpt-4o", system_prompt="sys")
|
|
61
|
+
|
|
62
|
+
self.assertEqual(response.choices[0].message.content, "ok")
|
|
63
|
+
call_kwargs = instance.chat.completions.create.call_args.kwargs
|
|
64
|
+
self.assertEqual(call_kwargs["messages"][0]["role"], "system")
|
|
65
|
+
|
|
66
|
+
@mock.patch("copilot_wrapper.client.urllib.request.urlopen")
|
|
67
|
+
def test_device_flow_start(self, mock_urlopen):
|
|
68
|
+
mock_urlopen.return_value = _FakeHTTPResponse(
|
|
69
|
+
'{"device_code":"dev","user_code":"ABCD-EFGH","verification_uri":"https://github.com/login/device","expires_in":900,"interval":5}'
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
flow = GitHubOAuthDeviceFlow(client_id="client-id", scope="models")
|
|
73
|
+
info = flow.start()
|
|
74
|
+
self.assertEqual(info.user_code, "ABCD-EFGH")
|
|
75
|
+
|
|
76
|
+
@mock.patch("copilot_wrapper.client.time.sleep")
|
|
77
|
+
@mock.patch("copilot_wrapper.client.urllib.request.urlopen")
|
|
78
|
+
def test_device_flow_poll(self, mock_urlopen, _sleep):
|
|
79
|
+
mock_urlopen.side_effect = [
|
|
80
|
+
_FakeHTTPResponse('{"error":"authorization_pending"}'),
|
|
81
|
+
_FakeHTTPResponse('{"access_token":"gho_token"}'),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
flow = GitHubOAuthDeviceFlow(client_id="client-id")
|
|
85
|
+
token = flow.poll(
|
|
86
|
+
mock.Mock(
|
|
87
|
+
device_code="dev",
|
|
88
|
+
user_code="code",
|
|
89
|
+
verification_uri="https://github.com/login/device",
|
|
90
|
+
expires_in=900,
|
|
91
|
+
interval=0,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
self.assertEqual(token, "gho_token")
|
|
95
|
+
|
|
96
|
+
@mock.patch("copilot_wrapper.cli.GitHubOAuthDeviceFlow")
|
|
97
|
+
@mock.patch("sys.stdout")
|
|
98
|
+
@mock.patch("sys.stderr")
|
|
99
|
+
def test_cli_login(self, _stderr, stdout, mock_flow_cls):
|
|
100
|
+
flow = mock.Mock()
|
|
101
|
+
flow.start.return_value = mock.Mock(
|
|
102
|
+
verification_uri="https://github.com/login/device",
|
|
103
|
+
user_code="ABCD-EFGH",
|
|
104
|
+
)
|
|
105
|
+
flow.poll.return_value = "gho_token"
|
|
106
|
+
mock_flow_cls.return_value = flow
|
|
107
|
+
|
|
108
|
+
exit_code = main(["login", "--client-id", "client-id", "--no-open-browser"])
|
|
109
|
+
|
|
110
|
+
self.assertEqual(exit_code, 0)
|
|
111
|
+
stdout.write.assert_any_call("gho_token")
|
|
112
|
+
|
|
113
|
+
@mock.patch("copilot_wrapper.cli.GitHubOAuthDeviceFlow")
|
|
114
|
+
@mock.patch("sys.stdout")
|
|
115
|
+
@mock.patch("sys.stderr")
|
|
116
|
+
def test_cli_login_uses_built_in_client_id(self, _stderr, _stdout, mock_flow_cls):
|
|
117
|
+
flow = mock.Mock()
|
|
118
|
+
flow.start.return_value = mock.Mock(
|
|
119
|
+
verification_uri="https://github.com/login/device",
|
|
120
|
+
user_code="ABCD-EFGH",
|
|
121
|
+
)
|
|
122
|
+
flow.poll.return_value = "gho_token"
|
|
123
|
+
mock_flow_cls.return_value = flow
|
|
124
|
+
|
|
125
|
+
exit_code = main(["login", "--no-open-browser"])
|
|
126
|
+
|
|
127
|
+
self.assertEqual(exit_code, 0)
|
|
128
|
+
mock_flow_cls.assert_called_once()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
unittest.main()
|