Fix Claude model limits and provider labels in model pickers
This commit is contained in:
@@ -28,7 +28,7 @@ pub fn has_reasoning_variants(llm: &LLMInfo, all_models: &[&LLMInfo]) -> bool {
|
||||
}
|
||||
all_models
|
||||
.iter()
|
||||
.filter(|other| other.base_model_name() == llm.base_model_name() && other.id != llm.id)
|
||||
.filter(|other| other.reasoning_group_key() == llm.reasoning_group_key() && other.id != llm.id)
|
||||
.any(|other| other.has_reasoning_level())
|
||||
}
|
||||
|
||||
@@ -77,9 +77,9 @@ fn make_item_fields<A: Action + Clone>(
|
||||
app: &AppContext,
|
||||
) -> MenuItem<A> {
|
||||
let label = if collapse_auto && is_auto(llm) {
|
||||
"auto".to_string()
|
||||
llm.label_with_provider("auto")
|
||||
} else if collapse_reasoning_variants && llm.has_reasoning_level() {
|
||||
llm.base_model_name().to_string()
|
||||
llm.label_with_provider(llm.base_model_name())
|
||||
} else {
|
||||
llm.menu_display_name()
|
||||
};
|
||||
|
||||
+29
-24
@@ -320,14 +320,24 @@ impl LLMInfo {
|
||||
if custom_model_routers::is_custom_router_id(self.id.as_str()) {
|
||||
return self.display_name.clone();
|
||||
}
|
||||
// Base label includes optional description in parentheses
|
||||
match &self.description {
|
||||
// This is a temporary implementation that won't scale well for longer
|
||||
// descriptions. We should implement a better approach for displaying
|
||||
// model descriptions, maybe through subtext.
|
||||
Some(desc) => format!("{} ({})", self.display_name, desc),
|
||||
None => self.display_name.clone(),
|
||||
self.label_with_provider(&self.display_name)
|
||||
}
|
||||
|
||||
pub fn label_with_provider(&self, label: &str) -> String {
|
||||
if custom_model_routers::is_custom_router_id(self.id.as_str()) {
|
||||
return label.to_string();
|
||||
}
|
||||
match self.description.as_deref().filter(|name| !name.trim().is_empty()) {
|
||||
Some(name) => format!("{label} ({name})"),
|
||||
None => label.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reasoning menus must never merge models belonging to different connections.
|
||||
pub fn reasoning_group_key(&self) -> String {
|
||||
let connection = self.id.as_str().rsplit_once("::connection::")
|
||||
.map(|(_, connection)| connection).unwrap_or_default();
|
||||
format!("{}\u{1f}{}\u{1f}{:?}\u{1f}{connection}", self.base_model_name, self.description.as_deref().unwrap_or_default(), self.provider)
|
||||
}
|
||||
|
||||
/// Returns the given model's base name.
|
||||
@@ -600,7 +610,7 @@ pub struct LLMPreferences {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fetched_openai_models: Vec<OpenAIModelConfig>,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
litellm_connections: Vec<OpenAIProviderConfig>,
|
||||
catalog_connections: Vec<OpenAIProviderConfig>,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
catalog_refresh_generations: HashMap<Option<usize>, u64>,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
@@ -740,7 +750,7 @@ impl LLMPreferences {
|
||||
) {
|
||||
me.fetch_openai_models_from_endpoint(ctx);
|
||||
}
|
||||
me.refresh_changed_litellm_connections(ctx);
|
||||
me.refresh_changed_catalog_connections(ctx);
|
||||
// Safety: ensure the default model is still present in choices.
|
||||
// If all provider models were removed, the default_id would dangle.
|
||||
me.ensure_default_model_present();
|
||||
@@ -780,7 +790,7 @@ impl LLMPreferences {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fetched_openai_models: Vec::new(),
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
litellm_connections: Vec::new(),
|
||||
catalog_connections: Vec::new(),
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
catalog_refresh_generations: HashMap::new(),
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
@@ -816,7 +826,7 @@ impl LLMPreferences {
|
||||
me.inject_openai_models(ctx);
|
||||
me.ensure_default_model_present();
|
||||
me.fetch_openai_models_from_endpoint(ctx);
|
||||
me.refresh_changed_litellm_connections(ctx);
|
||||
me.refresh_changed_catalog_connections(ctx);
|
||||
me.refresh_chatgpt_subscription_models(ctx);
|
||||
me.schedule_chatgpt_subscription_model_refresh(ctx);
|
||||
}
|
||||
@@ -1440,11 +1450,6 @@ impl LLMPreferences {
|
||||
|reasoning| format!("{display_name} ({})", reasoning.name),
|
||||
);
|
||||
let provider_name = provider.display_name();
|
||||
let display_name = if display_name.eq_ignore_ascii_case(&provider_name) {
|
||||
display_name
|
||||
} else {
|
||||
format!("{display_name} · {provider_name}")
|
||||
};
|
||||
let agent_id = provider.agent_id.trim();
|
||||
let agent_id = if agent_id.is_empty() {
|
||||
"codex"
|
||||
@@ -1475,7 +1480,7 @@ impl LLMPreferences {
|
||||
request_multiplier: 1,
|
||||
credit_multiplier: None,
|
||||
},
|
||||
description: Some("ACP".to_string()),
|
||||
description: Some(provider_name),
|
||||
disable_reason: None,
|
||||
vision_supported: false,
|
||||
spec: None,
|
||||
@@ -1693,10 +1698,10 @@ impl LLMPreferences {
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn refresh_changed_litellm_connections(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
fn refresh_changed_catalog_connections(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
let settings = AISettings::as_ref(ctx);
|
||||
if !settings.is_openai_provider_enabled() {
|
||||
self.litellm_connections.clear();
|
||||
self.catalog_connections.clear();
|
||||
return;
|
||||
}
|
||||
let connections: Vec<_> = settings
|
||||
@@ -1714,19 +1719,19 @@ impl LLMPreferences {
|
||||
.enumerate()
|
||||
.filter_map(|(index, provider)| {
|
||||
(provider.enabled
|
||||
&& provider.kind == OpenAIProviderKind::LiteLLM
|
||||
&& self.litellm_connections.get(index) != Some(provider))
|
||||
&& matches!(provider.kind, OpenAIProviderKind::LiteLLM | OpenAIProviderKind::Anthropic)
|
||||
&& self.catalog_connections.get(index) != Some(provider))
|
||||
.then_some(index)
|
||||
})
|
||||
.collect();
|
||||
self.litellm_connections = connections;
|
||||
self.catalog_connections = connections;
|
||||
for index in changed {
|
||||
self.fetch_openai_provider_models(index, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Explicitly refreshes the models for one entry in the OpenAI-compatible
|
||||
/// provider registry. LiteLLM also refreshes on startup and connector changes.
|
||||
/// provider registry. LiteLLM and Anthropic also refresh on startup and connector changes.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn fetch_openai_provider_models(
|
||||
&mut self,
|
||||
@@ -1989,7 +1994,7 @@ impl LLMPreferences {
|
||||
context_size: model.context_size.unwrap_or(128_000),
|
||||
max_input_tokens: model.context_size,
|
||||
default_context_size: None,
|
||||
max_output_tokens: None,
|
||||
max_output_tokens: model.max_output_tokens,
|
||||
provider: None,
|
||||
supports_system_messages: Some(true),
|
||||
capability_overrides: std::collections::HashMap::new(),
|
||||
|
||||
@@ -231,7 +231,7 @@ fn empty_preferences() -> LLMPreferences {
|
||||
custom_model_routers: Vec::new(),
|
||||
openai_provider_routing: HashMap::new(),
|
||||
fetched_openai_models: Vec::new(),
|
||||
litellm_connections: Vec::new(),
|
||||
catalog_connections: Vec::new(),
|
||||
catalog_refresh_generations: HashMap::new(),
|
||||
chatgpt_subscription_models_refresh_in_flight: false,
|
||||
bedrock_models_refresh_in_flight: false,
|
||||
@@ -447,7 +447,7 @@ fn acp_models_are_injected_only_while_acp_is_enabled() {
|
||||
custom_model_routers: Vec::new(),
|
||||
openai_provider_routing: HashMap::new(),
|
||||
fetched_openai_models: Vec::new(),
|
||||
litellm_connections: Vec::new(),
|
||||
catalog_connections: Vec::new(),
|
||||
catalog_refresh_generations: HashMap::new(),
|
||||
chatgpt_subscription_models_refresh_in_flight: false,
|
||||
bedrock_models_refresh_in_flight: false,
|
||||
@@ -1256,3 +1256,23 @@ fn bedrock_verification_survives_settings_reload_but_not_connection_changes() {
|
||||
app.read(|ctx| assert!(!preferences.bedrock_model_is_verified(&model.model_id, ctx)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_selector_labels_and_reasoning_groups_include_configured_provider() {
|
||||
let first: LLMInfo = serde_json::from_value(serde_json::json!({
|
||||
"id": "model-a", "display_name": "Claude (high)", "base_model_name": "Claude",
|
||||
"description": "My Claude Subscription", "provider": "LiteLLM",
|
||||
"usage_metadata": { "request_multiplier": 1 }
|
||||
})).unwrap();
|
||||
assert_eq!(first.menu_display_name(), "Claude (high) (My Claude Subscription)");
|
||||
assert_eq!(first.label_with_provider(first.base_model_name()), "Claude (My Claude Subscription)");
|
||||
let mut other = first.clone();
|
||||
other.description = Some("My LiteLLM".to_string());
|
||||
assert_ne!(first.reasoning_group_key(), other.reasoning_group_key());
|
||||
other.description = first.description.clone();
|
||||
other.display_name = "Claude (low)".to_string();
|
||||
other.id = LLMId::from("model-b");
|
||||
assert_eq!(first.reasoning_group_key(), other.reasoning_group_key());
|
||||
other.id = LLMId::from("model-b::connection::second-connection");
|
||||
assert_ne!(first.reasoning_group_key(), other.reasoning_group_key());
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ use crate::ai::blocklist::{
|
||||
BlocklistAIController, BlocklistAIControllerEvent, BlocklistAIInputEvent, BlocklistAIInputModel,
|
||||
};
|
||||
use crate::ai::cloud_agent_settings::CloudAgentSettings;
|
||||
use crate::ai::custom_model_routers::is_custom_router_id;
|
||||
use crate::ai::execution_profiles::model_menu_items::{
|
||||
available_model_menu_items, has_reasoning_variants, is_auto,
|
||||
};
|
||||
@@ -715,17 +714,7 @@ impl ProfileModelSelector {
|
||||
llm_preferences.get_active_base_model(ctx, Some(self.terminal_view_id))
|
||||
};
|
||||
|
||||
// Don't append description for custom model routers — it would add a
|
||||
// redundant "(Custom auto · Local)" suffix to the button label.
|
||||
if !is_custom_router_id(active_llm.id.as_str()) {
|
||||
if let Some(description) = &active_llm.description {
|
||||
format!("{} ({})", active_llm.display_name, description)
|
||||
} else {
|
||||
active_llm.display_name.clone()
|
||||
}
|
||||
} else {
|
||||
active_llm.display_name.clone()
|
||||
}
|
||||
active_llm.menu_display_name()
|
||||
};
|
||||
|
||||
// Non-Oz runs lock silently: the harness owns model selection, and the
|
||||
@@ -1021,7 +1010,7 @@ impl ProfileModelSelector {
|
||||
.filter(|llm| custom_ids.contains(&llm.id))
|
||||
.collect();
|
||||
|
||||
// Group models by base_model_name to collapse reasoning variants.
|
||||
// Group reasoning variants by model and configured provider.
|
||||
// Use "auto" as the key for all auto models so they collapse together.
|
||||
// Only group models that have reasoning levels - others stay separate.
|
||||
let mut groups: IndexMap<String, Vec<&LLMInfo>> = IndexMap::new();
|
||||
@@ -1029,7 +1018,7 @@ impl ProfileModelSelector {
|
||||
let key = if is_auto(llm) {
|
||||
"auto".to_string()
|
||||
} else if llm.has_reasoning_level() {
|
||||
llm.base_model_name().to_string()
|
||||
llm.reasoning_group_key()
|
||||
} else {
|
||||
llm.id.to_string()
|
||||
};
|
||||
@@ -1057,7 +1046,7 @@ impl ProfileModelSelector {
|
||||
ProfileModelSelectorAction::SelectAutoModel
|
||||
} else if has_reasoning_variants(llm, &all_refs) {
|
||||
ProfileModelSelectorAction::SelectReasoningModel(
|
||||
llm.base_model_name().to_string(),
|
||||
llm.reasoning_group_key(),
|
||||
)
|
||||
} else {
|
||||
ProfileModelSelectorAction::SelectModel(llm.id.clone())
|
||||
@@ -1108,7 +1097,7 @@ impl ProfileModelSelector {
|
||||
ProfileModelSelectorAction::SelectAutoModel
|
||||
} else if has_reasoning_variants(llm, &all_refs) {
|
||||
ProfileModelSelectorAction::SelectReasoningModel(
|
||||
llm.base_model_name().to_string(),
|
||||
llm.reasoning_group_key(),
|
||||
)
|
||||
} else {
|
||||
ProfileModelSelectorAction::SelectModel(llm.id.clone())
|
||||
@@ -1197,7 +1186,7 @@ impl ProfileModelSelector {
|
||||
let items: Vec<MenuItem<ProfileModelSelectorAction>> = self
|
||||
.all_model_choices
|
||||
.iter()
|
||||
.filter(|llm| llm.base_model_name() == base_name && llm.has_reasoning_level())
|
||||
.filter(|llm| llm.reasoning_group_key() == base_name && llm.has_reasoning_level())
|
||||
.map(|llm| {
|
||||
let is_selected = llm.id == active_llm_id;
|
||||
let label = llm.reasoning_level().unwrap_or_default();
|
||||
@@ -1287,10 +1276,10 @@ impl ProfileModelSelector {
|
||||
Some(ProfileModelSelectorAction::SelectAutoModel)
|
||||
)
|
||||
} else if active_llm.has_reasoning_level() {
|
||||
// For models with reasoning levels, match by base_model_name
|
||||
// Match the selected model and its configured provider.
|
||||
matches!(
|
||||
item.item_on_select_action(),
|
||||
Some(ProfileModelSelectorAction::SelectReasoningModel(name)) if *name == active_llm.base_model_name()
|
||||
Some(ProfileModelSelectorAction::SelectReasoningModel(name)) if *name == active_llm.reasoning_group_key()
|
||||
)
|
||||
} else {
|
||||
let item_model_id = item
|
||||
@@ -1349,7 +1338,7 @@ impl ProfileModelSelector {
|
||||
self.all_model_choices
|
||||
.iter()
|
||||
.find(|llm| {
|
||||
llm.base_model_name() == base_name && llm.has_reasoning_level()
|
||||
llm.reasoning_group_key() == *base_name && llm.has_reasoning_level()
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
@@ -1405,7 +1394,7 @@ impl ProfileModelSelector {
|
||||
self.set_sidecar_hovered_info(auto_index, ctx);
|
||||
} else if self.has_multiple_reasoning_variants(info) {
|
||||
// If hovering a model with multiple reasoning variants, refresh reasoning menu
|
||||
self.refresh_model_spec_sidecar_for_model(info.base_model_name(), ctx);
|
||||
self.refresh_model_spec_sidecar_for_model(&info.reasoning_group_key(), ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,8 +402,7 @@ impl ModelSelector {
|
||||
.unwrap_or_else(|| "default".to_string()),
|
||||
_ => LLMPreferences::as_ref(ctx)
|
||||
.get_active_base_model(ctx, Some(self.terminal_view_id))
|
||||
.display_name
|
||||
.clone(),
|
||||
.menu_display_name(),
|
||||
};
|
||||
self.button.update(ctx, |button, ctx| {
|
||||
button.set_label(active_label, ctx);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"source": "https://models.dev/api.json",
|
||||
"retrieved": "2026-10-04",
|
||||
"models": {
|
||||
"claude-fable-5": {
|
||||
"context": 1000000,
|
||||
"output": 128000
|
||||
},
|
||||
"claude-fable-5-1": {
|
||||
"context": 1000000,
|
||||
"output": 128000
|
||||
},
|
||||
"claude-haiku-4-5": {
|
||||
"context": 200000,
|
||||
"output": 64000
|
||||
},
|
||||
"claude-haiku-4-5-20251001": {
|
||||
"context": 200000,
|
||||
"output": 64000
|
||||
},
|
||||
"claude-opus-4-5": {
|
||||
"context": 200000,
|
||||
"output": 64000
|
||||
},
|
||||
"claude-opus-4-5-20251101": {
|
||||
"context": 200000,
|
||||
"output": 64000
|
||||
},
|
||||
"claude-opus-4-6": {
|
||||
"context": 1000000,
|
||||
"output": 128000
|
||||
},
|
||||
"claude-opus-4-7": {
|
||||
"context": 1000000,
|
||||
"output": 128000
|
||||
},
|
||||
"claude-opus-4-8": {
|
||||
"context": 1000000,
|
||||
"output": 128000
|
||||
},
|
||||
"claude-opus-5": {
|
||||
"context": 1000000,
|
||||
"output": 128000
|
||||
},
|
||||
"claude-opus-5-5": {
|
||||
"context": 1000000,
|
||||
"output": 128000
|
||||
},
|
||||
"claude-sonnet-4-5": {
|
||||
"context": 1000000,
|
||||
"output": 64000
|
||||
},
|
||||
"claude-sonnet-4-5-20250929": {
|
||||
"context": 1000000,
|
||||
"output": 64000
|
||||
},
|
||||
"claude-sonnet-4-6": {
|
||||
"context": 1000000,
|
||||
"output": 128000
|
||||
},
|
||||
"claude-sonnet-5": {
|
||||
"context": 1000000,
|
||||
"output": 128000
|
||||
},
|
||||
"claude-sonnet-5-5": {
|
||||
"context": 1000000,
|
||||
"output": 128000
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
@@ -9,6 +10,7 @@ use galaxy_agent_core::{
|
||||
};
|
||||
use rig_core::client::{CompletionClient, ModelListingClient};
|
||||
use rig_core::providers::gemini;
|
||||
use rig_core::http_client::{self, HttpClientExt};
|
||||
|
||||
use crate::anthropic_auth::{anthropic_client, prepare_subscription_request, restore_tool_names};
|
||||
use crate::request::build_completion_request;
|
||||
@@ -19,22 +21,67 @@ pub struct RigModelInfo {
|
||||
pub id: String,
|
||||
pub display_name: String,
|
||||
pub context_size: Option<u32>,
|
||||
pub max_output_tokens: Option<u32>,
|
||||
}
|
||||
|
||||
pub async fn discover_anthropic_models(api_key: &str) -> Result<Vec<RigModelInfo>, String> {
|
||||
let client = anthropic_client(api_key)?;
|
||||
let models = client
|
||||
.list_models()
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok(models
|
||||
.into_iter()
|
||||
.map(|model| RigModelInfo {
|
||||
display_name: model.display_name().to_string(),
|
||||
id: model.id,
|
||||
context_size: model.context_length,
|
||||
})
|
||||
.collect())
|
||||
let mut models = Vec::new();
|
||||
let mut cursors = HashSet::new();
|
||||
let mut path = "/v1/models?limit=1000".to_string();
|
||||
loop {
|
||||
let request = client.get(&path).and_then(|request| Ok(request.body(http_client::NoBody)?))
|
||||
.map_err(|_| "Could not build the Anthropic models request.".to_string())?;
|
||||
let response = client.send::<_, Vec<u8>>(request).await
|
||||
.map_err(|_| "Could not retrieve Anthropic models.".to_string())?;
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Anthropic model discovery returned HTTP {}.", response.status()));
|
||||
}
|
||||
let body = response.into_body().await
|
||||
.map_err(|_| "Could not read the Anthropic models response.".to_string())?;
|
||||
let page: serde_json::Value = serde_json::from_slice(&body)
|
||||
.map_err(|_| "Invalid Anthropic models response.".to_string())?;
|
||||
let entries = page["data"].as_array()
|
||||
.ok_or_else(|| "Anthropic models response is missing its model list.".to_string())?;
|
||||
for entry in entries {
|
||||
models.push(anthropic_model_info(entry)?);
|
||||
}
|
||||
match page["has_more"].as_bool() {
|
||||
Some(false) => return Ok(models),
|
||||
Some(true) => {},
|
||||
None => return Err("Anthropic models response is missing pagination state.".to_string()),
|
||||
}
|
||||
let cursor = page["last_id"].as_str().filter(|cursor| {
|
||||
!cursor.is_empty() && cursor.bytes().all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
|
||||
}).ok_or_else(|| "Invalid Anthropic model pagination cursor.".to_string())?;
|
||||
if !cursors.insert(cursor.to_string()) {
|
||||
return Err("Anthropic model pagination did not advance.".to_string());
|
||||
}
|
||||
path = format!("/v1/models?limit=1000&after_id={cursor}");
|
||||
}
|
||||
}
|
||||
|
||||
fn positive_token_limit(value: &serde_json::Value) -> Option<u32> {
|
||||
value.as_u64().and_then(|tokens| u32::try_from(tokens).ok()).filter(|tokens| *tokens > 0)
|
||||
}
|
||||
|
||||
fn anthropic_model_info(entry: &serde_json::Value) -> Result<RigModelInfo, String> {
|
||||
let id = entry["id"].as_str().filter(|id| !id.is_empty())
|
||||
.ok_or_else(|| "Anthropic returned a model without an ID.".to_string())?;
|
||||
// Read live limits first. Rig's generic model lister drops these fields.
|
||||
// The models.dev snapshot is metadata only, never an availability allowlist.
|
||||
static LIMITS: OnceLock<serde_json::Value> = OnceLock::new();
|
||||
let limits = LIMITS.get_or_init(|| serde_json::from_str(include_str!("../data/anthropic_limits.json"))
|
||||
.expect("bundled Anthropic limits must be valid JSON"));
|
||||
let fallback = &limits["models"][id];
|
||||
Ok(RigModelInfo {
|
||||
id: id.to_string(),
|
||||
display_name: entry["display_name"].as_str().unwrap_or(id).to_string(),
|
||||
context_size: positive_token_limit(&entry["max_input_tokens"])
|
||||
.or_else(|| positive_token_limit(&fallback["context"])),
|
||||
max_output_tokens: positive_token_limit(&entry["max_tokens"])
|
||||
.or_else(|| positive_token_limit(&fallback["output"])),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn discover_gemini_models(api_key: &str) -> Result<Vec<RigModelInfo>, String> {
|
||||
@@ -50,6 +97,7 @@ pub async fn discover_gemini_models(api_key: &str) -> Result<Vec<RigModelInfo>,
|
||||
display_name: model.display_name().to_string(),
|
||||
id: model.id,
|
||||
context_size: model.context_length,
|
||||
max_output_tokens: None,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
@@ -76,6 +124,7 @@ pub fn vertex_ai_model_catalog() -> Vec<RigModelInfo> {
|
||||
id: id.to_string(),
|
||||
display_name: display_name.to_string(),
|
||||
context_size: None,
|
||||
max_output_tokens: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -7,3 +7,24 @@ fn native_descriptors_are_provider_specific() {
|
||||
assert_eq!(descriptor.id, "rig-anthropic:claude-sonnet");
|
||||
assert_eq!(descriptor.display_name, "Rig / anthropic / claude-sonnet");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anthropic_discovery_prefers_live_limits_over_catalog_defaults() {
|
||||
let model = super::anthropic_model_info(&serde_json::json!({
|
||||
"id": "claude-opus-4-6", "display_name": "Opus", "max_input_tokens": 500000, "max_tokens": 32000
|
||||
})).unwrap();
|
||||
assert_eq!(model.context_size, Some(500000));
|
||||
assert_eq!(model.max_output_tokens, Some(32000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anthropic_discovery_uses_exact_catalog_limits_when_api_omits_them() {
|
||||
let model = super::anthropic_model_info(&serde_json::json!({
|
||||
"id": "claude-opus-4-6", "max_input_tokens": null, "max_tokens": null
|
||||
})).unwrap();
|
||||
assert_eq!(model.context_size, Some(1000000));
|
||||
assert_eq!(model.max_output_tokens, Some(128000));
|
||||
let unknown = super::anthropic_model_info(&serde_json::json!({"id": "claude-unknown"})).unwrap();
|
||||
assert_eq!(unknown.context_size, None);
|
||||
assert_eq!(unknown.max_output_tokens, None);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user