mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 17:09:13 -10:00
fix(custom): support wire = "responses" | "anthropic" | "chat" for kind="openai-compatible"
Custom provider was fixed to ChatCompletions, ignoring providers.<name>.wire. Now honors per-config wire in both client::provider_wire_format_for_config and config::provider_capability, keeping Custom::wire_policy default as Chat for compat. Aliases: responses/openai-responses/responses-api -> Responses; anthropic/messages/claude -> AnthropicMessages; default -> Chat. Fixes custom muse-spark-1.2 on opencode.ai/zen/v1 needing Responses. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
1 parent
9cf3243285
commit
ff504585a4
3 files changed
+120
-9
No files matched your search
@@ -1650,6 +1650,13 @@ impl Provider for Custom {
|
||||
}
|
||||
|
||||
fn wire_policy(&self) -> WirePolicy {
|
||||
// Static default remains Chat Completions for backward compatibility.
|
||||
// Per-config `wire = "responses" | "anthropic" | "chat"` overrides are
|
||||
// honored in `crates/tui/src/client.rs::provider_wire_format_for_config`
|
||||
// and `crates/tui/src/config.rs::provider_capability`, which read
|
||||
// `ProviderConfig::wire` for the `Custom` catalog identity. This keeps
|
||||
// the `Provider` trait `Fixed` while giving custom endpoints the same
|
||||
// three-way switch (`responses` / `anthropic` / `chat`) as built-ins.
|
||||
WirePolicy::Fixed(WireFormat::ChatCompletions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1195,12 +1195,15 @@ impl DeepSeekClient {
|
||||
if api_provider == ApiProvider::OpencodeGo {
|
||||
validate_route(api_provider, &default_model).map_err(anyhow::Error::msg)?;
|
||||
}
|
||||
let (api_key, codex_account_id) = if api_provider == ApiProvider::OpenaiCodex {
|
||||
let credentials = config.codex_credentials()?;
|
||||
(credentials.access_token, credentials.account_id)
|
||||
} else {
|
||||
(config.deepseek_api_key()?, None)
|
||||
};
|
||||
let (api_key, codex_account_id) =
|
||||
if api_provider == ApiProvider::OpenaiCodex
|
||||
&& !config.provider_uses_custom_endpoint(ApiProvider::OpenaiCodex)
|
||||
{
|
||||
let credentials = config.codex_credentials()?;
|
||||
(credentials.access_token, credentials.account_id)
|
||||
} else {
|
||||
(config.deepseek_api_key()?, None)
|
||||
};
|
||||
let model_bound_secret_values =
|
||||
Arc::new(configured_model_bound_secret_values(config, &api_key));
|
||||
validate_base_url_security(&base_url)?;
|
||||
@@ -1687,9 +1690,11 @@ fn provider_default_wire_format(api_provider: ApiProvider) -> WireFormat {
|
||||
|
||||
/// Resolve the wire dialect for a dual-protocol vendor.
|
||||
///
|
||||
/// Power-user toggle: `providers.<id>.wire = "openai" | "anthropic"`.
|
||||
/// Legacy dialect kinds (`*Anthropic`) still force Messages. Everyone else
|
||||
/// keeps the descriptor's fixed policy (or Chat Completions).
|
||||
/// Power-user toggle: `providers.<id>.wire = "openai" | "anthropic" | "responses"`.
|
||||
/// Legacy dialect kinds (`*Anthropic`) still force Messages. Custom providers
|
||||
/// honor `wire = "responses" | "anthropic" | "chat"` per-config (see
|
||||
/// `crates/config/src/provider.rs:Custom`). Everyone else keeps the descriptor's
|
||||
/// fixed policy (or Chat Completions).
|
||||
fn provider_wire_format_for_config(
|
||||
api_provider: ApiProvider,
|
||||
config: Option<&crate::config::Config>,
|
||||
@@ -1722,6 +1727,22 @@ fn provider_wire_format_for_config(
|
||||
return WireFormat::AnthropicMessages;
|
||||
}
|
||||
|
||||
// Custom providers honor `wire = "anthropic"` / `wire = "responses"` explicitly.
|
||||
// The static `Custom::wire_policy()` remains `Chat` as a safe default; the
|
||||
// per-config override lives here (and in `provider_capability`) so existing
|
||||
// `[providers.<name>]` tables gain the three-way switch without changing the
|
||||
// provider registry trait. Supported aliases:
|
||||
// anthropic: "anthropic" | "messages" | "claude" | "anthropic-messages" | ...
|
||||
// responses: "responses" | "responses-api" | "openai-responses" | "openai_responses" | ...
|
||||
if api_provider == ApiProvider::Custom {
|
||||
if wire_config_prefers_anthropic(wire) {
|
||||
return WireFormat::AnthropicMessages;
|
||||
}
|
||||
if wire_config_prefers_responses(wire) {
|
||||
return WireFormat::Responses;
|
||||
}
|
||||
}
|
||||
|
||||
api_provider
|
||||
.kind()
|
||||
.and_then(|kind| {
|
||||
@@ -1754,6 +1775,24 @@ fn wire_config_prefers_anthropic(wire: Option<&str>) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn wire_config_prefers_responses(wire: Option<&str>) -> bool {
|
||||
let Some(raw) = wire.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return false;
|
||||
};
|
||||
let normalized = raw.to_ascii_lowercase().replace(['_', ' '], "-");
|
||||
matches!(
|
||||
normalized.as_str(),
|
||||
"responses"
|
||||
| "responses-api"
|
||||
| "openai-responses"
|
||||
| "openai-responses-api"
|
||||
| "response"
|
||||
| "response-api"
|
||||
| "openai-responses-compat"
|
||||
| "responses-compat"
|
||||
) || normalized.contains("responses")
|
||||
}
|
||||
|
||||
fn api_provider_skips_models_probe(api_provider: ApiProvider) -> bool {
|
||||
matches!(api_provider, ApiProvider::DeepseekAnthropic)
|
||||
}
|
||||
|
||||
@@ -628,6 +628,53 @@ pub enum RequestPayloadMode {
|
||||
/// in the API payload (after normalization / provider-specific mapping).
|
||||
#[must_use]
|
||||
pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> ProviderCapability {
|
||||
provider_capability_with_wire(provider, resolved_model, None)
|
||||
}
|
||||
|
||||
/// Wire-aware variant of [`provider_capability`] that respects
|
||||
/// `wire = "responses" | "anthropic" | "chat"` for `Custom` providers.
|
||||
///
|
||||
/// Built-ins keep their fixed policy; `Custom` defaults to `Chat` when `wire`
|
||||
/// is absent so existing configs stay compatible. Mirrors
|
||||
/// `crates/tui/src/client.rs::provider_wire_format_for_config` and the
|
||||
/// `Custom` comment in `crates/config/src/provider.rs`.
|
||||
#[must_use]
|
||||
pub fn provider_capability_with_wire(
|
||||
provider: ApiProvider,
|
||||
resolved_model: &str,
|
||||
wire: Option<&str>,
|
||||
) -> ProviderCapability {
|
||||
// Custom wire overrides must be checked before the generic fallback so
|
||||
// `[providers.<name>] wire = "responses"` / `"anthropic"` is honored.
|
||||
if provider == ApiProvider::Custom {
|
||||
if wire_config_prefers_anthropic(wire) {
|
||||
return ProviderCapability {
|
||||
provider,
|
||||
resolved_model: resolved_model.to_string(),
|
||||
context_window: crate::models::context_window_for_model(resolved_model)
|
||||
.unwrap_or(crate::models::LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS),
|
||||
max_output: crate::models::max_output_tokens_for_model(resolved_model),
|
||||
thinking_supported: crate::models::model_supports_reasoning(resolved_model),
|
||||
cache_telemetry_supported: false,
|
||||
request_payload_mode: RequestPayloadMode::AnthropicMessages,
|
||||
alias_deprecation: None,
|
||||
};
|
||||
}
|
||||
if wire_config_prefers_responses(wire) {
|
||||
return ProviderCapability {
|
||||
provider,
|
||||
resolved_model: resolved_model.to_string(),
|
||||
context_window: crate::models::context_window_for_model(resolved_model)
|
||||
.unwrap_or(crate::models::LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS),
|
||||
max_output: crate::models::max_output_tokens_for_model(resolved_model),
|
||||
thinking_supported: crate::models::model_supports_reasoning(resolved_model),
|
||||
cache_telemetry_supported: false,
|
||||
request_payload_mode: RequestPayloadMode::Responses,
|
||||
alias_deprecation: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
provider,
|
||||
ApiProvider::Anthropic | ApiProvider::MinimaxAnthropic | ApiProvider::Openmodel
|
||||
@@ -9383,6 +9430,24 @@ fn wire_config_prefers_anthropic(wire: Option<&str>) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn wire_config_prefers_responses(wire: Option<&str>) -> bool {
|
||||
let Some(raw) = wire.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return false;
|
||||
};
|
||||
let normalized = raw.to_ascii_lowercase().replace(['_', ' '], "-");
|
||||
matches!(
|
||||
normalized.as_str(),
|
||||
"responses"
|
||||
| "responses-api"
|
||||
| "openai-responses"
|
||||
| "openai-responses-api"
|
||||
| "response"
|
||||
| "response-api"
|
||||
| "openai-responses-compat"
|
||||
| "responses-compat"
|
||||
) || normalized.contains("responses")
|
||||
}
|
||||
|
||||
fn modelstudio_mode_is_coding_plan(provider: ApiProvider, mode: Option<&str>) -> bool {
|
||||
if matches!(
|
||||
provider,
|
||||
|
||||
Reference in new issue
Block a user