mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-09 16:00:35 +00:00
bedrock: Add native support for Bedrock Mantle models (#60480)
Adds native support for AWS Bedrock's Mantle endpoint (`bedrock-mantle`), which serves models with no `Converse`/`Invoke` support on `bedrock-runtime`, such as GPT-5.5, GPT-5.4, and Grok 4.3 but more importantly **open-weight** models Closes #60471 ## What's changed - Renamed the existing `Model` enum in the `bedrock` crate to `ConverseModel`, and added a new `MantleModel` enum for Mantle-only models. Mantle models reuse the existing OpenAI-compatible Chat Completions/Responses request and response plumbing (`into_open_ai`/`into_open_ai_response`, `OpenAiEventMapper`/`OpenAiResponseEventMapper`) already used by the native OpenAI and OpenAI-compatible providers, rather than introducing new marshalling code. - Added a `BedrockMantleModel` language model that routes requests to the `bedrock-mantle` endpoint, dispatching to Chat Completions or the Responses API depending on the model. Mantle models appear in the model picker alongside Converse models under the same Bedrock provider. - Added region gating: `bedrock-mantle` is only available in a subset of AWS Regions, so using a Mantle model outside of them surfaces a clear error naming the current Region and the supported ones, instead of an opaque HTTP failure. - Implemented Bedrock bearer token authentication for Mantle requests: a configured Bedrock API key is used as-is, and every other auth method (IAM credentials, named profile, SSO, automatic) derives a short-term token by locally SigV4-presigning a `CallWithBearerToken` request. This requires no extra network round trip and no token caching, since re-signing locally is cheap. - Added a specific error for the 403 you get when your credentials have `bedrock:CallWithBearerToken` but not the separate `bedrock-mantle:CallWithBearerToken` permission Mantle models require, since this is the most common misconfiguration. - Added a `mantle_available_models` setting so custom models served through `bedrock-mantle` can be configured, the same way other providers support custom models via `available_models`. - Documented Mantle models and the new setting in the Amazon Bedrock section of [Use a Gateway](https://zed.dev/docs/ai/use-a-gateway#amazon-bedrock). ## Testing - Added unit tests covering: the local SigV4 bearer-token signing (including a byte-for-byte cross-check against a reference implementation), Mantle endpoint URL construction, the Mantle-supported-regions list, thinking-effort normalization, and the settings-to-model protocol mapping. - `cargo test -p bedrock -p language_models -p settings_content -p settings` passes. - `./script/clippy` passes with no new warnings. Release Notes: - Added native support for AWS Bedrock's Mantle endpoint, enabling GPT-5.5, GPT-5.4, and Grok 4.3 through the Amazon Bedrock provider.
This commit is contained in:
parent
5d7d1d3f09
commit
d12b980ee0
10 changed files with 1250 additions and 137 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -9811,6 +9811,7 @@ dependencies = [
|
|||
"async-lock",
|
||||
"aws-config",
|
||||
"aws-credential-types",
|
||||
"aws-sigv4",
|
||||
"aws_http_client",
|
||||
"base64 0.22.1",
|
||||
"bedrock",
|
||||
|
|
|
|||
|
|
@ -543,6 +543,7 @@ aws-credential-types = { version = "1.2.8", features = [
|
|||
aws-sdk-bedrockruntime = { version = "1.112.0", features = [
|
||||
"behavior-version-latest",
|
||||
] }
|
||||
aws-sigv4 = { version = "1.4.0", features = ["http1"] }
|
||||
aws-smithy-runtime-api = { version = "1.9.2", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.3.4", features = ["http-body-1-x"] }
|
||||
backtrace = "0.3"
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ pub struct BedrockModelCacheConfiguration {
|
|||
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
|
||||
pub enum Model {
|
||||
pub enum ConverseModel {
|
||||
// Anthropic Claude 4+ models
|
||||
#[serde(
|
||||
rename = "claude-fable-5",
|
||||
|
|
@ -227,7 +227,7 @@ pub enum Model {
|
|||
},
|
||||
}
|
||||
|
||||
impl Model {
|
||||
impl ConverseModel {
|
||||
pub fn default_fast(_region: &str) -> Self {
|
||||
Self::ClaudeHaiku4_5
|
||||
}
|
||||
|
|
@ -817,34 +817,162 @@ impl Model {
|
|||
}
|
||||
}
|
||||
|
||||
/// The wire protocol used to talk to a [`MantleModel`] on the `bedrock-mantle` endpoint.
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
|
||||
pub enum MantleProtocol {
|
||||
/// The OpenAI Chat Completions API (`/chat/completions`).
|
||||
#[default]
|
||||
ChatCompletions,
|
||||
/// The OpenAI Responses API (`/responses`).
|
||||
Responses,
|
||||
}
|
||||
|
||||
/// Models only reachable through the `bedrock-mantle` endpoint's
|
||||
/// OpenAI-compatible APIs, i.e. with no `Converse`/`Invoke` support on
|
||||
/// `bedrock-runtime`.
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, EnumIter)]
|
||||
pub enum MantleModel {
|
||||
#[serde(rename = "gpt-5.5")]
|
||||
Gpt5_5,
|
||||
#[serde(rename = "gpt-5.4")]
|
||||
Gpt5_4,
|
||||
#[serde(rename = "grok-4.3")]
|
||||
Grok4_3,
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
name: String,
|
||||
display_name: Option<String>,
|
||||
max_tokens: u64,
|
||||
max_output_tokens: Option<u64>,
|
||||
protocol: MantleProtocol,
|
||||
supports_tools: bool,
|
||||
supports_images: bool,
|
||||
supports_thinking: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl MantleModel {
|
||||
/// The model id Zed uses internally (also used as the `name` in settings).
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Self::Gpt5_5 => "gpt-5.5",
|
||||
Self::Gpt5_4 => "gpt-5.4",
|
||||
Self::Grok4_3 => "grok-4.3",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
/// The model id as expected in Bedrock Mantle request bodies, e.g. `openai.gpt-5.5`.
|
||||
pub fn request_id(&self) -> &str {
|
||||
match self {
|
||||
Self::Gpt5_5 => "openai.gpt-5.5",
|
||||
Self::Gpt5_4 => "openai.gpt-5.4",
|
||||
Self::Grok4_3 => "xai.grok-4.3",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> &str {
|
||||
match self {
|
||||
Self::Gpt5_5 => "GPT-5.5",
|
||||
Self::Gpt5_4 => "GPT-5.4",
|
||||
Self::Grok4_3 => "Grok 4.3",
|
||||
Self::Custom {
|
||||
display_name, name, ..
|
||||
} => display_name.as_deref().unwrap_or(name.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Which OpenAI-compatible API this model must be called through.
|
||||
pub fn protocol(&self) -> MantleProtocol {
|
||||
match self {
|
||||
Self::Gpt5_5 | Self::Gpt5_4 | Self::Grok4_3 => MantleProtocol::Responses,
|
||||
Self::Custom { protocol, .. } => *protocol,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_token_count(&self) -> u64 {
|
||||
match self {
|
||||
Self::Gpt5_5 | Self::Gpt5_4 => 272_000,
|
||||
Self::Grok4_3 => 1_000_000,
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_output_tokens(&self) -> u64 {
|
||||
match self {
|
||||
// AWS doesn't document a hard cap for GPT-5.5/5.4 on Mantle.
|
||||
Self::Gpt5_5 | Self::Gpt5_4 => 128_000,
|
||||
Self::Grok4_3 => 131_072,
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
} => max_output_tokens.unwrap_or(4_096),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supports_tools(&self) -> bool {
|
||||
match self {
|
||||
Self::Gpt5_5 | Self::Gpt5_4 | Self::Grok4_3 => true,
|
||||
Self::Custom { supports_tools, .. } => *supports_tools,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supports_images(&self) -> bool {
|
||||
match self {
|
||||
Self::Gpt5_5 | Self::Gpt5_4 | Self::Grok4_3 => true,
|
||||
Self::Custom {
|
||||
supports_images, ..
|
||||
} => *supports_images,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supports_thinking(&self) -> bool {
|
||||
match self {
|
||||
Self::Gpt5_5 | Self::Gpt5_4 | Self::Grok4_3 => true,
|
||||
Self::Custom {
|
||||
supports_thinking, ..
|
||||
} => *supports_thinking,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_builtin_mantle_models_use_responses_protocol() {
|
||||
assert_eq!(MantleModel::Gpt5_5.protocol(), MantleProtocol::Responses);
|
||||
assert_eq!(MantleModel::Gpt5_4.protocol(), MantleProtocol::Responses);
|
||||
assert_eq!(MantleModel::Grok4_3.protocol(), MantleProtocol::Responses);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_us_region_inference_ids() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4_5.cross_region_inference_id("us-east-1", false)?,
|
||||
ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("us-east-1", false)?,
|
||||
"us.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4.cross_region_inference_id("us-west-2", false)?,
|
||||
ConverseModel::ClaudeSonnet4.cross_region_inference_id("us-west-2", false)?,
|
||||
"us.anthropic.claude-sonnet-4-20250514-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeFable5.cross_region_inference_id("us-east-1", false)?,
|
||||
ConverseModel::ClaudeFable5.cross_region_inference_id("us-east-1", false)?,
|
||||
"us.anthropic.claude-fable-5"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet5.cross_region_inference_id("us-east-1", false)?,
|
||||
ConverseModel::ClaudeSonnet5.cross_region_inference_id("us-east-1", false)?,
|
||||
"us.anthropic.claude-sonnet-5"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::NovaPro.cross_region_inference_id("us-east-2", false)?,
|
||||
ConverseModel::NovaPro.cross_region_inference_id("us-east-2", false)?,
|
||||
"us.amazon.nova-pro-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::DeepSeekR1.cross_region_inference_id("us-east-1", false)?,
|
||||
ConverseModel::DeepSeekR1.cross_region_inference_id("us-east-1", false)?,
|
||||
"us.deepseek.r1-v1:0"
|
||||
);
|
||||
Ok(())
|
||||
|
|
@ -853,27 +981,27 @@ mod tests {
|
|||
#[test]
|
||||
fn test_eu_region_inference_ids() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4.cross_region_inference_id("eu-west-1", false)?,
|
||||
ConverseModel::ClaudeSonnet4.cross_region_inference_id("eu-west-1", false)?,
|
||||
"eu.anthropic.claude-sonnet-4-20250514-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1", false)?,
|
||||
ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1", false)?,
|
||||
"eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::NovaLite.cross_region_inference_id("eu-north-1", false)?,
|
||||
ConverseModel::NovaLite.cross_region_inference_id("eu-north-1", false)?,
|
||||
"eu.amazon.nova-lite-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeOpus4_6.cross_region_inference_id("eu-west-1", false)?,
|
||||
ConverseModel::ClaudeOpus4_6.cross_region_inference_id("eu-west-1", false)?,
|
||||
"eu.anthropic.claude-opus-4-6-v1"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeOpus4_7.cross_region_inference_id("eu-west-1", false)?,
|
||||
ConverseModel::ClaudeOpus4_7.cross_region_inference_id("eu-west-1", false)?,
|
||||
"eu.anthropic.claude-opus-4-7"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeOpus4_8.cross_region_inference_id("eu-west-1", false)?,
|
||||
ConverseModel::ClaudeOpus4_8.cross_region_inference_id("eu-west-1", false)?,
|
||||
"eu.anthropic.claude-opus-4-8"
|
||||
);
|
||||
Ok(())
|
||||
|
|
@ -882,19 +1010,19 @@ mod tests {
|
|||
#[test]
|
||||
fn test_inference_profile_only_models_fall_back_to_global() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
Model::ClaudeFable5.cross_region_inference_id("eu-west-1", false)?,
|
||||
ConverseModel::ClaudeFable5.cross_region_inference_id("eu-west-1", false)?,
|
||||
"global.anthropic.claude-fable-5"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet5.cross_region_inference_id("eu-west-1", false)?,
|
||||
ConverseModel::ClaudeSonnet5.cross_region_inference_id("eu-west-1", false)?,
|
||||
"global.anthropic.claude-sonnet-5"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeFable5.cross_region_inference_id("ap-southeast-2", false)?,
|
||||
ConverseModel::ClaudeFable5.cross_region_inference_id("ap-southeast-2", false)?,
|
||||
"global.anthropic.claude-fable-5"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet5.cross_region_inference_id("ap-northeast-1", false)?,
|
||||
ConverseModel::ClaudeSonnet5.cross_region_inference_id("ap-northeast-1", false)?,
|
||||
"global.anthropic.claude-sonnet-5"
|
||||
);
|
||||
Ok(())
|
||||
|
|
@ -903,11 +1031,11 @@ mod tests {
|
|||
#[test]
|
||||
fn test_apac_region_inference_ids() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4_5.cross_region_inference_id("ap-south-1", false)?,
|
||||
ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("ap-south-1", false)?,
|
||||
"apac.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::NovaLite.cross_region_inference_id("ap-south-1", false)?,
|
||||
ConverseModel::NovaLite.cross_region_inference_id("ap-south-1", false)?,
|
||||
"apac.amazon.nova-lite-v1:0"
|
||||
);
|
||||
Ok(())
|
||||
|
|
@ -916,23 +1044,23 @@ mod tests {
|
|||
#[test]
|
||||
fn test_au_region_inference_ids() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
Model::ClaudeHaiku4_5.cross_region_inference_id("ap-southeast-2", false)?,
|
||||
ConverseModel::ClaudeHaiku4_5.cross_region_inference_id("ap-southeast-2", false)?,
|
||||
"au.anthropic.claude-haiku-4-5-20251001-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4_5.cross_region_inference_id("ap-southeast-4", false)?,
|
||||
ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("ap-southeast-4", false)?,
|
||||
"au.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeOpus4_6.cross_region_inference_id("ap-southeast-2", false)?,
|
||||
ConverseModel::ClaudeOpus4_6.cross_region_inference_id("ap-southeast-2", false)?,
|
||||
"au.anthropic.claude-opus-4-6-v1"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeOpus4_7.cross_region_inference_id("ap-southeast-2", false)?,
|
||||
ConverseModel::ClaudeOpus4_7.cross_region_inference_id("ap-southeast-2", false)?,
|
||||
"au.anthropic.claude-opus-4-7"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeOpus4_8.cross_region_inference_id("ap-southeast-2", false)?,
|
||||
ConverseModel::ClaudeOpus4_8.cross_region_inference_id("ap-southeast-2", false)?,
|
||||
"au.anthropic.claude-opus-4-8"
|
||||
);
|
||||
Ok(())
|
||||
|
|
@ -941,15 +1069,15 @@ mod tests {
|
|||
#[test]
|
||||
fn test_jp_region_inference_ids() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
Model::ClaudeHaiku4_5.cross_region_inference_id("ap-northeast-1", false)?,
|
||||
ConverseModel::ClaudeHaiku4_5.cross_region_inference_id("ap-northeast-1", false)?,
|
||||
"jp.anthropic.claude-haiku-4-5-20251001-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4_5.cross_region_inference_id("ap-northeast-3", false)?,
|
||||
ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("ap-northeast-3", false)?,
|
||||
"jp.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::Nova2Lite.cross_region_inference_id("ap-northeast-1", false)?,
|
||||
ConverseModel::Nova2Lite.cross_region_inference_id("ap-northeast-1", false)?,
|
||||
"jp.amazon.nova-2-lite-v1:0"
|
||||
);
|
||||
Ok(())
|
||||
|
|
@ -958,7 +1086,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_ca_region_inference_ids() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
Model::NovaLite.cross_region_inference_id("ca-central-1", false)?,
|
||||
ConverseModel::NovaLite.cross_region_inference_id("ca-central-1", false)?,
|
||||
"ca.amazon.nova-lite-v1:0"
|
||||
);
|
||||
Ok(())
|
||||
|
|
@ -967,11 +1095,11 @@ mod tests {
|
|||
#[test]
|
||||
fn test_gov_region_inference_ids() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4_5.cross_region_inference_id("us-gov-east-1", false)?,
|
||||
ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("us-gov-east-1", false)?,
|
||||
"us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4_5.cross_region_inference_id("us-gov-west-1", false)?,
|
||||
ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("us-gov-west-1", false)?,
|
||||
"us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
);
|
||||
Ok(())
|
||||
|
|
@ -980,45 +1108,45 @@ mod tests {
|
|||
#[test]
|
||||
fn test_global_inference_ids() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4.cross_region_inference_id("us-east-1", true)?,
|
||||
ConverseModel::ClaudeSonnet4.cross_region_inference_id("us-east-1", true)?,
|
||||
"global.anthropic.claude-sonnet-4-20250514-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1", true)?,
|
||||
ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1", true)?,
|
||||
"global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeHaiku4_5.cross_region_inference_id("ap-south-1", true)?,
|
||||
ConverseModel::ClaudeHaiku4_5.cross_region_inference_id("ap-south-1", true)?,
|
||||
"global.anthropic.claude-haiku-4-5-20251001-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeOpus4_6.cross_region_inference_id("us-east-1", true)?,
|
||||
ConverseModel::ClaudeOpus4_6.cross_region_inference_id("us-east-1", true)?,
|
||||
"global.anthropic.claude-opus-4-6-v1"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeOpus4_7.cross_region_inference_id("us-east-1", true)?,
|
||||
ConverseModel::ClaudeOpus4_7.cross_region_inference_id("us-east-1", true)?,
|
||||
"global.anthropic.claude-opus-4-7"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeOpus4_8.cross_region_inference_id("us-east-1", true)?,
|
||||
ConverseModel::ClaudeOpus4_8.cross_region_inference_id("us-east-1", true)?,
|
||||
"global.anthropic.claude-opus-4-8"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeFable5.cross_region_inference_id("us-east-1", true)?,
|
||||
ConverseModel::ClaudeFable5.cross_region_inference_id("us-east-1", true)?,
|
||||
"global.anthropic.claude-fable-5"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet5.cross_region_inference_id("us-east-1", true)?,
|
||||
ConverseModel::ClaudeSonnet5.cross_region_inference_id("us-east-1", true)?,
|
||||
"global.anthropic.claude-sonnet-5"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::Nova2Lite.cross_region_inference_id("us-east-1", true)?,
|
||||
ConverseModel::Nova2Lite.cross_region_inference_id("us-east-1", true)?,
|
||||
"global.amazon.nova-2-lite-v1:0"
|
||||
);
|
||||
|
||||
// Models without global support fall back to regional
|
||||
assert_eq!(
|
||||
Model::NovaPro.cross_region_inference_id("us-east-1", true)?,
|
||||
ConverseModel::NovaPro.cross_region_inference_id("us-east-1", true)?,
|
||||
"us.amazon.nova-pro-v1:0"
|
||||
);
|
||||
Ok(())
|
||||
|
|
@ -1028,27 +1156,27 @@ mod tests {
|
|||
fn test_models_without_cross_region() -> anyhow::Result<()> {
|
||||
// Models without cross-region support return their request_id directly
|
||||
assert_eq!(
|
||||
Model::Gemma3_4B.cross_region_inference_id("us-east-1", false)?,
|
||||
ConverseModel::Gemma3_4B.cross_region_inference_id("us-east-1", false)?,
|
||||
"google.gemma-3-4b-it"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::MistralLarge3.cross_region_inference_id("eu-west-1", false)?,
|
||||
ConverseModel::MistralLarge3.cross_region_inference_id("eu-west-1", false)?,
|
||||
"mistral.mistral-large-3-675b-instruct"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::Qwen3VL235B.cross_region_inference_id("ap-south-1", false)?,
|
||||
ConverseModel::Qwen3VL235B.cross_region_inference_id("ap-south-1", false)?,
|
||||
"qwen.qwen3-vl-235b-a22b"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::GptOss120B.cross_region_inference_id("us-east-1", false)?,
|
||||
ConverseModel::GptOss120B.cross_region_inference_id("us-east-1", false)?,
|
||||
"openai.gpt-oss-120b-1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::MiniMaxM2.cross_region_inference_id("us-east-1", false)?,
|
||||
ConverseModel::MiniMaxM2.cross_region_inference_id("us-east-1", false)?,
|
||||
"minimax.minimax-m2"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::KimiK2Thinking.cross_region_inference_id("us-east-1", false)?,
|
||||
ConverseModel::KimiK2Thinking.cross_region_inference_id("us-east-1", false)?,
|
||||
"moonshot.kimi-k2-thinking"
|
||||
);
|
||||
Ok(())
|
||||
|
|
@ -1056,7 +1184,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_custom_model_inference_ids() -> anyhow::Result<()> {
|
||||
let custom_model = Model::Custom {
|
||||
let custom_model = ConverseModel::Custom {
|
||||
name: "custom.my-model-v1:0".to_string(),
|
||||
max_tokens: 100000,
|
||||
display_name: Some("My Custom Model".to_string()),
|
||||
|
|
@ -1078,78 +1206,90 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_friendly_id_vs_request_id() {
|
||||
assert_eq!(Model::ClaudeSonnet4_5.id(), "claude-sonnet-4-5");
|
||||
assert_eq!(Model::NovaLite.id(), "nova-lite");
|
||||
assert_eq!(Model::DeepSeekR1.id(), "deepseek-r1");
|
||||
assert_eq!(Model::Llama4Scout17B.id(), "llama-4-scout-17b");
|
||||
assert_eq!(Model::ClaudeFable5.id(), "claude-fable-5");
|
||||
assert_eq!(Model::ClaudeSonnet5.id(), "claude-sonnet-5");
|
||||
assert_eq!(ConverseModel::ClaudeSonnet4_5.id(), "claude-sonnet-4-5");
|
||||
assert_eq!(ConverseModel::NovaLite.id(), "nova-lite");
|
||||
assert_eq!(ConverseModel::DeepSeekR1.id(), "deepseek-r1");
|
||||
assert_eq!(ConverseModel::Llama4Scout17B.id(), "llama-4-scout-17b");
|
||||
assert_eq!(ConverseModel::ClaudeFable5.id(), "claude-fable-5");
|
||||
assert_eq!(ConverseModel::ClaudeSonnet5.id(), "claude-sonnet-5");
|
||||
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4_5.request_id(),
|
||||
ConverseModel::ClaudeSonnet4_5.request_id(),
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
);
|
||||
assert_eq!(Model::NovaLite.request_id(), "amazon.nova-lite-v1:0");
|
||||
assert_eq!(Model::DeepSeekR1.request_id(), "deepseek.r1-v1:0");
|
||||
assert_eq!(
|
||||
Model::Llama4Scout17B.request_id(),
|
||||
ConverseModel::NovaLite.request_id(),
|
||||
"amazon.nova-lite-v1:0"
|
||||
);
|
||||
assert_eq!(ConverseModel::DeepSeekR1.request_id(), "deepseek.r1-v1:0");
|
||||
assert_eq!(
|
||||
ConverseModel::Llama4Scout17B.request_id(),
|
||||
"meta.llama4-scout-17b-instruct-v1:0"
|
||||
);
|
||||
assert_eq!(Model::ClaudeFable5.request_id(), "anthropic.claude-fable-5");
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet5.request_id(),
|
||||
ConverseModel::ClaudeFable5.request_id(),
|
||||
"anthropic.claude-fable-5"
|
||||
);
|
||||
assert_eq!(
|
||||
ConverseModel::ClaudeSonnet5.request_id(),
|
||||
"anthropic.claude-sonnet-5"
|
||||
);
|
||||
|
||||
// Thinking aliases deserialize to the same model
|
||||
assert_eq!(Model::ClaudeSonnet4.id(), "claude-sonnet-4");
|
||||
assert_eq!(ConverseModel::ClaudeSonnet4.id(), "claude-sonnet-4");
|
||||
assert_eq!(
|
||||
Model::from_id("claude-sonnet-4-thinking").unwrap().id(),
|
||||
ConverseModel::from_id("claude-sonnet-4-thinking")
|
||||
.unwrap()
|
||||
.id(),
|
||||
"claude-sonnet-4"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::from_id("claude-fable-5-thinking").unwrap().id(),
|
||||
ConverseModel::from_id("claude-fable-5-thinking")
|
||||
.unwrap()
|
||||
.id(),
|
||||
"claude-fable-5"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::from_id("claude-sonnet-5-thinking").unwrap().id(),
|
||||
ConverseModel::from_id("claude-sonnet-5-thinking")
|
||||
.unwrap()
|
||||
.id(),
|
||||
"claude-sonnet-5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_thinking_modes() {
|
||||
assert!(Model::ClaudeHaiku4_5.supports_thinking());
|
||||
assert!(Model::ClaudeSonnet4.supports_thinking());
|
||||
assert!(Model::ClaudeSonnet4_5.supports_thinking());
|
||||
assert!(Model::ClaudeOpus4_6.supports_thinking());
|
||||
assert!(Model::ClaudeFable5.supports_thinking());
|
||||
assert!(ConverseModel::ClaudeHaiku4_5.supports_thinking());
|
||||
assert!(ConverseModel::ClaudeSonnet4.supports_thinking());
|
||||
assert!(ConverseModel::ClaudeSonnet4_5.supports_thinking());
|
||||
assert!(ConverseModel::ClaudeOpus4_6.supports_thinking());
|
||||
assert!(ConverseModel::ClaudeFable5.supports_thinking());
|
||||
|
||||
assert!(!Model::ClaudeSonnet4.supports_adaptive_thinking());
|
||||
assert!(Model::ClaudeOpus4_6.supports_adaptive_thinking());
|
||||
assert!(Model::ClaudeSonnet4_6.supports_adaptive_thinking());
|
||||
assert!(Model::ClaudeFable5.supports_adaptive_thinking());
|
||||
assert!(Model::ClaudeSonnet5.supports_adaptive_thinking());
|
||||
assert!(!Model::ClaudeOpus4_7.supports_xhigh_adaptive_thinking());
|
||||
assert!(Model::ClaudeFable5.supports_xhigh_adaptive_thinking());
|
||||
assert!(Model::ClaudeSonnet5.supports_xhigh_adaptive_thinking());
|
||||
assert!(Model::ClaudeOpus4_8.supports_xhigh_adaptive_thinking());
|
||||
assert!(!ConverseModel::ClaudeSonnet4.supports_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeOpus4_6.supports_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeSonnet4_6.supports_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeFable5.supports_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeSonnet5.supports_adaptive_thinking());
|
||||
assert!(!ConverseModel::ClaudeOpus4_7.supports_xhigh_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeFable5.supports_xhigh_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeSonnet5.supports_xhigh_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeOpus4_8.supports_xhigh_adaptive_thinking());
|
||||
assert_eq!(BedrockAdaptiveThinkingEffort::XHigh.as_str(), "xhigh");
|
||||
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4.thinking_mode(),
|
||||
ConverseModel::ClaudeSonnet4.thinking_mode(),
|
||||
BedrockModelMode::Thinking {
|
||||
budget_tokens: Some(4096)
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeOpus4_6.thinking_mode(),
|
||||
ConverseModel::ClaudeOpus4_6.thinking_mode(),
|
||||
BedrockModelMode::AdaptiveThinking {
|
||||
effort: BedrockAdaptiveThinkingEffort::High
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeHaiku4_5.thinking_mode(),
|
||||
ConverseModel::ClaudeHaiku4_5.thinking_mode(),
|
||||
BedrockModelMode::Thinking {
|
||||
budget_tokens: Some(4096)
|
||||
}
|
||||
|
|
@ -1158,44 +1298,44 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_max_token_count() {
|
||||
assert_eq!(Model::ClaudeSonnet4_5.max_token_count(), 1_000_000);
|
||||
assert_eq!(Model::ClaudeOpus4_6.max_token_count(), 1_000_000);
|
||||
assert_eq!(Model::ClaudeFable5.max_token_count(), 1_000_000);
|
||||
assert_eq!(Model::ClaudeSonnet5.max_token_count(), 1_000_000);
|
||||
assert_eq!(Model::Llama4Scout17B.max_token_count(), 128_000);
|
||||
assert_eq!(Model::NovaPremier.max_token_count(), 1_000_000);
|
||||
assert_eq!(ConverseModel::ClaudeSonnet4_5.max_token_count(), 1_000_000);
|
||||
assert_eq!(ConverseModel::ClaudeOpus4_6.max_token_count(), 1_000_000);
|
||||
assert_eq!(ConverseModel::ClaudeFable5.max_token_count(), 1_000_000);
|
||||
assert_eq!(ConverseModel::ClaudeSonnet5.max_token_count(), 1_000_000);
|
||||
assert_eq!(ConverseModel::Llama4Scout17B.max_token_count(), 128_000);
|
||||
assert_eq!(ConverseModel::NovaPremier.max_token_count(), 1_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_output_tokens() {
|
||||
assert_eq!(Model::ClaudeSonnet4_5.max_output_tokens(), 64_000);
|
||||
assert_eq!(Model::ClaudeOpus4_6.max_output_tokens(), 128_000);
|
||||
assert_eq!(Model::ClaudeFable5.max_output_tokens(), 128_000);
|
||||
assert_eq!(Model::ClaudeSonnet5.max_output_tokens(), 128_000);
|
||||
assert_eq!(Model::ClaudeOpus4_1.max_output_tokens(), 32_000);
|
||||
assert_eq!(Model::Gemma3_4B.max_output_tokens(), 8_192);
|
||||
assert_eq!(ConverseModel::ClaudeSonnet4_5.max_output_tokens(), 64_000);
|
||||
assert_eq!(ConverseModel::ClaudeOpus4_6.max_output_tokens(), 128_000);
|
||||
assert_eq!(ConverseModel::ClaudeFable5.max_output_tokens(), 128_000);
|
||||
assert_eq!(ConverseModel::ClaudeSonnet5.max_output_tokens(), 128_000);
|
||||
assert_eq!(ConverseModel::ClaudeOpus4_1.max_output_tokens(), 32_000);
|
||||
assert_eq!(ConverseModel::Gemma3_4B.max_output_tokens(), 8_192);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supports_tool_use() {
|
||||
assert!(Model::ClaudeSonnet4_5.supports_tool_use());
|
||||
assert!(Model::ClaudeFable5.supports_tool_use());
|
||||
assert!(Model::NovaPro.supports_tool_use());
|
||||
assert!(Model::MistralLarge3.supports_tool_use());
|
||||
assert!(!Model::Gemma3_4B.supports_tool_use());
|
||||
assert!(Model::Qwen3_32B.supports_tool_use());
|
||||
assert!(Model::MiniMaxM2.supports_tool_use());
|
||||
assert!(Model::KimiK2_5.supports_tool_use());
|
||||
assert!(Model::DeepSeekR1.supports_tool_use());
|
||||
assert!(!Model::Llama4Scout17B.supports_tool_use());
|
||||
assert!(ConverseModel::ClaudeSonnet4_5.supports_tool_use());
|
||||
assert!(ConverseModel::ClaudeFable5.supports_tool_use());
|
||||
assert!(ConverseModel::NovaPro.supports_tool_use());
|
||||
assert!(ConverseModel::MistralLarge3.supports_tool_use());
|
||||
assert!(!ConverseModel::Gemma3_4B.supports_tool_use());
|
||||
assert!(ConverseModel::Qwen3_32B.supports_tool_use());
|
||||
assert!(ConverseModel::MiniMaxM2.supports_tool_use());
|
||||
assert!(ConverseModel::KimiK2_5.supports_tool_use());
|
||||
assert!(ConverseModel::DeepSeekR1.supports_tool_use());
|
||||
assert!(!ConverseModel::Llama4Scout17B.supports_tool_use());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supports_caching() {
|
||||
assert!(Model::ClaudeSonnet4_5.supports_caching());
|
||||
assert!(Model::ClaudeOpus4_6.supports_caching());
|
||||
assert!(Model::ClaudeFable5.supports_caching());
|
||||
assert!(!Model::Llama4Scout17B.supports_caching());
|
||||
assert!(!Model::NovaPro.supports_caching());
|
||||
assert!(ConverseModel::ClaudeSonnet4_5.supports_caching());
|
||||
assert!(ConverseModel::ClaudeOpus4_6.supports_caching());
|
||||
assert!(ConverseModel::ClaudeFable5.supports_caching());
|
||||
assert!(!ConverseModel::Llama4Scout17B.supports_caching());
|
||||
assert!(!ConverseModel::NovaPro.supports_caching());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ anthropic = { workspace = true, features = ["schemars"] }
|
|||
anyhow.workspace = true
|
||||
aws-config = { workspace = true, features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { workspace = true, features = ["hardcoded-credentials"] }
|
||||
aws-sigv4.workspace = true
|
||||
aws_http_client.workspace = true
|
||||
base64.workspace = true
|
||||
bedrock = { workspace = true, features = ["schemars"] }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -95,6 +95,7 @@ impl settings::Settings for AllLanguageModelSettings {
|
|||
.collect(),
|
||||
bedrock: AmazonBedrockSettings {
|
||||
available_models: bedrock.available_models.unwrap_or_default(),
|
||||
mantle_available_models: bedrock.mantle_available_models.unwrap_or_default(),
|
||||
custom_headers: custom_headers_from(
|
||||
"Amazon Bedrock",
|
||||
bedrock.custom_headers,
|
||||
|
|
|
|||
|
|
@ -814,7 +814,8 @@ impl OpenAiResponseEventMapper {
|
|||
}
|
||||
events
|
||||
}
|
||||
ResponsesStreamEvent::ReasoningSummaryTextDelta { delta, .. } => {
|
||||
ResponsesStreamEvent::ReasoningSummaryTextDelta { delta, .. }
|
||||
| ResponsesStreamEvent::ReasoningDelta { delta, .. } => {
|
||||
if delta.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
|
|
@ -973,6 +974,7 @@ impl OpenAiResponseEventMapper {
|
|||
| ResponsesStreamEvent::ContentPartDone { .. }
|
||||
| ResponsesStreamEvent::ReasoningSummaryTextDone { .. }
|
||||
| ResponsesStreamEvent::ReasoningSummaryPartDone { .. }
|
||||
| ResponsesStreamEvent::ReasoningDone { .. }
|
||||
| ResponsesStreamEvent::Created { .. }
|
||||
| ResponsesStreamEvent::InProgress { .. }
|
||||
| ResponsesStreamEvent::Unknown => Vec::new(),
|
||||
|
|
@ -1371,6 +1373,22 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_stream_maps_mantle_reasoning_delta() {
|
||||
let event = serde_json::from_value::<ResponsesStreamEvent>(json!({
|
||||
"type": "response.reasoning.delta",
|
||||
"delta": "checking the contract terms"
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let mapped = map_response_events(vec![event]);
|
||||
assert!(matches!(
|
||||
&mapped[0],
|
||||
LanguageModelCompletionEvent::Thinking { text, signature: None }
|
||||
if text == "checking the contract terms"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_usage_deserializes_cached_tokens() -> Result<()> {
|
||||
let usage: ResponseUsage = serde_json::from_value(json!({
|
||||
|
|
|
|||
|
|
@ -310,6 +310,23 @@ pub enum StreamEvent {
|
|||
output_index: usize,
|
||||
summary_index: usize,
|
||||
},
|
||||
#[serde(rename = "response.reasoning.delta")]
|
||||
ReasoningDelta {
|
||||
#[serde(default)]
|
||||
item_id: Option<String>,
|
||||
#[serde(default)]
|
||||
output_index: Option<usize>,
|
||||
delta: String,
|
||||
},
|
||||
#[serde(rename = "response.reasoning.done")]
|
||||
ReasoningDone {
|
||||
#[serde(default)]
|
||||
item_id: Option<String>,
|
||||
#[serde(default)]
|
||||
output_index: Option<usize>,
|
||||
#[serde(default)]
|
||||
text: Option<String>,
|
||||
},
|
||||
#[serde(rename = "response.function_call_arguments.delta")]
|
||||
FunctionCallArgumentsDelta {
|
||||
item_id: String,
|
||||
|
|
|
|||
|
|
@ -114,6 +114,10 @@ pub struct AnthropicAvailableModel {
|
|||
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
|
||||
pub struct AmazonBedrockSettingsContent {
|
||||
pub available_models: Option<Vec<BedrockAvailableModel>>,
|
||||
/// Custom models served through the `bedrock-mantle` endpoint's
|
||||
/// OpenAI-compatible APIs, in addition to the built-in Mantle models
|
||||
/// (GPT-5.5, GPT-5.4, Grok 4.3).
|
||||
pub mantle_available_models: Option<Vec<BedrockMantleAvailableModel>>,
|
||||
pub custom_headers: Option<HashMap<String, String>>,
|
||||
pub endpoint_url: Option<String>,
|
||||
pub region: Option<String>,
|
||||
|
|
@ -139,6 +143,32 @@ pub struct BedrockAvailableModel {
|
|||
pub mode: Option<ModelMode>,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
pub struct BedrockMantleAvailableModel {
|
||||
/// The model id as expected in Bedrock Mantle request bodies, e.g. `openai.gpt-5.5`.
|
||||
pub name: String,
|
||||
pub display_name: Option<String>,
|
||||
pub max_tokens: u64,
|
||||
pub max_output_tokens: Option<u64>,
|
||||
/// The OpenAI-compatible API this model must be called through.
|
||||
pub protocol: BedrockMantleProtocolContent,
|
||||
pub supports_tools: Option<bool>,
|
||||
pub supports_images: Option<bool>,
|
||||
/// Whether this custom Mantle model supports OpenAI reasoning effort parameters.
|
||||
pub supports_thinking: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
pub enum BedrockMantleProtocolContent {
|
||||
/// The OpenAI Chat Completions API (`/chat/completions`).
|
||||
#[serde(rename = "chat_completions")]
|
||||
ChatCompletions,
|
||||
/// The OpenAI Responses API (`/responses`).
|
||||
#[serde(rename = "responses")]
|
||||
Responses,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
pub enum BedrockAuthMethodContent {
|
||||
#[serde(rename = "named_profile")]
|
||||
|
|
|
|||
|
|
@ -213,6 +213,38 @@ Some AWS environments require a guardrail on every Bedrock API call. Add `guardr
|
|||
}
|
||||
```
|
||||
|
||||
### Bedrock Mantle Models {#bedrock-mantle-models}
|
||||
|
||||
Some models, such as GPT-5.5, GPT-5.4, and Grok 4.3, aren't available through Bedrock's Converse API and are only reachable through `bedrock-mantle`, AWS's OpenAI-compatible inference endpoint. Zed routes these models through `bedrock-mantle` automatically; they appear alongside the rest of the Bedrock models in the model picker once you're authenticated, with no extra configuration required.
|
||||
|
||||
Mantle models require IAM permissions for the `bedrock-mantle` endpoint (for example via the `AmazonBedrockMantleInferenceAccess` managed policy) in addition to whatever permissions your existing Bedrock credentials already have, and `bedrock-mantle` is only available in [some AWS Regions](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html#regions). Zed surfaces an error naming the current Region and the supported ones if you try to use a Mantle model outside of them.
|
||||
|
||||
#### Custom Bedrock Mantle Models {#bedrock-mantle-custom-models}
|
||||
|
||||
You can add custom models served through `bedrock-mantle` with `mantle_available_models`:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"language_models": {
|
||||
"bedrock": {
|
||||
"mantle_available_models": [
|
||||
{
|
||||
"name": "openai.gpt-oss-120b",
|
||||
"display_name": "GPT-OSS 120B",
|
||||
"max_tokens": 128000,
|
||||
"protocol": "chat_completions",
|
||||
"supports_tools": true,
|
||||
"supports_images": false,
|
||||
"supports_thinking": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`protocol` selects which OpenAI-compatible API the model is called through, and must be either `chat_completions` or `responses`. Set `supports_thinking` to `true` for custom Mantle models that accept OpenAI reasoning effort parameters; Zed will then expose `low`, `medium`, `high`, and `xhigh` in the thinking effort picker, while disabling thinking sends `none`.
|
||||
|
||||
## OpenAI-Compatible Gateways {#openai-compatible}
|
||||
|
||||
If your gateway exposes an OpenAI-compatible API, configure it with [Use API Access](./use-api-access.md#openai-compatible).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue