Skip to content

feat: multi-provider support via OpenAI-compatible client#92

Closed
YSMsimon wants to merge 1 commit into
mainfrom
fix/openai-client
Closed

feat: multi-provider support via OpenAI-compatible client#92
YSMsimon wants to merge 1 commit into
mainfrom
fix/openai-client

Conversation

@YSMsimon
Copy link
Copy Markdown
Collaborator

  • Replace raw SSE/fetch with openai npm client (works for DeepSeek, OpenAI, Groq, Ollama, Gemini, Custom/OpenRouter)
  • Add src/api/adapter.js to route provider presets (baseUrl, model list, flags)
  • Add provider selector dropdown in settings modal
  • Per-provider model dropdowns; Ollama/Custom use free-text input
  • Conditionally send stream_options and parallel_tool_calls based on provider support
  • Auto-retry without tools on 404 "tool use unsupported" errors (OpenRouter routing)
  • Update i18n strings to be provider-generic (not hardcoded to DeepSeek)
  • Add deepseekAgent.provider config setting

变更说明 / 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

  • Bug 修复
  • [x ] 新功能
  • [x ] 重构 / 性能优化
  • 文档 / 注释
  • CI / 构建脚本
  • 其他:

影响范围 / Affected areas

  • src/api/**(DeepSeek 接入)
  • [x ] src/chat/**(对话核心)
  • src/tools/**(工具执行,安全敏感)
  • [x ] src/webview/**(UI)
  • src/prompts/**
  • [x ] package.json / 依赖
  • CI / .github/**
  • 其他:

自检 / Checklist

  • [x ] 本地手动跑过受影响功能
  • [x ] 没有引入新的安全风险(密钥、命令注入、路径穿越)
  • [x ] 没有写入个人 / 测试用 API key
  • [x ] 修改的 webview 资源遵守了 CSP
  • 必要时已更新 README / Release Notes

截图 / 录屏(可选)

关联 Issue

Closes #

@YSMsimon YSMsimon requested a review from ZhouChaunge as a code owner May 17, 2026 13:44
@ZhouChaunge ZhouChaunge self-assigned this May 17, 2026
@ZhouChaunge ZhouChaunge requested a review from Copilot May 17, 2026 13:47
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment thread src/api/openai-client.js Outdated
Comment thread src/chat/provider.js
Comment thread media/chat.js Outdated
Comment thread media/chat.js Outdated
Comment thread src/api/openai-client.js Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 openai dependency.

结论:需修改

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.apiKey before resolving the provider. This makes spawn_agent unusable with the keyless Ollama provider even though the adapter preset marks Ollama as noApiKey.
        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.example pass 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, switchToProvider sets 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 says sk-... 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 stop finish reason can skip the final usage-only chunk emitted when stream_options.include_usage is enabled. OpenAI-compatible streams commonly send token usage after the stop chunk with choices: [], so normal no-tool replies will return usage: null and 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 hardcodes deepseek-chat and falls back to https://api.deepseek.com. With non-DeepSeek providers this can either fail title generation or send the configured provider key to the wrong endpoint when no apiBaseUrl override is present.
        const provider = cfg.get('provider') || 'deepseek';
        const model    = cfg.get('defaultModel') || 'deepseek-v4-pro';
        const baseUrl  = (cfg.get('apiBaseUrl') || '').trim();

Comment thread src/chat/agent-loop.js Outdated
Comment thread media/chat.js Outdated
Comment thread package.json
Comment thread src/api/openai-client.js Outdated
Comment thread media/chat.js Outdated
Comment thread src/chat/agent-loop.js
Comment thread src/chat/sub-agent.js Outdated
Comment thread src/api/openai-client.js Outdated
Comment thread media/chat.js
Comment thread media/chat.js Outdated
@github-actions
Copy link
Copy Markdown

⚠️ PR 偏大:有效变更约 885 行(建议阈值 500 行)。

AI 审查在超大 diff 下质量会下降,建议:

  • 将无关改动拆分为独立 PR
  • 确保每个 PR 只聚焦一个功能或修复

Comment thread media/chat.js Fixed
Comment thread media/chat.js Fixed
@github-actions
Copy link
Copy Markdown

⚠️ PR 偏大:有效变更约 893 行(建议阈值 500 行)。

AI 审查在超大 diff 下质量会下降,建议:

  • 将无关改动拆分为独立 PR
  • 确保每个 PR 只聚焦一个功能或修复

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Code review by ChatGPT

Comment thread media/chat.js
}
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(){
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

在这段代码中,虽然对 modelBtn 的内容进行了更改,但需要注意以下几点:

  1. 安全性:在处理用户输入的 md.name 时,确保其内容是安全的,避免 XSS 攻击。虽然使用了 textContent 来设置文本内容,但在其他地方可能存在未经过滤的用户输入。

  2. 性能:使用 appendChild 多次操作 DOM 可能会影响性能,尤其是在频繁更新的情况下。可以考虑使用文档片段(DocumentFragment)来减少重排和重绘。

  3. 可维护性:建议将创建 chev 的逻辑提取为一个单独的函数,以提高代码的可读性和可维护性。

  4. 代码风格:确保代码风格与项目中其他部分一致,例如使用一致的缩进和空格。

综上所述,建议对这段代码进行修改以提高安全性和性能。

@YSMsimon YSMsimon force-pushed the fix/openai-client branch from a6c5919 to 2a8e6f3 Compare May 17, 2026 15:43
@ZhouChaunge
Copy link
Copy Markdown
Collaborator

@copilot resolve the merge conflicts in this pull request

Copy link
Copy Markdown
Contributor

Copilot AI commented May 17, 2026

@copilot resolve the merge conflicts in this pull request

Done. I merged origin/main, resolved all conflicts in this PR branch, and pushed commit b500079.

@github-actions
Copy link
Copy Markdown

⚠️ PR 偏大:有效变更约 891 行(建议阈值 500 行)。

AI 审查在超大 diff 下质量会下降,建议:

  • 将无关改动拆分为独立 PR
  • 确保每个 PR 只聚焦一个功能或修复

@github-actions
Copy link
Copy Markdown

⚠️ PR 偏大:有效变更约 899 行(建议阈值 500 行)。

AI 审查在超大 diff 下质量会下降,建议:

  • 将无关改动拆分为独立 PR
  • 确保每个 PR 只聚焦一个功能或修复

@YSMsimon YSMsimon force-pushed the fix/openai-client branch from 8f384f9 to 8cb7c51 Compare May 17, 2026 15:59
@YSMsimon
Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts in this pull request

@ZhouChaunge
Copy link
Copy Markdown
Collaborator

Hi @YSMsimon

睡醒看到这条 :) 我对 PR #92 做了个详细对比,发现你这个 PR 的目标
(multi-provider via OpenAI-compatible client)已经在 #109 里完整实现,
而且 main 当前版本更完整:

  • src/api/adapter.js: main 4429 bytes vs PR 2907 bytes,
    main 含 7 个 provider(包括你这里没有的 anthropic),并已支持
    parallelTools / noApiKey / isAnthropic 等差异化开关。
  • src/api/openai-client.js: 两版大小相近,main 经过更多迭代修复。
  • 其余 9 个文件 main 也已经改造完毕。

考虑到再硬解 11 个文件的冲突(其中 adapter.js 和 openai-client.js
是 add/add)实际收益已经为 0,我建议直接 关闭 #92

如果你觉得当前 main 的实现还缺什么(比如某些 provider 切换 UI 细节
或 pricing 表项),欢迎在 main 基础上开一个新的小 PR 来补,会干净很多。

如果你觉得 #92 仍有独立价值,告诉我具体是哪几处,我再重新评估。
辛苦了 🙏

@ZhouChaunge ZhouChaunge deleted the fix/openai-client branch May 23, 2026 12:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants