Skip to content

feat(ai-provider): ✨ Add AI SDK multi-provider support with unified runtime API#7

Merged
jorben merged 3 commits into
masterfrom
feat/add-ai-provider-factory
Apr 19, 2026
Merged

feat(ai-provider): ✨ Add AI SDK multi-provider support with unified runtime API#7
jorben merged 3 commits into
masterfrom
feat/add-ai-provider-factory

Conversation

@HayWolf

@HayWolf HayWolf commented Apr 19, 2026

Copy link
Copy Markdown

Summary

  • Replace direct OpenAI SDK integration with Vercel AI SDK, enabling multi-provider support (OpenAI, Anthropic, Google, Mistral, and OpenAI-compatible endpoints)
  • Introduce src/provider.js factory module with createProvider and createModel for unified provider/model instantiation
  • Simplify src/model-runtime.js by removing manual compatibility mode fallback logic in favor of AI SDK's built-in structured output handling

Test Plan

  • All existing unit tests updated and passing (npm test)
  • New test suites: test/provider.test.js and updated test/model-runtime.test.js
  • Backward compatibility preserved: openai_api_key and openai_api_base inputs still work
  • Manual verification with each supported provider

🤖 Generated with TiyCode

@github-actions

github-actions Bot commented Apr 19, 2026

Copy link
Copy Markdown

AI 代码审查汇总

PR: #7 (feat(ai-provider): ✨ Add AI SDK multi-provider support with unified runtime API)
指定语言: 简体中文

总体评价

共发现 12 条可执行问题,建议优先处理 CRITICAL/HIGH。

主要问题(按严重级别)

  • CRITICAL (2)
    • src/model-runtime.js - model-runtime 导出 API 大幅变更,现有测试套件应全部失败
    • src/provider.js - 新增模块 src/provider.js 完全没有单元测试
  • HIGH (2)
    • src/config.js:38 - validateBaseURL 空白 allowlist 阻断逻辑缺少测试
    • src/model-runtime.js:98 - requestStructuredOutput output=null 回退路径未测试
  • MEDIUM (5)
    • src/config.js:38 - validateBaseURL 当 allowedHosts 非数组时跳过 allowlist 检查,构成安全回归
    • src/config.js:38 - validateBaseURL 在 allowedHosts 非数组时跳过整个允许列表校验(SSRF风险回归)
    • src/config.js:114 - config.js 向后兼容输入链缺少测试
    • src/model-runtime.js:13 - configureRuntime 缺少 null/undefined model 的测试
    • src/provider.js - 新增 provider.js 模块缺少对应测试文件
  • LOW (3)
    • scripts/verify-schema-support.js:128 - verify-schema-support.js baseURL未经allowlist校验直接传入createProvider
    • scripts/verify-schema-support.js:132 - baseURL被输出到控制台日志
    • src/provider.js:57 - openai-compatible 提供商的 name 硬编码为 'custom'

可执行建议

  • 修复 validateBaseURL:在 Array.isArray 检查的 else 分支中,当 baseURL 非空时抛出安全错误
  • 创建 test/provider.test.js 覆盖所有提供商创建路径和错误路径
  • 扩展 test/model-runtime.test.js 覆盖新的 configureRuntime 和 requestStructuredOutput 逻辑
  • 测试 validateBaseURL 传入 undefined/非数组 allowedHosts 的边界情况
  • 修复 validateBaseURL:在 Array.isArray(allowedHosts) 的 else 分支中抛错或默认阻止所有主机
  • 考虑在 requestStructuredOutput 中对 result.output 添加显式 Zod safeParse 校验以与 fallback 路径保持一致
  • 为 validateBaseURL 和 createProvider 添加单元测试覆盖边界条件
  • 立即创建 test/provider.test.js,覆盖全部 switch 分支和错误路径
  • 重写 test/model-runtime.test.js,用 mock 替代 AI SDK 调用,覆盖 generateText 成功/output=null 回退/空文本/JSON 解析失败/修复成功/两次失败
  • 在 test/config.test.js 补充 validateBaseURL 空 allowlist、非数组 allowlist、输入回退链的用例
  • 确保 npm test 在 CI 中实际执行且不会因旧测试引用已删除导出而静默失败
  • 添加 agent.modelInstance 优先于 runtimeState.model 的测试

潜在风险

  • validateBaseURL 在 allowedHosts 为 undefined 时跳过主机验证,可能被内部调用误用导致 SSRF
  • provider 包未安装时仅在运行时 lazy require 才报错,错误信息可能不够明确
  • AI SDK Output.object 对某些提供商的 schema 支持可能不完整,fallback 路径可靠性取决于 result.text 内容
  • 若 validateBaseURL 的 allowedHosts 被意外传入非数组值,API 密钥可能被发送到攻击者控制的 HTTPS 端点
  • openai-compatible provider 天然支持任意端点,允许列表是唯一防线——需确保配置正确
  • 旧测试文件引用已删除导出导致整个测试套件崩溃,CI 可能配置为忽略失败
  • provider.js 的动态 require 在缺少对应 @ai-sdk/* 包时仅在实际调用时才崩溃,无提前验证
  • 某些 AI SDK 提供商可能不支持 Output.object 导致 output=null 回退路径成为高频路径而非边缘路径
  • reviewerModel === plannerModel 的实例复用可能导致状态意外共享
  • verify脚本的baseURL无allowlist校验可能在共享CI环境中被利用进行SSRF(低概率)

测试建议

  • test/provider.test.js: 各提供商正常创建、不支持的提供商抛错、openai-compatible 无 baseURL 抛错、默认 provider
  • test/config.test.js: validateBaseURL(undefined allowlist)、空 allowlist 抛错、向后兼容输入名
  • test/model-runtime.test.js: configureRuntime 校验、agent.modelInstance 优先、repair retry 流程
  • 测试 validateBaseURL 在 allowedHosts 为 undefined/null/空数组时的行为(应阻止或抛错)
  • 测试 createProvider 各类型正确创建及 openai-compatible 无 baseURL 抛错
  • 测试 requestStructuredOutput 的 result.output 和 fallback 两条路径的 schema 校验一致性
  • 创建 test/provider.test.js: mock 各 @ai-sdk/* 包,测试 5 种 provider + default + baseURL 条件 + createModel 校验
  • 重写 test/model-runtime.test.js: mock generateText/Output,测试 configureRuntime、requestStructuredOutput(含 output=null 回退)、runStructuredWithRepair(首次成功/修复成功/两次失败)
  • 扩展 test/config.test.js: validateBaseURL 空/非数组 allowlist、api_key 回退链、ai_provider 枚举校验、llm_compatibility_mode 弃用警告
  • 添加集成级测试: index.js 中 createProvider→createModel→configureRuntime 完整链路的 happy path
  • 添加回归测试: 验证旧输入名(openai_api_key/openai_api_base)仍然可用
  • 为verify-schema-support.js添加baseURL安全校验的集成测试

文件级覆盖说明

  • src/provider.js: critical_risk (这是本次变更中测试覆盖最严重的缺口。)
  • src/model-runtime.js: critical_risk (核心 LLM 交互层,从 OpenAI 专有实现切换到 AI SDK 通用实现,必须有完整的 mock 测试。)
  • src/config.js: high_risk (安全相关的 URL 校验逻辑变更需要严格的回归测试。)
  • src/agents.js: low_risk (变更较小,主要是参数透传,风险较低。)
  • src/index.js: medium_risk (核心编排层的 provider/model 初始化是新的关键路径,需要至少集成级别覆盖。)
  • scripts/verify-schema-support.js: positive (移除modes循环(L210-L238旧代码)将API调用从 models×modes×3 降至 models×3,是显著性能提升。createModel在runCase内调用(R81)但应为轻量工厂函数,无性能隐患。SUPPORTED_PROVIDERS.includes为O(n)但数组仅5元素,可忽略。)
  • test/provider.test.js: ok (新增纯单元测试,无I/O、网络或复杂算法,无性能问题。)
  • test/config.test.js: ok (增加require.cache清除(R35,R43)带来微不足道的额外开销。_warnings数组追踪为轻量操作,无性能影响。)
  • test/model-runtime.test.js: ok (Mock从复杂OpenAI多端点模拟简化为单一generateText函数。旧代码的模式缓存测试已移除,但新模式下AI SDK单次调用即完成,无需缓存,实际运行时性能更优。修复重试上限从4+次降至2次(R91,R107)。)
  • README.md: ok (纯文档变更,无性能影响。)

无法 inline 的已处理项

  • src/model-runtime.js: model-runtime 导出 API 大幅变更,现有测试套件应全部失败 (line_missing_or_invalid)
  • src/provider.js: 新增模块 src/provider.js 完全没有单元测试 (file_level_finding)
  • src/provider.js: 新增 provider.js 模块缺少对应测试文件 (file_level_finding)

覆盖状态

  • Target files: 10
  • Covered files: 10
  • Uncovered files: 0
  • No-patch/binary covered as file-level: 0
  • 置信度未知(N/A)的问题数: 0

未覆盖文件清单:

无 patch 文件覆盖清单:

轮次与预算

  • 轮次: 2/2
  • 计划批次: 3
  • 执行批次: 3
  • SubAgent 执行次数: 8
  • Planner 调用: 2
  • SubAgent 调用: 12
  • 模型调用: 14/64
  • 结构化输出降级为仅汇总评论: 否

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

自动化 PR 审查已完成。

  • Findings kept: 11
  • Findings with unknown confidence: 0
  • Inline comments attempted: 10
  • Target files: 10
  • Covered files: 10
  • Uncovered files: 0
    详细结论请查看汇总评论。

Comment thread src/index.js
baseURL: config.apiBase || undefined
});
core.info(`LLM compatibility mode: ${config.llmCompatibilityMode}`);
const plannerModelInstance = createModel(provider, config.plannerModel);

This comment was marked as outdated.

Comment thread scripts/verify-schema-support.js Outdated
runStructuredWithRepair
} = require('../src/agents');
const { COMPATIBILITY_MODES } = require('../src/model-runtime');
const { createProvider, createModel } = require('../src/provider');

This comment was marked as outdated.

} else {
process.stdout.write(`FAIL (${result.error})\n`);
}
const cases = [

This comment was marked as outdated.

if (recommended) {
console.log(`Recommended mode: ${recommended.mode}`);
if (plannerOk && reviewerOk) {
console.log('Result: PASS');

This comment was marked as outdated.

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.

Comment thread src/config.js Outdated
`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.

Comment thread src/model-runtime.js
async function requestStructuredOutput({ agent, input, repairContext }) {
const userPrompt = buildUserInput(agent, input, repairContext);

const result = await generateText({

This comment was marked as outdated.

Comment thread README.md
| `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.


if (recommended) {
console.log(`Recommended mode: ${recommended.mode}`);
if (plannerOk && reviewerOk) {

This comment was marked as outdated.

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.

jorben added 2 commits April 19, 2026 13:53
…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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

自动化 PR 审查已完成。

  • Findings kept: 12
  • Findings with unknown confidence: 0
  • Inline comments attempted: 9
  • Target files: 10
  • Covered files: 10
  • Uncovered files: 0
    详细结论请查看汇总评论。

Comment thread src/config.js
`Input openai_api_base host is not in allowlist: ${host}. ` +
'Set openai_api_base_allowlist to explicitly trust this host.'
);
if (Array.isArray(allowedHosts)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[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

[来自 SubAgent:testing]

Comment thread src/model-runtime.js
});

// AI SDK sets output to the parsed object when successful
if (result.output !== undefined && result.output !== null) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[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

[来自 SubAgent:testing]

Comment thread src/config.js
`Input openai_api_base host is not in allowlist: ${host}. ` +
'Set openai_api_base_allowlist to explicitly trust this host.'
);
if (Array.isArray(allowedHosts)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MEDIUM] validateBaseURL 当 allowedHosts 非数组时跳过 allowlist 检查,构成安全回归

旧版 validateOpenAIBaseURL 无论 allowedHosts 值为何都执行 allowlist 校验(空值默认空集,阻止所有主机);新版 validateBaseURL 仅在 Array.isArray 时才校验,非数组输入时完全跳过检查,允许任意主机。

建议: 在 if (Array.isArray(allowedHosts)) 块之后添加 else 分支,当 baseURL 非空且 allowedHosts 不是数组时抛出错误或默认阻止。

风险: 若调用方误传 undefined/null 作为 allowedHosts,任意 baseURL 将通过验证,可能导致 SSRF 等安全问题。

置信度: 0.88

[来自 SubAgent:general]

Comment thread src/config.js
`Input openai_api_base host is not in allowlist: ${host}. ` +
'Set openai_api_base_allowlist to explicitly trust this host.'
);
if (Array.isArray(allowedHosts)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[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

[来自 SubAgent:security]

Comment thread src/config.js
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[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

[来自 SubAgent:testing]

Comment thread src/model-runtime.js
* @param {{ model: import('ai').LanguageModel }} opts
*/
function configureRuntime({ model }) {
if (!model) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MEDIUM] configureRuntime 缺少 null/undefined model 的测试

configureRuntime 和 runStructuredWithRepair 都有 model 为空时的守卫抛错,但缺少测试验证这些错误路径。

建议: 添加测试:configureRuntime({ model: null }) 抛错、configureRuntime({ model: undefined }) 抛错、configureRuntime({}) 抛错;未调用 configureRuntime 直接调用 runStructuredWithRepair 抛错。

风险: 低风险,但错误信息质量和守卫完整性无法回归验证。

置信度: 0.82

[来自 SubAgent:testing]

const provider = createProvider({
provider: providerType,
apiKey,
baseURL: baseURL || undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[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

[来自 SubAgent:security]


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}` : ''}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[LOW] baseURL被输出到控制台日志

baseURL完整输出到console.log,在共享CI日志中可能泄露内部服务端点信息。

建议: 考虑仅输出baseURL的域名部分,或在CI场景下省略baseURL输出。

风险: CI日志可能被多人可见,泄露内部API网关地址。

置信度: 0.72

[来自 SubAgent:security]

Comment thread src/provider.js
throw new Error('openai-compatible provider requires a base URL (api_base).');
}
return createOpenAICompatible({
name: 'custom',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[LOW] openai-compatible 提供商的 name 硬编码为 'custom'

createOpenAICompatible 调用中 name 字段硬编码为 'custom',使用者无法区分不同的兼容端点。

建议: 将 name 作为可选参数暴露,例如 { provider, apiKey, baseURL, name? },默认值保持 'custom'。

风险: 当配置多个 openai-compatible 端点时无法区分,但当前使用场景单一。

置信度: 0.85

[来自 SubAgent:general]

@jorben jorben merged commit b92ec09 into master Apr 19, 2026
2 checks passed
@jorben jorben deleted the feat/add-ai-provider-factory branch April 19, 2026 06:34
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.

2 participants