fix(custom): support wire = "responses" | "anthropic" for kind="openai-compatible"
Summary / 摘要
Custom provider (kind = "openai-compatible") currently only supports Chat Completions wire. 配置 wire = "responses" / "anthropic" 被忽略,始终走 ChatCompletions,导致需要 Responses 或 Anthropic Messages 的模型(如 muse-spark-1.2 on opencode.ai/zen/v1)无法在 custom provider 下工作。
Custom provider (kind="openai-compatible") only speaks Chat Completions, ignoring wire setting. Models requiring Responses / Anthropic wire (e.g. muse-spark-1.2 via opencode.ai/zen/v1) cannot be used as custom.
Reproduction / 复现
Minimal config.toml:
[providers.myspark]
kind = "openai-compatible"
base_url = "https://opencode.ai/zen/v1"
api_key_env = "OPENCODE_API_KEY" # or OPENCODE_ZEN_API_KEY
model = "muse-spark-1.2"
wire = "responses" # 期望走 OpenAI Responses;改成 "anthropic" / "messages" 同理
# wire = "anthropic"
[providers.myspark2]
kind = "openai-compatible"
base_url = "https://opencode.ai/zen/v1"
api_key_env = "OPENCODE_API_KEY"
model = "muse-spark-1.2"
wire = "anthropic"
Steps:
codewhale --provider myspark (or set as default)
- Send any message /
codewhale doctor --verbose
- Observe request goes to
/v1/chat/completions instead of /responses or /v1/messages
Expected / 期望
wire = "responses" (also response, openai-responses) → WireFormat::Responses, requests hit /responses (OpenAI Responses API, like openai_codex).
wire = "anthropic" / "messages" / "anthropic-messages" → WireFormat::AnthropicMessages, requests hit /v1/messages.
wire omitted / "openai" / "chat" / "chat_completions" → WireFormat::ChatCompletions (default, backward compatible).
- Case-insensitive, trims whitespace.
Actual / 实际
Always WireFormat::ChatCompletions regardless of wire value. 对 opencode.ai/zen/v1 + muse-spark-1.2 这类 Responses-Anthropic hybrid 网关,请求 400/404 或 unsupported wire,因为服务端期望 Responses/Anthropic payload 而收到 Chat Completions schema.
Root cause / 根因
crates/config/src/provider.rs:1651-1653:
impl Provider for Custom {
fn wire_policy(&self) -> WirePolicy {
WirePolicy::Fixed(WireFormat::ChatCompletions) // ← fixed, ignores config
}
}
Custom::wire_policy() is Fixed(ChatCompletions), so WirePolicy::resolve() never consults providers.<name>.wire.
crates/tui/src/config.rs & crates/tui/src/client.rs resolve wire_format via provider.wire_policy() + provider_wire_format_for_config(), but Custom never reaches the ModelAware / wire_config_prefers_anthropic path. providers.<name>.wire is parsed & stored in lib.rs (ProviderConfigToml::wire) yet never read for Custom.
- Other built-ins (e.g.
OpencodeZen → ModelAware, OpenaiCodex → Fixed(Responses), Anthropic → Fixed(AnthropicMessages)) demonstrate the wire abstraction already exists; Custom is the outlier.
Proposed fix / 修复建议
Branch: fix/custom-wire-responses (or main with following changes)
-
crates/config/src/provider.rs — Custom::wire_policy
Make Custom respect wire field: either ModelAware or Fixed with override via wire string. Suggested mapping (reuse WirePolicy::resolve keys):
responses → Responses
anthropic / messages / anthropic_messages / anthropic-messages → AnthropicMessages
chat / chat_completions / openai / empty → ChatCompletions (default)
-
crates/tui/src/config.rs — provider_wire_format_for_config / wire_config_prefers_anthropic
Ensure ApiProvider::Custom path reads config.wire (like dual-wire vendors do). Normalize aliases (wire, wire_format, api_style already aliased in config_document.rs).
-
crates/tui/src/client.rs — dispatch & validation
No wire-specific hardcoding for Custom; rely on resolved wire_format for match self.wire_format { ChatCompletions | Responses | AnthropicMessages } → correct body builder / endpoint. Keep validate_route / limits intact.
Backward compat: 无 wire 时保持 ChatCompletions,不影响现有 custom 用户。
Workaround / 临时绕过
Reuse the existing Responses-fixed built-in provider openai_codex + env override (since Custom is blocked):
[providers.openai_codex]
# base_url/model 留空,由 env 覆盖,避免写死官方 URL
export OPENAI_CODEX_BASE_URL="https://opencode.ai/zen/v1"
export OPENAI_CODEX_API_KEY="$OPENCODE_API_KEY" # or OPENCODE_ZEN_API_KEY
# 可选:指定默认模型
export OPENAI_CODEX_MODEL="muse-spark-1.2"
codewhale --provider openai_codex --model muse-spark-1.2
或直接改 env 启动:
$env:OPENAI_CODEX_BASE_URL="https://opencode.ai/zen/v1"
$env:OPENAI_CODEX_API_KEY=$env:OPENCODE_API_KEY
codewhale --provider openai_codex
原理:OpenaiCodex::wire_policy = Fixed(Responses) 天然走 /responses,绕过 Custom 的 Fixed-Chat 限制。For Anthropic wire, use providers.anthropic similarly if gateway supports it.
缺点:占用 openai_codex 槽位,不能同时配多个 custom Responses 网关;env 覆盖对多路由不友好。
Related files / 相关文件
crates/config/src/provider.rs — Custom struct & wire_policy() (root cause)
crates/tui/src/config.rs — provider_wire_format_for_config(), wire_config_prefers_anthropic(), ProviderConfigToml::wire handling
crates/tui/src/client.rs — provider_default_wire_format(), DeepSeekClient::wire_format dispatch (ChatCompletions / Responses / AnthropicMessages)
crates/config/src/lib.rs — ProviderKind::Custom, ProviderConfigToml, wire field definition
Environment
- CodeWhale:
main @ a0341b5a4 (pre-fix) / branch fix/custom-wire-responses
- Provider:
kind = "openai-compatible" (Custom) → opencode.ai/zen/v1 / muse-spark-1.2
- OS: Windows 10 (also repro on Linux/macOS)
fix(custom): support wire = "responses" | "anthropic" for kind="openai-compatible"
Summary / 摘要
Custom provider (
kind = "openai-compatible") currently only supports Chat Completions wire. 配置wire = "responses"/"anthropic"被忽略,始终走ChatCompletions,导致需要 Responses 或 Anthropic Messages 的模型(如muse-spark-1.2onopencode.ai/zen/v1)无法在 custom provider 下工作。Custom provider (
kind="openai-compatible") only speaks Chat Completions, ignoringwiresetting. Models requiring Responses / Anthropic wire (e.g.muse-spark-1.2viaopencode.ai/zen/v1) cannot be used ascustom.Reproduction / 复现
Minimal
config.toml:Steps:
codewhale --provider myspark(or set as default)codewhale doctor --verbose/v1/chat/completionsinstead of/responsesor/v1/messagesExpected / 期望
wire = "responses"(alsoresponse,openai-responses) →WireFormat::Responses, requests hit/responses(OpenAI Responses API, likeopenai_codex).wire = "anthropic"/"messages"/"anthropic-messages"→WireFormat::AnthropicMessages, requests hit/v1/messages.wireomitted /"openai"/"chat"/"chat_completions"→WireFormat::ChatCompletions(default, backward compatible).Actual / 实际
Always
WireFormat::ChatCompletionsregardless ofwirevalue. 对opencode.ai/zen/v1+muse-spark-1.2这类 Responses-Anthropic hybrid 网关,请求 400/404 或unsupported wire,因为服务端期望 Responses/Anthropic payload 而收到 Chat Completions schema.Root cause / 根因
crates/config/src/provider.rs:1651-1653:Custom::wire_policy()isFixed(ChatCompletions), soWirePolicy::resolve()never consultsproviders.<name>.wire.crates/tui/src/config.rs&crates/tui/src/client.rsresolvewire_formatviaprovider.wire_policy()+provider_wire_format_for_config(), but Custom never reaches theModelAware/wire_config_prefers_anthropicpath.providers.<name>.wireis parsed & stored inlib.rs(ProviderConfigToml::wire) yet never read for Custom.OpencodeZen→ModelAware,OpenaiCodex→Fixed(Responses),Anthropic→Fixed(AnthropicMessages)) demonstrate the wire abstraction already exists; Custom is the outlier.Proposed fix / 修复建议
Branch:
fix/custom-wire-responses(ormainwith following changes)crates/config/src/provider.rs—Custom::wire_policyMake Custom respect
wirefield: eitherModelAwareorFixedwith override viawirestring. Suggested mapping (reuseWirePolicy::resolvekeys):responses→Responsesanthropic/messages/anthropic_messages/anthropic-messages→AnthropicMessageschat/chat_completions/openai/ empty →ChatCompletions(default)crates/tui/src/config.rs—provider_wire_format_for_config/wire_config_prefers_anthropicEnsure
ApiProvider::Custompath readsconfig.wire(like dual-wire vendors do). Normalize aliases (wire,wire_format,api_stylealready aliased inconfig_document.rs).crates/tui/src/client.rs— dispatch & validationNo wire-specific hardcoding for Custom; rely on resolved
wire_formatformatch self.wire_format { ChatCompletions | Responses | AnthropicMessages }→ correct body builder / endpoint. Keepvalidate_route/ limits intact.Backward compat: 无
wire时保持ChatCompletions,不影响现有 custom 用户。Workaround / 临时绕过
Reuse the existing
Responses-fixed built-in provideropenai_codex+ env override (since Custom is blocked):或直接改 env 启动:
原理:
OpenaiCodex::wire_policy = Fixed(Responses)天然走/responses,绕过 Custom 的 Fixed-Chat 限制。For Anthropic wire, useproviders.anthropicsimilarly if gateway supports it.缺点:占用
openai_codex槽位,不能同时配多个 custom Responses 网关;env 覆盖对多路由不友好。Related files / 相关文件
crates/config/src/provider.rs—Customstruct &wire_policy()(root cause)crates/tui/src/config.rs—provider_wire_format_for_config(),wire_config_prefers_anthropic(),ProviderConfigToml::wirehandlingcrates/tui/src/client.rs—provider_default_wire_format(),DeepSeekClient::wire_formatdispatch (ChatCompletions/Responses/AnthropicMessages)crates/config/src/lib.rs—ProviderKind::Custom,ProviderConfigToml,wirefield definitionEnvironment
main@a0341b5a4(pre-fix) / branchfix/custom-wire-responseskind = "openai-compatible"(Custom) →opencode.ai/zen/v1/muse-spark-1.2