Coverage for aipyapp/gui/providers.py: 0%

475 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-11 12:02 +0200

1#! /usr/bin/env python3 

2# -*- coding: utf-8 -*- 

3 

4import requests 

5from typing import List 

6import time 

7import webbrowser 

8import threading 

9 

10import wx 

11import wx.adv 

12from loguru import logger 

13 

14from ..aipy.trustoken import TrustTokenAPI 

15from .. import T 

16 

17class InitialProviderPage(wx.adv.WizardPage): 

18 def __init__(self, parent, provider_config): 

19 super().__init__(parent) 

20 self.provider_config = provider_config 

21 self.init_ui() 

22 self.SetSize(800, 600) 

23 

24 def init_ui(self): 

25 vbox = wx.BoxSizer(wx.VERTICAL) 

26 

27 # 标题 

28 title = wx.StaticText(self, label=T('Select LLM Provider')) 

29 title.SetFont(wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)) 

30 vbox.Add(title, 0, wx.ALL | wx.ALIGN_CENTER, 10) 

31 

32 # Provider 选择 

33 provider_box = wx.StaticBox(self, label=T('Provider')) 

34 provider_sizer = wx.StaticBoxSizer(provider_box, wx.VERTICAL) 

35 

36 self.provider_choice = wx.Choice( 

37 self, 

38 choices=['Trustoken', T('Other')] 

39 ) 

40 self.provider_choice.SetSelection(0) # 默认选中 TrustToken 

41 self.provider_choice.Bind(wx.EVT_CHOICE, self.on_provider_selected) 

42 provider_sizer.Add(self.provider_choice, 0, wx.EXPAND | wx.ALL, 5) 

43 vbox.Add(provider_sizer, 0, wx.EXPAND | wx.ALL, 5) 

44 

45 # 提示信息 

46 self.hint = wx.StaticText(self, label="") 

47 # 设置文本自动换行 

48 self.hint.Wrap(400) 

49 vbox.Add(self.hint, 0, wx.ALL | wx.EXPAND, 10) 

50 

51 self.SetSizer(vbox) 

52 # 设置初始提示信息 

53 self.on_provider_selected(None) 

54 

55 def on_provider_selected(self, event): 

56 provider = self.provider_choice.GetStringSelection() 

57 if provider == 'Trustoken': 

58 self.hint.SetLabel(f"""{T('Trustoken is an intelligent API Key management service')} 

59 

601. {T('Auto get and manage API Key')} 

612. {T('Support multiple LLM providers')} 

623. {T('Auto select optimal model')} 

634. {T('Recommended for beginners, easy to configure and feature-rich')}""") 

64 else: 

65 self.hint.SetLabel(f"""{T('Select other providers requires manual configuration')} 

66 

671. {T('Need to apply for API Key yourself')} 

682. {T('Manually input API Key')} 

693. {T('Manually select model')}""") 

70 # 重新计算换行 

71 self.hint.Wrap(400) 

72 

73 def get_selection(self): 

74 # 获取当前选择,如果没有选择则返回默认值 

75 selection = self.provider_choice.GetStringSelection() 

76 if not selection: 

77 # 如果获取不到选择,使用 GetSelection() 获取索引 

78 index = self.provider_choice.GetSelection() 

79 if index >= 0: 

80 selection = self.provider_choice.GetString(index) 

81 else: 

82 selection = 'Trustoken' # 如果索引也无效,返回默认值 

83 return selection 

84 

85 def GetNext(self): 

86 selection = self.get_selection() 

87 if selection == "Trustoken": 

88 return self.GetParent().trust_token_page 

89 else: 

90 return self.GetParent().provider_page 

91 

92 def GetPrev(self): 

93 return None 

94 

95class TrustTokenPage(wx.adv.WizardPage): 

96 def __init__(self, parent, provider_config): 

97 super().__init__(parent) 

98 self.provider_config = provider_config 

99 self.api = TrustTokenAPI() 

100 self.poll_interval = 5 

101 self.request_id = None 

102 self.polling_thread = None 

103 self.stop_polling = False 

104 self.start_time = None 

105 self.polling_timeout = 310 # 5分钟10秒 

106 self.init_ui() 

107 self.SetSize(800, 600) 

108 # 绑定页面切换事件 

109 self.GetParent().Bind(wx.adv.EVT_WIZARD_PAGE_CHANGING, self.on_page_changing) 

110 

111 def init_ui(self): 

112 vbox = wx.BoxSizer(wx.VERTICAL) 

113 

114 # 标题 

115 title = wx.StaticText(self, label=T('Trustoken Configuration')) 

116 title.SetFont(wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)) 

117 vbox.Add(title, 0, wx.ALL | wx.ALIGN_CENTER, 10) 

118 

119 # API Key 显示 

120 api_key_box = wx.StaticBox(self, label="API Key") 

121 api_key_sizer = wx.StaticBoxSizer(api_key_box, wx.VERTICAL) 

122 

123 self.api_key_text = wx.TextCtrl( 

124 self, 

125 style=wx.TE_READONLY 

126 ) 

127 api_key_sizer.Add(self.api_key_text, 1, wx.EXPAND | wx.ALL, 5) 

128 vbox.Add(api_key_sizer, 0, wx.EXPAND | wx.ALL, 5) 

129 

130 # 获取按钮 

131 self.fetch_button = wx.Button(self, label=T('Auto open browser to get API Key')) 

132 self.fetch_button.Bind(wx.EVT_BUTTON, self.on_fetch) 

133 vbox.Add(self.fetch_button, 0, wx.ALL | wx.ALIGN_CENTER, 10) 

134 

135 # 状态信息 

136 self.status_text = wx.StaticText(self, label="") 

137 vbox.Add(self.status_text, 0, wx.ALL | wx.EXPAND, 5) 

138 

139 self.url_ctrl = wx.adv.HyperlinkCtrl(self, label=T('You can also click this link to open browser to get API Key'), style=wx.adv.HL_ALIGN_LEFT | wx.adv.HL_CONTEXTMENU) 

140 self.url_ctrl.Hide() 

141 vbox.Add(self.url_ctrl, 0, wx.ALL, 5) 

142 

143 vbox.AddSpacer(20) 

144 

145 # 剩余时间文本 

146 self.time_text = wx.StaticText(self, label='') 

147 vbox.Add(self.time_text, 0, wx.EXPAND | wx.ALL, 5) 

148 

149 # 进度条 

150 self.progress_bar = wx.Gauge(self, range=100) 

151 vbox.Add(self.progress_bar, 0, wx.EXPAND | wx.ALL, 5) 

152 

153 self._toggle_progress(False) 

154 self.SetSizer(vbox) 

155 

156 def _toggle_progress(self, show: bool): 

157 self.progress_bar.Show(show) 

158 self.time_text.Show(show) 

159 

160 def _update_progress(self): 

161 if not self.start_time: 

162 return 

163 

164 elapsed = time.time() - self.start_time 

165 if elapsed >= self.polling_timeout: 

166 progress = 100 

167 time_remaining = 0 

168 else: 

169 progress = int((elapsed / self.polling_timeout) * 100) 

170 time_remaining = int(self.polling_timeout - elapsed) 

171 

172 wx.CallAfter(self.progress_bar.SetValue, progress) 

173 wx.CallAfter(self.time_text.SetLabel, f"{T('Remaining time')}: {time_remaining}{T('seconds')}") 

174 

175 def _poll_status(self, save_func): 

176 self.start_time = time.time() 

177 while not self.stop_polling and time.time() - self.start_time < self.polling_timeout: 

178 self._update_progress() 

179 

180 data = self.api.check_status(self.request_id) 

181 if not data: 

182 time.sleep(self.poll_interval) 

183 continue 

184 

185 status = data.get('status') 

186 wx.CallAfter(self._update_status, f"{T('Current status')}: {status}") 

187 

188 if status == 'approved': 

189 if save_func: 

190 save_func(data['secret_token']) 

191 wx.CallAfter(self._on_success) 

192 return True 

193 elif status == 'expired': 

194 wx.CallAfter(self._update_status, T('Binding expired')) 

195 wx.CallAfter(self._on_failure) 

196 return False 

197 elif status == 'pending': 

198 pass 

199 else: 

200 wx.CallAfter(self._update_status, f"{T('Unknown status')}: {status}") 

201 wx.CallAfter(self._on_failure) 

202 return False 

203 

204 time.sleep(self.poll_interval) 

205 

206 if not self.stop_polling: 

207 wx.CallAfter(self._update_status, T('Waiting timeout')) 

208 wx.CallAfter(self._on_failure) 

209 return False 

210 

211 def _update_status(self, status): 

212 self.status_text.SetLabel(status) 

213 

214 def _on_success(self): 

215 self._toggle_progress(False) 

216 self.fetch_button.Enable() 

217 self.status_text.SetLabel(T("API Key obtained successfully!")) 

218 self.Layout() # 强制重新布局 

219 

220 def _on_failure(self): 

221 self._toggle_progress(False) 

222 self.fetch_button.Enable() 

223 self.status_text.SetLabel(T('API Key acquisition failed, please try again')) 

224 self.Layout() # 强制重新布局 

225 

226 def on_fetch(self, event): 

227 self.status_text.SetLabel(T('Requesting binding')) 

228 self.fetch_button.Disable() 

229 

230 data = self.api.request_binding() 

231 if not data: 

232 self._on_failure() 

233 return 

234 

235 approval_url = data['approval_url'] 

236 self.request_id = data['request_id'] 

237 expires_in = data['expires_in'] 

238 self.polling_timeout = expires_in 

239 self._update_status(T('Waiting for user confirmation')) 

240 

241 # 打开浏览器 

242 self.url_ctrl.SetURL(approval_url) 

243 self.url_ctrl.Show() 

244 webbrowser.open(approval_url) 

245 

246 # 显示进度组并重新布局 

247 self._toggle_progress(True) 

248 self.Layout() 

249 

250 # 开始轮询 

251 self.polling_thread = threading.Thread( 

252 target=self._poll_status, 

253 args=(self._save_token,) 

254 ) 

255 self.polling_thread.daemon = True 

256 self.polling_thread.start() 

257 

258 def _save_token(self, token): 

259 self.api_key_text.SetValue(token) 

260 

261 def get_api_key(self): 

262 return self.api_key_text.GetValue() 

263 

264 def on_page_changing(self, event): 

265 # 如果是当前页面,且是向前切换 

266 if event.GetPage() == self and event.GetDirection(): 

267 api_key = self.get_api_key() 

268 if not api_key: 

269 wx.MessageBox(T('Please get API Key first'), T('Hint'), wx.OK | wx.ICON_INFORMATION) 

270 event.Veto() 

271 return 

272 event.Skip() 

273 

274 def GetNext(self): 

275 return self.GetParent().model_page 

276 

277 def GetPrev(self): 

278 return self.GetParent().initial_page 

279 

280class ProviderPage(wx.adv.WizardPage): 

281 def __init__(self, parent, provider_config): 

282 super().__init__(parent) 

283 self.provider_config = provider_config 

284 self.init_ui() 

285 self.SetSize(800, 600) 

286 self.GetParent().Bind(wx.adv.EVT_WIZARD_PAGE_CHANGING, self.on_page_changing) 

287 

288 def init_ui(self): 

289 vbox = wx.BoxSizer(wx.VERTICAL) 

290 

291 # 标题 

292 title = wx.StaticText(self, label=T('Select LLM Provider')) 

293 title.SetFont(wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)) 

294 vbox.Add(title, 0, wx.ALL | wx.ALIGN_CENTER, 10) 

295 

296 # Provider 选择 

297 provider_box = wx.StaticBox(self, label=T('Provider')) 

298 provider_sizer = wx.StaticBoxSizer(provider_box, wx.VERTICAL) 

299 

300 # 过滤掉 TrustToken 

301 providers = [name for name in self.provider_config.providers.keys() if name != "Trustoken"] 

302 self.provider_choice = wx.Choice( 

303 self, 

304 choices=providers 

305 ) 

306 if providers: # 确保列表不为空 

307 self.provider_choice.SetSelection(0) # 默认选中第一个选项 

308 self.provider_choice.Bind(wx.EVT_CHOICE, self.on_provider_selected) 

309 provider_sizer.Add(self.provider_choice, 0, wx.EXPAND | wx.ALL, 5) 

310 vbox.Add(provider_sizer, 0, wx.EXPAND | wx.ALL, 5) 

311 

312 # API Key 输入 

313 api_key_box = wx.StaticBox(self, label="API Key") 

314 api_key_sizer = wx.StaticBoxSizer(api_key_box, wx.VERTICAL) 

315 

316 self.api_key_text = wx.TextCtrl( 

317 self, 

318 size=(400, -1) 

319 ) 

320 api_key_sizer.Add(self.api_key_text, 0, wx.EXPAND | wx.ALL, 5) 

321 vbox.Add(api_key_sizer, 0, wx.EXPAND | wx.ALL, 5) 

322 

323 # 提示信息 

324 hint = wx.StaticText(self, label=T('Please select provider and input API Key, click Next to verify')) 

325 hint.SetForegroundColour(wx.Colour(100, 100, 100)) 

326 vbox.Add(hint, 0, wx.ALL, 10) 

327 

328 self.SetSizer(vbox) 

329 

330 def on_provider_selected(self, event): 

331 provider = self.provider_choice.GetStringSelection() 

332 if provider in self.provider_config.config: 

333 config = self.provider_config.config[provider] 

334 self.api_key_text.SetValue(config["api_key"]) 

335 

336 def get_provider(self): 

337 return self.provider_choice.GetStringSelection() 

338 

339 def get_api_key(self): 

340 return self.api_key_text.GetValue() 

341 

342 def GetNext(self): 

343 return self.GetParent().model_page 

344 

345 def GetPrev(self): 

346 return self.GetParent().initial_page 

347 

348 def on_page_changing(self, event): 

349 # 如果是当前页面,且是向前切换 

350 if event.GetPage() == self and event.GetDirection(): 

351 api_key = self.get_api_key() 

352 if not api_key: 

353 wx.MessageBox(T('Please get API Key first'), T('Hint'), wx.OK | wx.ICON_INFORMATION) 

354 event.Veto() 

355 return 

356 event.Skip() 

357 

358class ModelPage(wx.adv.WizardPage): 

359 def __init__(self, parent, provider_config): 

360 super().__init__(parent) 

361 self.provider_config = provider_config 

362 self.prev_page = None # 添加一个变量来记录上一页 

363 self.init_ui() 

364 self.SetSize(800, 600) 

365 

366 def init_ui(self): 

367 vbox = wx.BoxSizer(wx.VERTICAL) 

368 

369 # 标题 

370 title = wx.StaticText(self, label=T('Select Model')) 

371 title.SetFont(wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)) 

372 vbox.Add(title, 0, wx.ALL | wx.ALIGN_CENTER, 10) 

373 

374 # Model 选择 

375 model_box = wx.StaticBox(self, label=T('Available Models')) 

376 model_sizer = wx.StaticBoxSizer(model_box, wx.VERTICAL) 

377 

378 self.model_choice = wx.Choice(self) 

379 model_sizer.Add(self.model_choice, 0, wx.EXPAND | wx.ALL, 5) 

380 vbox.Add(model_sizer, 0, wx.EXPAND | wx.ALL, 5) 

381 

382 # Max Tokens 配置 

383 max_tokens_box = wx.StaticBox(self, label=T('Max Tokens')) 

384 max_tokens_sizer = wx.StaticBoxSizer(max_tokens_box, wx.VERTICAL) 

385 

386 self.max_tokens_text = wx.TextCtrl(self, value="8192") 

387 max_tokens_sizer.Add(self.max_tokens_text, 0, wx.EXPAND | wx.ALL, 5) 

388 vbox.Add(max_tokens_sizer, 0, wx.EXPAND | wx.ALL, 5) 

389 

390 # 提示信息 

391 self.hint = wx.StaticText(self, label=T('Please select model to use and configure parameters')) 

392 self.hint.SetForegroundColour(wx.Colour(100, 100, 100)) 

393 vbox.Add(self.hint, 0, wx.ALL, 10) 

394 

395 self.SetSizer(vbox) 

396 

397 def set_models(self, models: List[str], selected_model: str = None, is_trust_token: bool = False): 

398 if is_trust_token: 

399 self.model_choice.SetItems(["auto"]) 

400 self.model_choice.SetSelection(0) 

401 self.model_choice.Disable() # 禁用选择 

402 self.hint.SetLabel(T('Trustoken uses automatic model selection')) 

403 else: 

404 self.model_choice.SetItems(models) 

405 self.model_choice.Enable() # 启用选择 

406 if selected_model and selected_model in models: 

407 self.model_choice.SetStringSelection(selected_model) 

408 elif models: # 如果有模型列表但没有指定选择的模型,默认选择第一个 

409 self.model_choice.SetSelection(0) 

410 self.hint.SetLabel(T('Please select model to use and configure parameters')) 

411 

412 def get_selected_model(self): 

413 return self.model_choice.GetStringSelection() 

414 

415 def get_max_tokens(self): 

416 try: 

417 return int(self.max_tokens_text.GetValue()) 

418 except ValueError: 

419 return 8192 

420 

421 def GetNext(self): 

422 return None 

423 

424 def GetPrev(self): 

425 return self.prev_page 

426 

427class OAuthPage(wx.adv.WizardPage): 

428 def __init__(self, parent, provider_config): 

429 super().__init__(parent) 

430 self.provider_config = provider_config 

431 self.prev_page = None 

432 self.init_ui() 

433 

434 def init_ui(self): 

435 vbox = wx.BoxSizer(wx.VERTICAL) 

436 

437 # 标题 

438 title = wx.StaticText(self, label=T('OAuth Gateway Configuration')) 

439 title.SetFont(wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)) 

440 vbox.Add(title, 0, wx.ALL | wx.ALIGN_CENTER, 10) 

441 

442 # Client ID 

443 client_id_box = wx.BoxSizer(wx.HORIZONTAL) 

444 client_id_label = wx.StaticText(self, label=T('Client ID:')) 

445 self.client_id_input = wx.TextCtrl(self) 

446 client_id_box.Add(client_id_label, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) 

447 client_id_box.Add(self.client_id_input, 1, wx.ALL | wx.EXPAND, 5) 

448 vbox.Add(client_id_box, 0, wx.EXPAND | wx.ALL, 5) 

449 

450 # Client Secret 

451 client_secret_box = wx.BoxSizer(wx.HORIZONTAL) 

452 client_secret_label = wx.StaticText(self, label=T('Client Secret:')) 

453 self.client_secret_input = wx.TextCtrl(self, style=wx.TE_PASSWORD) 

454 client_secret_box.Add(client_secret_label, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) 

455 client_secret_box.Add(self.client_secret_input, 1, wx.ALL | wx.EXPAND, 5) 

456 vbox.Add(client_secret_box, 0, wx.EXPAND | wx.ALL, 5) 

457 

458 # Token URL 

459 token_url_box = wx.BoxSizer(wx.HORIZONTAL) 

460 token_url_label = wx.StaticText(self, label=T('Token URL:')) 

461 self.token_url_input = wx.TextCtrl(self) 

462 token_url_box.Add(token_url_label, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) 

463 token_url_box.Add(self.token_url_input, 1, wx.ALL | wx.EXPAND, 5) 

464 vbox.Add(token_url_box, 0, wx.EXPAND | wx.ALL, 5) 

465 

466 # API Base URL 

467 base_url_box = wx.BoxSizer(wx.HORIZONTAL) 

468 base_url_label = wx.StaticText(self, label=T('API Base URL:')) 

469 self.base_url_input = wx.TextCtrl(self) 

470 base_url_box.Add(base_url_label, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) 

471 base_url_box.Add(self.base_url_input, 1, wx.ALL | wx.EXPAND, 5) 

472 vbox.Add(base_url_box, 0, wx.EXPAND | wx.ALL, 5) 

473 

474 # Model Name 

475 model_box = wx.BoxSizer(wx.HORIZONTAL) 

476 model_label = wx.StaticText(self, label=T('Model Name:')) 

477 self.model_input = wx.TextCtrl(self) 

478 model_box.Add(model_label, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) 

479 model_box.Add(self.model_input, 1, wx.ALL | wx.EXPAND, 5) 

480 vbox.Add(model_box, 0, wx.EXPAND | wx.ALL, 5) 

481 

482 # 测试按钮 

483 test_box = wx.BoxSizer(wx.HORIZONTAL) 

484 self.test_button = wx.Button(self, label=T('Test Connection')) 

485 self.test_button.Bind(wx.EVT_BUTTON, self.on_test) 

486 test_box.Add(self.test_button, 0, wx.ALL, 5) 

487 self.test_result = wx.StaticText(self, label="") 

488 test_box.Add(self.test_result, 1, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) 

489 vbox.Add(test_box, 0, wx.EXPAND | wx.ALL, 10) 

490 

491 self.SetSizer(vbox) 

492 

493 def get_client_id(self): 

494 return self.client_id_input.GetValue().strip() 

495 

496 def get_client_secret(self): 

497 return self.client_secret_input.GetValue().strip() 

498 

499 def get_token_url(self): 

500 return self.token_url_input.GetValue().strip() 

501 

502 def get_base_url(self): 

503 return self.base_url_input.GetValue().strip() 

504 

505 def get_model(self): 

506 return self.model_input.GetValue().strip() 

507 

508 def on_test(self, event): 

509 client_id = self.get_client_id() 

510 client_secret = self.get_client_secret() 

511 token_url = self.get_token_url() 

512 base_url = self.get_base_url() 

513 model = self.get_model() 

514 

515 if not client_id or not client_secret or not token_url or not base_url or not model: 

516 self.test_result.SetLabel(T("Please fill in all fields")) 

517 return 

518 

519 # Start testing in a separate thread to avoid UI freeze 

520 threading.Thread(target=self._test_connection, args=(client_id, client_secret, token_url, base_url)).start() 

521 self.test_result.SetLabel(T("Testing connection...")) 

522 self.test_button.Enable(False) 

523 

524 def _test_connection(self, client_id, client_secret, token_url, base_url): 

525 try: 

526 # First try to get a token 

527 response = requests.post( 

528 token_url, 

529 data={ 

530 "grant_type": "client_credentials", 

531 "client_id": client_id, 

532 "client_secret": client_secret 

533 }, 

534 timeout=10 

535 ) 

536 response.raise_for_status() 

537 token_data = response.json() 

538 token = token_data.get("access_token") 

539 

540 if not token: 

541 wx.CallAfter(self._update_test_result, False, "No token received") 

542 return 

543 

544 # Now try to access the models endpoint 

545 models_url = f"{base_url}/models" 

546 response = requests.get( 

547 models_url, 

548 headers={"Authorization": f"Bearer {token}"}, 

549 timeout=10 

550 ) 

551 response.raise_for_status() 

552 

553 # If we get here, the connection is working 

554 wx.CallAfter(self._update_test_result, True) 

555 except Exception as e: 

556 wx.CallAfter(self._update_test_result, False, str(e)) 

557 

558 def _update_test_result(self, success, error=None): 

559 if success: 

560 self.test_result.SetLabel(T("✓ Connection successful")) 

561 self.test_result.SetForegroundColour(wx.Colour(0, 128, 0)) 

562 else: 

563 self.test_result.SetLabel(f"{error}") 

564 self.test_result.SetForegroundColour(wx.Colour(255, 0, 0)) 

565 self.test_button.Enable(True) 

566 

567 def GetNext(self): 

568 # In the wizard flow, this should go directly to the model page 

569 # but for OAuth we have the model defined here 

570 wizard = self.GetParent() 

571 return wizard.model_page 

572 

573 def GetPrev(self): 

574 return self.prev_page 

575 

576class ProviderConfigWizard(wx.adv.Wizard): 

577 def __init__(self, llm_config, parent): 

578 super().__init__(parent, title=T('LLM Provider Configuration Wizard')) 

579 self.provider_config = llm_config 

580 self.init_ui() 

581 self.SetPageSize((600, 400)) 

582 self.Centre() 

583 self.log = logger.bind(src="wizard") 

584 

585 def init_ui(self): 

586 # 创建向导页面 

587 self.initial_page = InitialProviderPage(self, self.provider_config) 

588 self.trust_token_page = TrustTokenPage(self, self.provider_config) 

589 self.provider_page = ProviderPage(self, self.provider_config) 

590 self.model_page = ModelPage(self, self.provider_config) 

591 self.oauth_page = OAuthPage(self, self.provider_config) 

592 

593 # 绑定事件 

594 self.Bind(wx.adv.EVT_WIZARD_PAGE_CHANGED, self.on_page_changed) 

595 self.Bind(wx.adv.EVT_WIZARD_FINISHED, self.on_finished) 

596 

597 wx.CallAfter(self._set_button_labels) 

598 

599 def _set_button_labels(self): 

600 btn_next = self.FindWindowById(wx.ID_FORWARD) 

601 btn_back = self.FindWindowById(wx.ID_BACKWARD) 

602 btn_cancel = self.FindWindowById(wx.ID_CANCEL) 

603 

604 if btn_next: btn_next.SetLabel(T('Next')) 

605 if btn_back: btn_back.SetLabel(T('Back')) 

606 if btn_cancel: btn_cancel.SetLabel(T('Cancel')) 

607 

608 def on_page_changed(self, event): 

609 btn_next = self.FindWindowById(wx.ID_FORWARD) 

610 if btn_next: btn_next.SetLabel(T('Next')) 

611 

612 if event.GetPage() == self.model_page: 

613 if btn_next: btn_next.SetLabel(T('Finish')) 

614 # 从第一步进入第二步时,验证 API Key 并获取模型列表 

615 provider = self.provider_page.get_provider() 

616 api_key = self.provider_page.get_api_key() 

617 if not provider or not api_key: 

618 self.model_page.prev_page = self.trust_token_page 

619 provider = "Trustoken" 

620 api_key = self.trust_token_page.get_api_key() 

621 # 对于 TrustToken,直接设置模型为 auto 

622 self.model_page.set_models([], None, is_trust_token=True) 

623 else: 

624 self.model_page.prev_page = self.provider_page 

625 models = self.get_models(provider, api_key) 

626 if not models: 

627 event.Veto() 

628 return 

629 

630 # 设置模型列表 

631 selected_model = None 

632 if provider in self.provider_config.config: 

633 selected_model = self.provider_config.config[provider].get("selected_model") 

634 self.model_page.set_models(models, selected_model) 

635 elif event.GetPage() == self.trust_token_page: 

636 self.trust_token_page.prev_page = self.initial_page 

637 elif event.GetPage() == self.provider_page: 

638 self.provider_page.prev_page = self.initial_page 

639 elif event.GetPage() == self.initial_page: 

640 self.initial_page.prev_page = None 

641 elif event.GetPage() == self.oauth_page: 

642 self.oauth_page.prev_page = self.initial_page 

643 

644 def on_finished(self, event): 

645 # 根据 model_page 的 prev_page 来判断是从哪个路径来的 

646 if self.model_page.prev_page == self.trust_token_page: 

647 provider = "Trustoken" 

648 api_key = self.trust_token_page.get_api_key() 

649 else: 

650 provider = self.provider_page.get_provider() 

651 api_key = self.provider_page.get_api_key() 

652 

653 selected_model = self.model_page.get_selected_model() 

654 max_tokens = self.model_page.get_max_tokens() 

655 provider_info = self.provider_config.providers[provider] 

656 

657 config = self.provider_config.config 

658 config[provider] = { 

659 "api_key": api_key, 

660 "models": self.model_page.model_choice.GetItems(), 

661 "model": selected_model, 

662 "max_tokens": max_tokens, 

663 "type": provider_info["type"], 

664 "enable": True 

665 } 

666 

667 self.provider_config.save_config(config) 

668 wx.MessageBox(T('Configuration saved'), T('Success'), wx.OK | wx.ICON_INFORMATION) 

669 

670 def get_models(self, provider: str, api_key: str) -> List[str]: 

671 provider_info = self.provider_config.providers[provider] 

672 headers = { 

673 "Content-Type": "application/json" 

674 } 

675 

676 if provider == "Claude": 

677 headers["x-api-key"] = api_key 

678 headers["anthropic-version"] = "2023-06-01" 

679 else: 

680 headers["Authorization"] = f"Bearer {api_key}" 

681 

682 try: 

683 response = requests.get( 

684 f"{provider_info['api_base']}{provider_info['models_endpoint']}", 

685 headers=headers 

686 ) 

687 self.log.info(f"获取模型列表: {response.text}") 

688 if response.status_code == 200: 

689 data = response.json() 

690 self.log.info(f"获取模型列表成功: {data}") 

691 if provider in ["OpenAI", "DeepSeek", "xAI", "Claude"]: 

692 return [model["id"] for model in data["data"]] 

693 elif provider == "Gemini": 

694 return [model["name"] for model in data["models"]] 

695 # 其他 provider 的模型解析逻辑 

696 return [] 

697 except Exception as e: 

698 wx.MessageBox(f"{T('Model list acquisition failed')}: {str(e)}", T('Error'), wx.OK | wx.ICON_ERROR) 

699 return [] 

700 

701def show_provider_config(llm_config, parent=None): 

702 wizard = ProviderConfigWizard(llm_config, parent) 

703 wizard.RunWizard(wizard.initial_page) 

704 wizard.Destroy() 

705 return wx.ID_OK