Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/build-windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Build Windows

on:
push:
branches: [main]
workflow_dispatch: {}

permissions:
contents: read
actions: write

concurrency:
group: build-windows-${{ github.ref }}
cancel-in-progress: true

jobs:
build-windows:
name: Build Windows x64
runs-on: windows-latest
steps:
- uses: actions/checkout@v7

- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
targets: x86_64-pc-windows-msvc

- uses: mozilla-actions/sccache-action@v0.0.10
- name: Enable sccache
shell: bash
run: |
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"

- uses: Swatinem/rust-cache@v2
with:
cache-bin: false

- name: Build
shell: bash
run: cargo build --release --locked --target x86_64-pc-windows-msvc -p codewhale-cli -p codewhale-tui

- name: Stage binaries
shell: bash
run: |
mkdir -p artifacts
cp target/x86_64-pc-windows-msvc/release/codewhale.exe artifacts/
cp target/x86_64-pc-windows-msvc/release/codewhale-tui.exe artifacts/
cp target/x86_64-pc-windows-msvc/release/codew.exe artifacts/ 2>/dev/null || true

- uses: actions/upload-artifact@v7
with:
name: codewhale-windows-x64
path: artifacts/*
if-no-files-found: error
7 changes: 7 additions & 0 deletions crates/config/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1649,6 +1649,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)
}
}
Expand Down
57 changes: 48 additions & 9 deletions crates/tui/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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| {
Expand Down Expand Up @@ -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)
}
Expand Down
65 changes: 65 additions & 0 deletions crates/tui/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading