Skip to content

pref(provider):完成OpenAI-compatible子包拆离#219

Merged
phantom5099 merged 6 commits into
1024XEngineer:mainfrom
phantom5099:main
Apr 10, 2026
Merged

pref(provider):完成OpenAI-compatible子包拆离#219
phantom5099 merged 6 commits into
1024XEngineer:mainfrom
phantom5099:main

Conversation

@phantom5099

Copy link
Copy Markdown
Collaborator

变更说明

本 PR 基于当前工作树相对上次提交的实际变更,对 OpenAI-compatible provider 的目录结构与职责边界进行收敛。当前仓库虽然已经完成了 openai -> openaicompat 的命名收口,但 openaicompat 根包仍然同时承载共享入口职责与 /chat/completions 具体协议实现,后续若继续接入 responses,根包会再次膨胀成新的“大而全兼容桶”。

这次改动的重点不是功能扩张,而是把已经可复用的 Chat Completions 协议处理逻辑迁移到独立子包,使 openaicompat 根包回到“共享入口 + discovery + transport + api_style 分流”的定位,并为后续 responses 子协议接入预留清晰扩展点。当前 api_style: responses 仍保持显式未支持状态,避免配置层已允许该值而 provider 层默默走错协议。

主要改动

  • 新增 internal/provider/openaicompat/chatcompletions 子包,集中承载 /chat/completions 协议实现。
  • 将原 openaicompat 根包中的请求结构、响应结构、请求组装、SSE 有界读取、流式事件转换、tool-call 增量拼装、usage 提取与 HTTP 错误解析迁移到子包。
  • 收敛 internal/provider/openaicompat/provider.go,使根包只负责:
    • provider 构建
    • HTTP client 与 transport 注入
    • /models discovery
    • api_style 规范化与分流
  • api_style 增加显式分流行为:
    • chat_completionschatcompletions 子包实现
    • responses 返回明确的未支持错误
    • 未知 api_style 直接失败
  • 调整 internal/provider/openaicompat 相关测试,改为覆盖新子包暴露的边界能力,并补充 responses 当前未支持的回归验证。
  • 更新 README 中的模块职责说明,反映 openaicompat 根包与 chatcompletions 子包的新边界。

预期收益

  • openaicompat 的共享职责与子协议职责边界更清晰,避免根包继续膨胀。
  • 为后续 internal/provider/openaicompat/responses 的接入预留稳定落点,降低 Phase F 改造成本。
  • 减少未来继续堆叠 if apiStyle == ... 条件分支带来的协议耦合。
  • 保持当前 /chat/completions 主链可用的同时,让 responses 以显式失败的方式暴露未完成状态,降低排查成本。
  • 让协议细节测试更聚焦,后续维护和回归验证更直接。

@phantom5099

Copy link
Copy Markdown
Collaborator Author

/review -codex

fennoai[bot]

This comment was marked as outdated.

fennoai[bot]

This comment was marked as outdated.

fennoai[bot]

This comment was marked as outdated.

fennoai[bot]

This comment was marked as outdated.

@phantom5099

Copy link
Copy Markdown
Collaborator Author

/review -codex

fennoai[bot]

This comment was marked as outdated.

fennoai[bot]

This comment was marked as outdated.

fennoai[bot]

This comment was marked as outdated.

fennoai[bot]

This comment was marked as outdated.

fennoai[bot]

This comment was marked as outdated.

@phantom5099

Copy link
Copy Markdown
Collaborator Author

/review -codex

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Found 2 noteworthy regressions, both in config loading compatibility.

Comment thread internal/config/loader.go
var file persistedConfig
if err := yaml.Unmarshal(data, &file); err != nil {
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This changes the top-level config loader from the previous "read old keys, then rewrite to the canonical format" behavior into a hard startup failure for any existing config.yaml that still contains legacy keys like workdir, default_workdir, or providers. The updated tests explicitly codify that regression, so users upgrading from the prior build will now have to hand-edit their config before NeoCode can start. If the breaking change is intentional, it needs a migration path or at least a compatibility shim here rather than failing on load.

var file customProviderFile
if err := yaml.Unmarshal(data, &file); err != nil {
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

KnownFields(true) combined with removing openai_compatible.profile makes previously accepted custom provider configs fail to load immediately. The old tests covered profile, and this PR flips that to an error, so existing providers/*/provider.yaml files can now break startup after upgrade even though the field was just being ignored before. This needs either a backward-compatibility allowance for retired fields or an explicit migration step.

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I found 2 regressions in the config-loading changes. Both are user-visible startup/configuration breakages, and the current docs still describe the pre-PR behavior.

Comment thread internal/config/loader.go
@@ -22,15 +22,13 @@ type Loader struct {
}

type persistedConfig struct {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Dropping workdir from persistedConfig removes the only YAML-backed way to configure the default workspace, but Config.Workdir is still validated and then fed into bootstrap, session storage, and tool sandbox setup. After this change a user can no longer persist a non-default workdir in ~/.neocode/config.yaml: old files fail to load, and newly saved files silently omit the setting. That is a behavior regression, not just a cleanup.

Comment thread internal/config/loader.go
var file persistedConfig
if err := yaml.Unmarshal(data, &file); err != nil {
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

KnownFields(true) is now applied after the legacy keys were removed from the persisted schema, so existing installs with providers, provider_overrides, workdir, or default_workdir now hard-fail during startup instead of being rewritten into the current minimal format. The previous loader explicitly preserved that upgrade path, and the docs still say those old keys are cleaned up automatically, so this is a breaking migration regression.

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

发现 2 个需要关注的回归点:配置解析被意外收紧,以及 driver 扩展点被硬编码限制。

Comment thread internal/config/loader.go
var file persistedConfig
if err := yaml.Unmarshal(data, &file); err != nil {
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

KnownFields(true) 把主配置解析从“忽略/重写旧字段”改成了“遇到未知字段直接启动失败”。这会让已经存在的 ~/.neocode/config.yaml 里只要还保留 workdirdefault_workdirprovidersworkspace_root 等历史字段,就在升级后无法启动,而这次 PR 的目标只是 provider 目录重构。这里需要保留迁移/重写路径,或者显式做 config version bump,否则是一次无预警的兼容性回归。

Comment thread internal/config/model.go Outdated
if normalizeProviderDriver(p.Driver) == "openai" {
return fmt.Errorf("provider %q driver %q is no longer supported", p.Name, p.Driver)
switch normalizedDriver {
case provider.DriverOpenAICompat, provider.DriverGemini, provider.DriverAnthropic:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

这里把允许的 driver 固定成 3 个字符串后,ProviderConfig.Validate() 就不再与 provider.Registry 的扩展点一致了。现在即使运行时已经注册了一个新 driver,配置层也会先报 driver ... is not supported,导致新增 in-tree/第三方 driver 根本无法落地。更稳妥的做法是让配置校验依赖实际注册表/支持集,而不是在 config 包里再维护一份硬编码 allowlist。

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I found 2 upgrade regressions in the config-loading path that are worth addressing before this lands.

Comment thread internal/config/loader.go
var file persistedConfig
if err := yaml.Unmarshal(data, &file); err != nil {
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Switching the root config parser to KnownFields(true) turns previously migratable configs into hard startup failures. On main, providers / provider_overrides were accepted and rewritten away during Load(), and the docs still describe that upgrade path. With this change, users carrying an older config.yaml can no longer boot to let the loader normalize it. Please preserve the legacy-key migration path (or rewrite before strict validation) instead of rejecting those files outright.

var file customProviderFile
if err := yaml.Unmarshal(data, &file); err != nil {
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This strict decode also breaks existing custom-provider files that still include openai_compatible.profile. Before this PR, that field was tolerated as a no-op; after removing it from customOpenAICompatibleFile, upgrades now fail during startup with field profile not found. Please keep profile as a deprecated accepted field, or add a migration path, so previously valid provider directories continue to load.

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the diff and ran go test ./.... I found 3 issues worth addressing before merge.

Comment thread internal/config/loader.go
var file persistedConfig
if err := yaml.Unmarshal(data, &file); err != nil {
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Switching the root config parser to KnownFields(true) removes the existing upgrade path for legacy config.yaml keys. On main, providers / provider_overrides were tolerated and rewritten away during Load(), and the docs still describe that cleanup behavior. After this change, older configs become hard startup failures instead of being normalized.

var file customProviderFile
if err := yaml.Unmarshal(data, &file); err != nil {
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This strict decode also breaks previously accepted custom-provider files that still carry retired fields such as openai_compatible.profile. Before this PR that field was a no-op; now startup fails with field profile not found. If the field is being retired, this needs a compatibility shim or migration path rather than an immediate load-time break.

Comment thread internal/config/model.go Outdated
if normalizeProviderDriver(p.Driver) == "openai" {
return fmt.Errorf("provider %q driver %q is no longer supported", p.Name, p.Driver)
switch normalizedDriver {
case provider.DriverOpenAICompat, provider.DriverGemini, provider.DriverAnthropic:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ProviderConfig.Validate() now hardcodes the allowed driver names in config, which breaks the registry-based extension point. A driver can be successfully registered in provider.Registry and still be rejected during config load here with driver ... is not supported. This support check should come from the actual registry/support set instead of a second allowlist in config.

@phantom5099 phantom5099 merged commit 67603b3 into 1024XEngineer:main Apr 10, 2026
2 checks passed
@fennoai fennoai Bot mentioned this pull request Apr 15, 2026
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.

1 participant