feat(ai-provider): ✨ Add AI SDK multi-provider support with unified runtime API#7
Conversation
AI 代码审查汇总PR: #7 (feat(ai-provider): ✨ Add AI SDK multi-provider support with unified runtime API) 总体评价共发现 12 条可执行问题,建议优先处理 CRITICAL/HIGH。 主要问题(按严重级别)
可执行建议
潜在风险
测试建议
文件级覆盖说明
无法 inline 的已处理项
覆盖状态
未覆盖文件清单:
无 patch 文件覆盖清单:
轮次与预算
|
| baseURL: config.apiBase || undefined | ||
| }); | ||
| core.info(`LLM compatibility mode: ${config.llmCompatibilityMode}`); | ||
| const plannerModelInstance = createModel(provider, config.plannerModel); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| runStructuredWithRepair | ||
| } = require('../src/agents'); | ||
| const { COMPATIBILITY_MODES } = require('../src/model-runtime'); | ||
| const { createProvider, createModel } = require('../src/provider'); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } else { | ||
| process.stdout.write(`FAIL (${result.error})\n`); | ||
| } | ||
| const cases = [ |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| if (recommended) { | ||
| console.log(`Recommended mode: ${recommended.mode}`); | ||
| if (plannerOk && reviewerOk) { | ||
| console.log('Result: PASS'); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| const baseURL = process.env.OPENAI_API_BASE || ''; | ||
| const apiKey = process.env.OPENAI_API_KEY || process.env.API_KEY || ''; | ||
| const baseURL = process.env.OPENAI_API_BASE || process.env.API_BASE || ''; | ||
| const providerType = process.env.AI_PROVIDER || 'openai'; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| `Input openai_api_base host is not in allowlist: ${host}. ` + | ||
| 'Set openai_api_base_allowlist to explicitly trust this host.' | ||
| ); | ||
| if (allowedHosts && allowedHosts.length > 0) { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| async function requestStructuredOutput({ agent, input, repairContext }) { | ||
| const userPrompt = buildUserInput(agent, input, repairContext); | ||
|
|
||
| const result = await generateText({ |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| | `openai_api_base` | no | env `OPENAI_API_BASE` | Optional custom OpenAI-compatible base URL | | ||
| | `openai_api_base_allowlist` | no | `api.openai.com` | Allowed hostnames for `openai_api_base` (HTTPS only) | | ||
| | `ai_provider` | no | `openai` | AI provider type: `openai`, `anthropic`, `google`, `mistral`, or `openai-compatible` | | ||
| | `api_key` | no | env `OPENAI_API_KEY` | API key for the selected AI provider | |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| if (recommended) { | ||
| console.log(`Recommended mode: ${recommended.mode}`); | ||
| if (plannerOk && reviewerOk) { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| const baseURL = process.env.OPENAI_API_BASE || ''; | ||
| const apiKey = process.env.OPENAI_API_KEY || process.env.API_KEY || ''; | ||
| const baseURL = process.env.OPENAI_API_BASE || process.env.API_BASE || ''; | ||
| const providerType = process.env.AI_PROVIDER || 'openai'; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
…d provider validation - Support per-agent model override via agent.modelInstance in requestStructuredOutput, falling back to runtimeState.model - Create separate reviewerModelInstance in index.js and inject into reviewer agents so reviewer_model config takes effect - Reject all hosts when allowlist resolves to empty set instead of silently skipping validation (security regression fix) - Add AI_PROVIDER validation against SUPPORTED_PROVIDERS in verify-schema-support.js for early error reporting
…t rejection - Test requestStructuredOutput uses agent.modelInstance when set - Test requestStructuredOutput falls back to runtime model when null - Test loadConfig rejects api_base with empty-after-normalization allowlist
| `Input openai_api_base host is not in allowlist: ${host}. ` + | ||
| 'Set openai_api_base_allowlist to explicitly trust this host.' | ||
| ); | ||
| if (Array.isArray(allowedHosts)) { |
There was a problem hiding this comment.
[HIGH] validateBaseURL 空白 allowlist 阻断逻辑缺少测试
validateBaseURL 新增了两个分支:(1) allowlist 为空数组时阻断所有请求;(2) allowlist 非数组时跳过校验。这两条路径都没有测试覆盖。
建议: 在 test/config.test.js 添加用例:allowedHosts=[] 时抛 'all hosts are blocked'、allowedHosts=undefined/null 时非 https 仍抛错但不在 allowlist 分支、allowedHosts 包含目标 host 时通过。
风险: 空 allowlist 阻断可能误杀合法配置,非数组跳过可能放行不应信任的 host。
置信度: 0.88
| }); | ||
|
|
||
| // AI SDK sets output to the parsed object when successful | ||
| if (result.output !== undefined && result.output !== null) { |
There was a problem hiding this comment.
[HIGH] requestStructuredOutput output=null 回退路径未测试
AI SDK 结构化输出失败时 result.output 可能为 null,新增的文本回退路径是关键容错机制,但完全没有测试。该路径涉及空文本、JSON 解析失败、schema 验证等多个边界。
建议: mock generateText 分别返回:output=null + text=''、output=null + text='not json'、output=null + text='valid json object'、output=valid object。验证每种情况下 requestStructuredOutput 的行为和错误码。
风险: 某些提供商(如 google/mistral)可能经常触发此回退路径,未测试意味着生产中遇到的解析失败无法被提前发现。
置信度: 0.85
| `Input openai_api_base host is not in allowlist: ${host}. ` + | ||
| 'Set openai_api_base_allowlist to explicitly trust this host.' | ||
| ); | ||
| if (Array.isArray(allowedHosts)) { |
There was a problem hiding this comment.
[MEDIUM] validateBaseURL 当 allowedHosts 非数组时跳过 allowlist 检查,构成安全回归
旧版 validateOpenAIBaseURL 无论 allowedHosts 值为何都执行 allowlist 校验(空值默认空集,阻止所有主机);新版 validateBaseURL 仅在 Array.isArray 时才校验,非数组输入时完全跳过检查,允许任意主机。
建议: 在 if (Array.isArray(allowedHosts)) 块之后添加 else 分支,当 baseURL 非空且 allowedHosts 不是数组时抛出错误或默认阻止。
风险: 若调用方误传 undefined/null 作为 allowedHosts,任意 baseURL 将通过验证,可能导致 SSRF 等安全问题。
置信度: 0.88
| `Input openai_api_base host is not in allowlist: ${host}. ` + | ||
| 'Set openai_api_base_allowlist to explicitly trust this host.' | ||
| ); | ||
| if (Array.isArray(allowedHosts)) { |
There was a problem hiding this comment.
[MEDIUM] validateBaseURL 在 allowedHosts 非数组时跳过整个允许列表校验(SSRF风险回归)
validateBaseURL 新增 Array.isArray(allowedHosts) 守卫,当参数非数组时整个允许列表校验被静默跳过。旧代码在 allowedHosts 为 falsy 时默认阻止所有主机,新代码则允许所有 HTTPS 主机。这是 SSRF 防护的回归。
建议: 在 Array.isArray 检查后添加 else 分支,当 allowedHosts 非数组时抛出错误或默认阻止:
if (Array.isArray(allowedHosts)) {
// existing check
} else {
throw new Error('api_base allowlist must be provided when api_base is set.');
}
或者恢复旧逻辑 const allow = new Set((Array.isArray(allowedHosts) ? allowedHosts : []).map(normalizeHost).filter(Boolean));
风险: 若 allowedHosts 意外为非数组值,API 密钥可被发送到任意 HTTPS 端点,造成密钥泄露和 SSRF
置信度: 0.82
| core.getInput('openai_api_base_allowlist') || process.env.OPENAI_API_BASE_ALLOWLIST || 'api.openai.com' | ||
|
|
||
| // Provider configuration with backward compatibility | ||
| const aiProvider = parseEnumInput('ai_provider', 'openai', SUPPORTED_PROVIDERS); |
There was a problem hiding this comment.
[MEDIUM] config.js 向后兼容输入链缺少测试
loadConfig 新增了多级输入回退链(新输入名 → 旧输入名 → 环境变量),以及 llm_compatibility_mode 弃用警告,这些向后兼容逻辑缺少测试。
建议: 在 test/config.test.js 添加:仅设 api_key 时优先取、仅设 openai_api_key 时回退取、同时设时 api_key 优先;api_base 同理;设 llm_compatibility_mode=chat_json_schema 时触发 core.warning。
风险: 向后兼容是用户升级的关键保障,回退链错误会导致现有用户配置失效。
置信度: 0.80
| * @param {{ model: import('ai').LanguageModel }} opts | ||
| */ | ||
| function configureRuntime({ model }) { | ||
| if (!model) { |
There was a problem hiding this comment.
[MEDIUM] configureRuntime 缺少 null/undefined model 的测试
configureRuntime 和 runStructuredWithRepair 都有 model 为空时的守卫抛错,但缺少测试验证这些错误路径。
建议: 添加测试:configureRuntime({ model: null }) 抛错、configureRuntime({ model: undefined }) 抛错、configureRuntime({}) 抛错;未调用 configureRuntime 直接调用 runStructuredWithRepair 抛错。
风险: 低风险,但错误信息质量和守卫完整性无法回归验证。
置信度: 0.82
| const provider = createProvider({ | ||
| provider: providerType, | ||
| apiKey, | ||
| baseURL: baseURL || undefined |
There was a problem hiding this comment.
[LOW] verify-schema-support.js baseURL未经allowlist校验直接传入createProvider
verify-schema-support.js从环境变量读取baseURL后直接传给createProvider,未像主action路径(config模块)那样执行HTTPS强制和域名allowlist校验。若该脚本在共享CI环境中运行且环境变量可被外部影响,可能构成SSRF向量。
建议: 在verify-schema-support.js中复用config模块的baseURL校验逻辑,或在createProvider调用前添加HTTPS和allowlist检查。
风险: 本地/CI脚本中用户自控环境变量,SSRF为自伤型风险,实际可利用性低。
置信度: 0.75
|
|
||
| console.log( | ||
| `Compatibility check start: models=${models.join('|')}${baseURL ? ` base=${baseURL}` : ''} modes=${modes.join(',')}` | ||
| `Compatibility check start: provider=${providerType} models=${models.join('|')}${baseURL ? ` base=${baseURL}` : ''}` |
There was a problem hiding this comment.
[LOW] baseURL被输出到控制台日志
baseURL完整输出到console.log,在共享CI日志中可能泄露内部服务端点信息。
建议: 考虑仅输出baseURL的域名部分,或在CI场景下省略baseURL输出。
风险: CI日志可能被多人可见,泄露内部API网关地址。
置信度: 0.72
| throw new Error('openai-compatible provider requires a base URL (api_base).'); | ||
| } | ||
| return createOpenAICompatible({ | ||
| name: 'custom', |
There was a problem hiding this comment.
[LOW] openai-compatible 提供商的 name 硬编码为 'custom'
createOpenAICompatible 调用中 name 字段硬编码为 'custom',使用者无法区分不同的兼容端点。
建议: 将 name 作为可选参数暴露,例如 { provider, apiKey, baseURL, name? },默认值保持 'custom'。
风险: 当配置多个 openai-compatible 端点时无法区分,但当前使用场景单一。
置信度: 0.85
Summary
src/provider.jsfactory module withcreateProviderandcreateModelfor unified provider/model instantiationsrc/model-runtime.jsby removing manual compatibility mode fallback logic in favor of AI SDK's built-in structured output handlingTest Plan
npm test)test/provider.test.jsand updatedtest/model-runtime.test.jsopenai_api_keyandopenai_api_baseinputs still work🤖 Generated with TiyCode