Polish provider setup and local-first settings
This commit is contained in:
@@ -7,10 +7,13 @@
|
||||
use aws_config::BehaviorVersion;
|
||||
use aws_sdk_bedrock::Client;
|
||||
use aws_sdk_bedrockruntime::config::Region;
|
||||
use futures::{stream, StreamExt};
|
||||
|
||||
use super::client::{BedrockClientConfig, BedrockError};
|
||||
use crate::settings::ai::BedrockModelConfig;
|
||||
|
||||
const AVAILABILITY_CHECK_CONCURRENCY: usize = 8;
|
||||
|
||||
pub async fn discover_available_models(
|
||||
config: BedrockClientConfig,
|
||||
) -> Result<Vec<BedrockModelConfig>, String> {
|
||||
@@ -24,61 +27,72 @@ pub async fn discover_available_models(
|
||||
.await
|
||||
.map_err(|error| format!("Could not list AWS Bedrock foundation models: {error}"))?;
|
||||
|
||||
let mut models = Vec::new();
|
||||
for summary in catalog.model_summaries() {
|
||||
let model_id = summary.model_id();
|
||||
let availability = match client
|
||||
.get_foundation_model_availability()
|
||||
.model_id(model_id)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(availability) => availability,
|
||||
Err(error) => {
|
||||
log::debug!(
|
||||
"[bedrock] Availability check failed for {model_id}; excluding model: {error}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// AWS exposes agreement/authorization state only through an individual
|
||||
// GetFoundationModelAvailability call. Keep using that control-plane API,
|
||||
// but bound the independent checks so a large catalog does not serialize
|
||||
// startup discovery one model at a time.
|
||||
let checks = catalog.model_summaries().iter().cloned().map(|summary| {
|
||||
let client = client.clone();
|
||||
async move {
|
||||
let model_id = summary.model_id();
|
||||
let availability = match client
|
||||
.get_foundation_model_availability()
|
||||
.model_id(model_id)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(availability) => availability,
|
||||
Err(error) => {
|
||||
log::debug!(
|
||||
"[bedrock] Availability check failed for {model_id}; excluding model: {error}"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
if !model_availability_is_usable(
|
||||
availability
|
||||
.agreement_availability()
|
||||
.map(|agreement| agreement.status().as_str()),
|
||||
availability.authorization_status().as_str(),
|
||||
availability.entitlement_availability().as_str(),
|
||||
availability.region_availability().as_str(),
|
||||
) {
|
||||
log::debug!(
|
||||
"[bedrock] Excluding {model_id}: agreement={}, authorization={}, entitlement={}, region={}",
|
||||
if !model_availability_is_usable(
|
||||
availability
|
||||
.agreement_availability()
|
||||
.map(|agreement| agreement.status().as_str())
|
||||
.unwrap_or("MISSING"),
|
||||
.map(|agreement| agreement.status().as_str()),
|
||||
availability.authorization_status().as_str(),
|
||||
availability.entitlement_availability().as_str(),
|
||||
availability.region_availability().as_str(),
|
||||
);
|
||||
continue;
|
||||
) {
|
||||
log::debug!(
|
||||
"[bedrock] Excluding {model_id}: agreement={}, authorization={}, entitlement={}, region={}",
|
||||
availability
|
||||
.agreement_availability()
|
||||
.map(|agreement| agreement.status().as_str())
|
||||
.unwrap_or("MISSING"),
|
||||
availability.authorization_status().as_str(),
|
||||
availability.entitlement_availability().as_str(),
|
||||
availability.region_availability().as_str(),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let display_name = summary
|
||||
.model_name()
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| prettify_model_id(model_id));
|
||||
let vision_supported = summary
|
||||
.input_modalities()
|
||||
.iter()
|
||||
.any(|modality| modality.as_str() == "IMAGE");
|
||||
|
||||
Some(BedrockModelConfig {
|
||||
model_id: model_id.to_owned(),
|
||||
display_name,
|
||||
vision_supported,
|
||||
use_rig: false,
|
||||
})
|
||||
}
|
||||
|
||||
let display_name = summary
|
||||
.model_name()
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| prettify_model_id(model_id));
|
||||
let vision_supported = summary
|
||||
.input_modalities()
|
||||
.iter()
|
||||
.any(|modality| modality.as_str() == "IMAGE");
|
||||
|
||||
models.push(BedrockModelConfig {
|
||||
model_id: model_id.to_owned(),
|
||||
display_name,
|
||||
vision_supported,
|
||||
use_rig: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
let mut models = stream::iter(checks)
|
||||
.buffer_unordered(AVAILABILITY_CHECK_CONCURRENCY)
|
||||
.filter_map(futures::future::ready)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
models.sort_by(|left, right| left.display_name.cmp(&right.display_name));
|
||||
if models.is_empty() {
|
||||
|
||||
@@ -166,26 +166,24 @@ impl ResponseStream {
|
||||
// Check if this specific model has an OpenAI-compatible routing entry.
|
||||
// This allows OpenAI/LiteLLM models to coexist with Bedrock models —
|
||||
// only models fetched from the OpenAI endpoint route through it.
|
||||
if *settings.openai_enabled.value() {
|
||||
let llm_prefs = LLMPreferences::as_ref(ctx);
|
||||
if let Some(client_config) = llm_prefs.openai_client_config_for_model(model_id) {
|
||||
return ProviderConfig::OpenAI(OpenAIClientConfig {
|
||||
kind: client_config.kind,
|
||||
base_url: client_config.base_url.clone(),
|
||||
api_key: client_config.api_key.clone(),
|
||||
project_id: client_config.project_id.clone(),
|
||||
location: client_config.location.clone(),
|
||||
model: client_config
|
||||
.model
|
||||
.clone()
|
||||
.or_else(|| Some(model_id.to_string())),
|
||||
reasoning_effort: client_config.reasoning_effort.clone(),
|
||||
max_input_tokens: client_config.max_input_tokens,
|
||||
max_output_tokens: client_config.max_output_tokens,
|
||||
use_rig: client_config.use_rig,
|
||||
supports_system_messages: client_config.supports_system_messages,
|
||||
});
|
||||
}
|
||||
let llm_prefs = LLMPreferences::as_ref(ctx);
|
||||
if let Some(client_config) = llm_prefs.openai_client_config_for_model(model_id) {
|
||||
return ProviderConfig::OpenAI(OpenAIClientConfig {
|
||||
kind: client_config.kind,
|
||||
base_url: client_config.base_url.clone(),
|
||||
api_key: client_config.api_key.clone(),
|
||||
project_id: client_config.project_id.clone(),
|
||||
location: client_config.location.clone(),
|
||||
model: client_config
|
||||
.model
|
||||
.clone()
|
||||
.or_else(|| Some(model_id.to_string())),
|
||||
reasoning_effort: client_config.reasoning_effort.clone(),
|
||||
max_input_tokens: client_config.max_input_tokens,
|
||||
max_output_tokens: client_config.max_output_tokens,
|
||||
use_rig: client_config.use_rig,
|
||||
supports_system_messages: client_config.supports_system_messages,
|
||||
});
|
||||
}
|
||||
|
||||
// Fall back to Bedrock
|
||||
|
||||
@@ -46,8 +46,17 @@ pub(crate) struct ChatGPTAuthModel {
|
||||
|
||||
impl ChatGPTAuthModel {
|
||||
pub(crate) fn new() -> Self {
|
||||
let state = match load_or_import_auth_credentials() {
|
||||
Ok(_) => ChatGPTAuthState::Connected,
|
||||
Err(error) => {
|
||||
log::debug!(
|
||||
"[chatgpt/auth] No usable persisted ChatGPT credentials at startup: {error}"
|
||||
);
|
||||
ChatGPTAuthState::NotConnected
|
||||
}
|
||||
};
|
||||
Self {
|
||||
state: ChatGPTAuthState::NotConnected,
|
||||
state,
|
||||
pending_code_verifier: None,
|
||||
pending_state: None,
|
||||
}
|
||||
|
||||
@@ -160,26 +160,24 @@ impl CrosscheckReviewer {
|
||||
let settings = AISettings::as_ref(ctx);
|
||||
|
||||
// Check if this model has an OpenAI-compatible routing entry
|
||||
if *settings.openai_enabled.value() {
|
||||
let llm_prefs = LLMPreferences::as_ref(ctx);
|
||||
if let Some(client_config) = llm_prefs.openai_client_config_for_model(model_id) {
|
||||
return ProviderConfig::OpenAI(OpenAIClientConfig {
|
||||
kind: client_config.kind,
|
||||
base_url: client_config.base_url.clone(),
|
||||
api_key: client_config.api_key.clone(),
|
||||
project_id: client_config.project_id.clone(),
|
||||
location: client_config.location.clone(),
|
||||
model: client_config
|
||||
.model
|
||||
.clone()
|
||||
.or_else(|| Some(model_id.to_string())),
|
||||
reasoning_effort: client_config.reasoning_effort.clone(),
|
||||
max_input_tokens: client_config.max_input_tokens,
|
||||
max_output_tokens: Some(REVIEWER_MAX_OUTPUT_TOKENS),
|
||||
use_rig: client_config.use_rig,
|
||||
supports_system_messages: client_config.supports_system_messages,
|
||||
});
|
||||
}
|
||||
let llm_prefs = LLMPreferences::as_ref(ctx);
|
||||
if let Some(client_config) = llm_prefs.openai_client_config_for_model(model_id) {
|
||||
return ProviderConfig::OpenAI(OpenAIClientConfig {
|
||||
kind: client_config.kind,
|
||||
base_url: client_config.base_url.clone(),
|
||||
api_key: client_config.api_key.clone(),
|
||||
project_id: client_config.project_id.clone(),
|
||||
location: client_config.location.clone(),
|
||||
model: client_config
|
||||
.model
|
||||
.clone()
|
||||
.or_else(|| Some(model_id.to_string())),
|
||||
reasoning_effort: client_config.reasoning_effort.clone(),
|
||||
max_input_tokens: client_config.max_input_tokens,
|
||||
max_output_tokens: Some(REVIEWER_MAX_OUTPUT_TOKENS),
|
||||
use_rig: client_config.use_rig,
|
||||
supports_system_messages: client_config.supports_system_messages,
|
||||
});
|
||||
}
|
||||
|
||||
// Fall back to Bedrock via external config
|
||||
|
||||
+56
-18
@@ -634,6 +634,27 @@ impl LLMPreferences {
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
if ctx
|
||||
.try_get_singleton_model_as_ref::<super::chatgpt_auth::ChatGPTAuthModel>()
|
||||
.is_some()
|
||||
{
|
||||
ctx.subscribe_to_model(
|
||||
&super::chatgpt_auth::ChatGPTAuthModel::handle(ctx),
|
||||
|me, _, event, ctx| {
|
||||
if matches!(
|
||||
event,
|
||||
super::chatgpt_auth::ChatGPTAuthModelEvent::StateChanged
|
||||
) && matches!(
|
||||
super::chatgpt_auth::ChatGPTAuthModel::as_ref(ctx).state(),
|
||||
super::chatgpt_auth::ChatGPTAuthState::Connected
|
||||
) {
|
||||
me.refresh_chatgpt_subscription_models(ctx);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ctx.subscribe_to_model(&UserWorkspaces::handle(ctx), |me, _, event, ctx| {
|
||||
if let UserWorkspacesEvent::TeamsChanged = event {
|
||||
me.sanitize_disabled_custom_model_preferences(ctx);
|
||||
@@ -735,6 +756,10 @@ impl LLMPreferences {
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
// Make the persisted Bedrock catalog available immediately. Agreement
|
||||
// revalidation stays in the background and replaces this cache when it
|
||||
// completes, so startup never waits on the per-model AWS checks.
|
||||
me.inject_bedrock_models(ctx);
|
||||
me.refresh_bedrock_models(ctx);
|
||||
me.inject_openai_models(ctx);
|
||||
me.ensure_default_model_present();
|
||||
@@ -978,7 +1003,7 @@ impl LLMPreferences {
|
||||
|
||||
let settings = AISettings::as_ref(ctx);
|
||||
self.inject_acp_models(ctx);
|
||||
if !*settings.openai_enabled.value() {
|
||||
if !settings.is_openai_provider_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1163,7 +1188,7 @@ impl LLMPreferences {
|
||||
},
|
||||
)]),
|
||||
discount_percentage: None,
|
||||
context_window: openai_model_context_window(model),
|
||||
context_window: openai_model_context_window(model, provider_kind),
|
||||
};
|
||||
self.models_by_feature
|
||||
.agent_mode
|
||||
@@ -1447,7 +1472,7 @@ impl LLMPreferences {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn fetch_openai_models_from_endpoint(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
let settings = AISettings::as_ref(ctx);
|
||||
if !*settings.openai_enabled.value() {
|
||||
if !settings.is_openai_provider_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1509,7 +1534,7 @@ impl LLMPreferences {
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let settings = AISettings::as_ref(ctx);
|
||||
if !*settings.openai_enabled.value() {
|
||||
if !settings.is_openai_provider_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1716,7 +1741,7 @@ impl LLMPreferences {
|
||||
}
|
||||
|
||||
let settings = AISettings::as_ref(ctx);
|
||||
if !*settings.openai_enabled.value()
|
||||
if !settings.is_openai_provider_enabled()
|
||||
|| !settings.openai_providers.value().iter().any(|provider| {
|
||||
provider.enabled && provider.kind == OpenAIProviderKind::ChatGPTSubscription
|
||||
})
|
||||
@@ -2682,13 +2707,21 @@ fn openai_model_variant_id(model_id: &str, reasoning_effort: &str) -> String {
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn openai_model_context_window(model: &OpenAIModelConfig) -> LLMContextWindow {
|
||||
let context_size = openai_model_context_size(model);
|
||||
fn openai_model_context_window(
|
||||
model: &OpenAIModelConfig,
|
||||
provider_kind: OpenAIProviderKind,
|
||||
) -> LLMContextWindow {
|
||||
let default_context_size = openai_model_context_size(model);
|
||||
let max_context_size = if provider_kind == OpenAIProviderKind::ChatGPTSubscription {
|
||||
model.context_size.max(default_context_size)
|
||||
} else {
|
||||
default_context_size
|
||||
};
|
||||
LLMContextWindow {
|
||||
is_configurable: false,
|
||||
min: context_size,
|
||||
max: context_size,
|
||||
default_max: context_size,
|
||||
is_configurable: max_context_size > default_context_size,
|
||||
min: default_context_size,
|
||||
max: max_context_size,
|
||||
default_max: default_context_size,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2822,18 +2855,23 @@ fn chatgpt_models_from_codex_response(body: &serde_json::Value) -> Vec<OpenAIMod
|
||||
return None;
|
||||
}
|
||||
|
||||
let context_size = u32_from_json_any(model, &["context_window", "max_context_window"])
|
||||
let default_context_size = u32_from_json_any(model, &["context_window"])
|
||||
.or_else(|| u32_from_json_any(model, &["max_context_window"]))
|
||||
.unwrap_or(DEFAULT_DISCOVERED_MODEL_CONTEXT_SIZE);
|
||||
let max_context_size = u32_from_json_any(model, &["max_context_window"])
|
||||
.unwrap_or(default_context_size)
|
||||
.max(default_context_size);
|
||||
let effective_context_percent = model["effective_context_window_percent"]
|
||||
.as_u64()
|
||||
.and_then(|value| u32::try_from(value).ok())
|
||||
.filter(|value| (1..=100).contains(value))
|
||||
.unwrap_or(100);
|
||||
let max_input_tokens = Some(
|
||||
context_size
|
||||
.checked_mul(effective_context_percent)
|
||||
.map(|tokens| tokens / 100)
|
||||
.unwrap_or(context_size),
|
||||
);
|
||||
let effective_context_size = |context_size: u32| {
|
||||
u32::try_from(u64::from(context_size) * u64::from(effective_context_percent) / 100)
|
||||
.unwrap_or(context_size)
|
||||
};
|
||||
let max_input_tokens = Some(effective_context_size(default_context_size));
|
||||
let context_size = effective_context_size(max_context_size);
|
||||
|
||||
let vision_supported = model["input_modalities"]
|
||||
.as_array()
|
||||
|
||||
@@ -760,7 +760,7 @@ fn chatgpt_codex_models_parse_visible_catalog_entries() {
|
||||
"display_name": "GPT-5.6-Sol",
|
||||
"visibility": "list",
|
||||
"context_window": 272000,
|
||||
"max_context_window": 272000,
|
||||
"max_context_window": 872000,
|
||||
"effective_context_window_percent": 95,
|
||||
"input_modalities": ["text", "image"],
|
||||
"supported_reasoning_levels": [
|
||||
@@ -795,7 +795,7 @@ fn chatgpt_codex_models_parse_visible_catalog_entries() {
|
||||
assert_eq!(models[0].model_id, "gpt-5.6-sol");
|
||||
assert_eq!(models[0].display_name, "GPT-5.6-Sol");
|
||||
assert!(models[0].vision_supported);
|
||||
assert_eq!(models[0].context_size, 272_000);
|
||||
assert_eq!(models[0].context_size, 828_400);
|
||||
assert_eq!(models[0].max_input_tokens, Some(258_400));
|
||||
assert_eq!(models[0].reasoning_efforts, ["low", "xhigh", "ultra"]);
|
||||
assert!(models[0].use_rig);
|
||||
@@ -804,8 +804,24 @@ fn chatgpt_codex_models_parse_visible_catalog_entries() {
|
||||
|
||||
assert_eq!(models[1].model_id, "gpt-text-only");
|
||||
assert!(!models[1].vision_supported);
|
||||
assert_eq!(models[1].context_size, 128_000);
|
||||
assert_eq!(models[1].context_size, 115_200);
|
||||
assert_eq!(models[1].max_input_tokens, Some(115_200));
|
||||
|
||||
let configurable = openai_model_context_window(
|
||||
&models[0],
|
||||
crate::settings::OpenAIProviderKind::ChatGPTSubscription,
|
||||
);
|
||||
assert!(configurable.is_configurable);
|
||||
assert_eq!(configurable.min, 258_400);
|
||||
assert_eq!(configurable.default_max, 258_400);
|
||||
assert_eq!(configurable.max, 828_400);
|
||||
|
||||
let fixed =
|
||||
openai_model_context_window(&models[0], crate::settings::OpenAIProviderKind::LiteLLM);
|
||||
assert!(!fixed.is_configurable);
|
||||
assert_eq!(fixed.min, 258_400);
|
||||
assert_eq!(fixed.default_max, 258_400);
|
||||
assert_eq!(fixed.max, 258_400);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user