feat: multi-provider support via OpenAI-compatible client#92
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the extension to support multiple AI providers, such as OpenAI, Groq, and Ollama, by introducing a provider adapter and migrating to the official openai library. The settings UI has been updated with a provider selector that dynamically manages API configurations and model defaults. Review feedback identifies a critical bug in the streaming logic that could skip usage data and a configuration key mismatch in the balance refresh function. Additionally, the reviewer suggests aligning model persistence with the save button logic, renaming core functions to be provider-agnostic, and refining default model selection for custom providers.
There was a problem hiding this comment.
Pull request overview
Adds multi-provider support for the VS Code extension by routing chat completions through an OpenAI-compatible client and exposing provider/model selection in the webview settings.
Changes:
- Introduces provider presets and an OpenAI SDK-based streaming client.
- Adds provider selection/model UI and persists provider/base URL settings.
- Generalizes several API error strings and adds the
openaidependency.
结论:需修改
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
src/webview/html.js |
Adds provider dropdown to the API settings modal. |
src/utils/i18n.js |
Makes selected API error strings provider-generic. |
src/chat/sub-agent.js |
Routes sub-agent chat calls through the new adapter. |
src/chat/provider.js |
Loads/saves provider settings and uses adapter balance export. |
src/chat/agent-loop.js |
Sends provider/base URL/model through the new chat adapter. |
src/api/openai-client.js |
Adds OpenAI-compatible streaming client and balance helper. |
src/api/deepseek.js |
Removes the previous raw DeepSeek SSE client. |
src/api/adapter.js |
Adds provider presets and request resolution. |
package.json |
Adds provider configuration and openai dependency. |
package-lock.json |
Locks the new openai dependency. |
media/chat.js |
Adds provider/model UI logic and settings modal handling. |
Comments suppressed due to low confidence (7)
src/chat/sub-agent.js:113
- Sub-agents also require
deepseekAgent.apiKeybefore resolving the provider. This makesspawn_agentunusable with the keyless Ollama provider even though the adapter preset marks Ollama asnoApiKey.
const provider = cfg.get('provider') || 'deepseek';
const model = cfg.get('subAgentModel') || cfg.get('defaultModel') || 'deepseek-v4-flash';
const baseUrl = (cfg.get('apiBaseUrl') || '').trim();
media/chat.js:1925
- The link text is updated for the currently selected provider, but the click handler opens
_stgOrigProvider. If the user changes provider in the modal before saving, the visible OpenAI/Groq/Gemini link can still open the old provider's API-key page.
var linkInfo = PROVIDER_KEY_LINKS[_stgOrigProvider];
if (linkInfo) vscode.postMessage({ type: 'openExternal', url: linkInfo.url });
src/api/openai-client.js:148
- The DeepSeek-only guard uses a substring check, so hosts such as
https://api.deepseek.com.evil.examplepass and receive the bearer token on the balance request. Validate the parsed hostname exactly (or with a strict subdomain check) before sending credentials.
const base = (baseUrl || 'https://api.deepseek.com').replace(/\/$/, '');
// Only query official DeepSeek endpoint; 3rd-party APIs may not support this route.
if (!base.includes('deepseek.com')) { resolve(null); return; }
let urlObj;
try { urlObj = new URL('/user/balance', base); } catch { resolve(null); return; }
media/chat.js:1975
- For Ollama,
switchToProvidersets the key field placeholder to “No API key required”, but the next line unconditionally overwrites it with the stored-key placeholder. Opening settings for an Ollama configuration therefore shows a disabled key field that still sayssk-...or the previous key hint.
_stgOrigProvider = m.provider || 'deepseek';
_stgOrigBaseUrl = m.baseUrl || PROVIDER_URLS[_stgOrigProvider] || 'https://api.deepseek.com';
if (stgProvider) stgProvider.value = _stgOrigProvider;
switchToProvider(_stgOrigProvider);
if (stgDsKey) { stgDsKey.value = ''; stgDsKey.placeholder = _stgDsKeySet ? (m.dsKeyHint || '(configured)') : 'sk-...'; }
src/api/openai-client.js:108
- Breaking on the
stopfinish reason can skip the final usage-only chunk emitted whenstream_options.include_usageis enabled. OpenAI-compatible streams commonly send token usage after the stop chunk withchoices: [], so normal no-tool replies will returnusage: nulland the UI will lose token/cost accounting.
if (choice.finish_reason === 'stop' && Object.keys(toolCalls).length === 0) {
break;
}
src/chat/agent-loop.js:324
- This provider-aware call still uses the DeepSeek-specific preflight limit just above (
MODEL_CTX_HARD_LIMIT = 900000) and the global 96k compact budget. Providers/models such as Groq 32k, OpenAI 128k, or local Ollama models can receive over-context requests instead of compacting/refusing at their actual limit.
const { toolCalls, usage } = await streamChat(
{ provider, apiKey, baseUrl, messages: finalMsgs, model, noTools: askMode, tools: allTools },
src/chat/agent-loop.js:121
- The selected provider is only used for the main streaming calls; the auto-title call later in this method still uses
SessionStore._llmTitle, which hardcodesdeepseek-chatand falls back tohttps://api.deepseek.com. With non-DeepSeek providers this can either fail title generation or send the configured provider key to the wrong endpoint when noapiBaseUrloverride is present.
const provider = cfg.get('provider') || 'deepseek';
const model = cfg.get('defaultModel') || 'deepseek-v4-pro';
const baseUrl = (cfg.get('apiBaseUrl') || '').trim();
|
AI 审查在超大 diff 下质量会下降,建议:
|
|
AI 审查在超大 diff 下质量会下降,建议:
|
| } | ||
| if (modelDrop){ var opts = modelDrop.querySelectorAll(".mo"); for (var i=0;i<opts.length;i++) opts[i].classList.toggle("sel", opts[i].dataset.model === md.value); } | ||
| } | ||
| function getSelectedModel(){ |
There was a problem hiding this comment.
在这段代码中,虽然对 modelBtn 的内容进行了更改,但需要注意以下几点:
-
安全性:在处理用户输入的
md.name时,确保其内容是安全的,避免 XSS 攻击。虽然使用了textContent来设置文本内容,但在其他地方可能存在未经过滤的用户输入。 -
性能:使用
appendChild多次操作 DOM 可能会影响性能,尤其是在频繁更新的情况下。可以考虑使用文档片段(DocumentFragment)来减少重排和重绘。 -
可维护性:建议将创建
chev的逻辑提取为一个单独的函数,以提高代码的可读性和可维护性。 -
代码风格:确保代码风格与项目中其他部分一致,例如使用一致的缩进和空格。
综上所述,建议对这段代码进行修改以提高安全性和性能。
a6c5919 to
2a8e6f3
Compare
|
@copilot resolve the merge conflicts in this pull request |
Done. I merged |
|
AI 审查在超大 diff 下质量会下降,建议:
|
|
AI 审查在超大 diff 下质量会下降,建议:
|
8f384f9 to
8cb7c51
Compare
8cb7c51 to
60e64ed
Compare
|
@copilot resolve the merge conflicts in this pull request |
|
Hi @YSMsimon, 睡醒看到这条 :) 我对 PR #92 做了个详细对比,发现你这个 PR 的目标
考虑到再硬解 11 个文件的冲突(其中 adapter.js 和 openai-client.js 如果你觉得当前 main 的实现还缺什么(比如某些 provider 切换 UI 细节 如果你觉得 #92 仍有独立价值,告诉我具体是哪几处,我再重新评估。 |
变更说明 / Summary
新增多 Provider 支持,用户可在设置弹窗中切换 DeepSeek、OpenAI、Groq、Ollama(本地)、Gemini 或自定义(OpenRouter 等)提供商,无需修改代码。
底层统一使用 openai npm 客户端(原生支持所有 OpenAI 兼容接口),替换原有的手写 SSE/fetch 逻辑。新增 src/api/adapter.js 作为路由层,按 Provider 自动选择正确的 baseURL、默认模型、以及是否启用 stream_options / parallel_tool_calls(部分 Provider 不支持这两个字段)。对于不支持 tool use 的路由(如 OpenRouter 某些模型返回 404),自动 fallback 重试(去掉 tools 字段)。i18n 错误文案也统一改为 Provider 无关的表述。
变更类型 / Type of change
影响范围 / Affected areas
src/api/**(DeepSeek 接入)src/chat/**(对话核心)src/tools/**(工具执行,安全敏感)src/webview/**(UI)src/prompts/**package.json/ 依赖.github/**自检 / Checklist
截图 / 录屏(可选)
关联 Issue
Closes #