mirror of
https://github.com/openai/codex.git
synced 2026-10-25 07:55:34 +00:00
Expose model upgrade retirement times (#38449)
## What changed - Parse the optional `retirement_at` RFC 3339 value from model upgrade metadata, treating missing, null, or malformed values as unknown. - Expose known retirement times from `model/list` as nullable Unix timestamps in `upgradeInfo.retirementAt` and preserve them when converting app-server models back into model presets. - Update the generated protocol schemas and app-server documentation. ## Testing - Cover absent, null, valid, and malformed catalog values. - Verify `model/list` serialization and app-server model conversion for known, missing, and out-of-range timestamps. GitOrigin-RevId: 969baf9f5b012997f460fb4611ba63c963ee658c
This commit is contained in:
+8
@@ -14041,6 +14041,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"retirementAt": {
|
||||
"description": "Informational Unix timestamp for this upgrade's scheduled retirement, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"upgradeCopy": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
+8
@@ -10351,6 +10351,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"retirementAt": {
|
||||
"description": "Informational Unix timestamp for this upgrade's scheduled retirement, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"upgradeCopy": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -197,6 +197,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"retirementAt": {
|
||||
"description": "Informational Unix timestamp for this upgrade's scheduled retirement, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"upgradeCopy": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -2,4 +2,8 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ModelUpgradeInfo = { model: string, upgradeCopy: string | null, modelLink: string | null, migrationMarkdown: string | null, };
|
||||
export type ModelUpgradeInfo = { model: string, upgradeCopy: string | null, modelLink: string | null, migrationMarkdown: string | null,
|
||||
/**
|
||||
* Informational Unix timestamp for this upgrade's scheduled retirement, if known.
|
||||
*/
|
||||
retirementAt: number | null, };
|
||||
|
||||
@@ -128,6 +128,9 @@ pub struct ModelUpgradeInfo {
|
||||
pub upgrade_copy: Option<String>,
|
||||
pub model_link: Option<String>,
|
||||
pub migration_markdown: Option<String>,
|
||||
/// Informational Unix timestamp for this upgrade's scheduled retirement, if known.
|
||||
#[ts(type = "number | null")]
|
||||
pub retirement_at: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -230,7 +230,7 @@ Example with notification opt-out:
|
||||
- `fs/watch` — subscribe this connection to filesystem change notifications for an absolute file or directory path and caller-provided `watchId`; returns the canonicalized `path`.
|
||||
- `fs/unwatch` — stop sending notifications for a prior `fs/watch`; returns `{}`.
|
||||
- `fs/changed` — notification emitted when watched paths change, including the `watchId` and `changedPaths`.
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with model-advertised string reasoning effort options in the catalog's intended progression order, optional `modelSpecialty`, nullable `multiAgentVersion` (`disabled`, `v1`, or `v2`), `additionalSpeedTiers`, `serviceTiers`, optional `defaultServiceTier`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata. Clients should preserve the `supportedReasoningEfforts` array order rather than deriving order from the effort names.
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with model-advertised string reasoning effort options in the catalog's intended progression order, optional `modelSpecialty`, nullable `multiAgentVersion` (`disabled`, `v1`, or `v2`), `additionalSpeedTiers`, `serviceTiers`, optional `defaultServiceTier`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`, nullable informational `retirementAt` Unix timestamp), and optional `availabilityNux` metadata. Clients should preserve the `supportedReasoningEfforts` array order rather than deriving order from the effort names.
|
||||
- `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider.
|
||||
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
|
||||
- `permissionProfile/list` — beta; list available permission profile ids with optional display `description` text and an `allowed` flag reflecting effective requirements, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.<id>]` entries to be included in the current catalog view.
|
||||
|
||||
@@ -34,6 +34,10 @@ fn model_from_preset(preset: ModelPreset) -> Model {
|
||||
upgrade_copy: upgrade.upgrade_copy.clone(),
|
||||
model_link: upgrade.model_link.clone(),
|
||||
migration_markdown: upgrade.migration_markdown.clone(),
|
||||
retirement_at: upgrade
|
||||
.retirement_at
|
||||
.as_ref()
|
||||
.map(chrono::DateTime::timestamp),
|
||||
}),
|
||||
availability_nux: preset.availability_nux.map(Into::into),
|
||||
display_name: preset.display_name.to_string(),
|
||||
|
||||
@@ -40,6 +40,10 @@ fn model_from_preset(preset: &ModelPreset) -> Model {
|
||||
upgrade_copy: upgrade.upgrade_copy.clone(),
|
||||
model_link: upgrade.model_link.clone(),
|
||||
migration_markdown: upgrade.migration_markdown.clone(),
|
||||
retirement_at: upgrade
|
||||
.retirement_at
|
||||
.as_ref()
|
||||
.map(chrono::DateTime::timestamp),
|
||||
}),
|
||||
availability_nux: preset.availability_nux.clone().map(Into::into),
|
||||
display_name: preset.display_name.clone(),
|
||||
@@ -155,38 +159,48 @@ async fn list_models_includes_hidden_models() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn list_models_uses_chatgpt_remote_catalog_as_source_of_truth() -> Result<()> {
|
||||
let server = MockServer::start().await;
|
||||
let remote_model: ModelInfo = serde_json::from_value(json!({
|
||||
"slug": "chatgpt-remote-only",
|
||||
"display_name": "ChatGPT Remote Only",
|
||||
"description": "Remote-only model for app-server model/list coverage",
|
||||
"model_specialty": MODEL_SPECIALTY_CYBER,
|
||||
"default_reasoning_level": "max",
|
||||
"supported_reasoning_levels": [
|
||||
{"effort": "max", "description": "Maximum"},
|
||||
{"effort": "low", "description": "Low"},
|
||||
{"effort": "focused", "description": "Focused"}
|
||||
],
|
||||
"shell_type": "shell_command",
|
||||
"visibility": "list",
|
||||
"minimal_client_version": [0, 1, 0],
|
||||
"supported_in_api": true,
|
||||
"priority": 0,
|
||||
"upgrade": null,
|
||||
"support_verbosity": false,
|
||||
"default_verbosity": null,
|
||||
"apply_patch_tool_type": null,
|
||||
"truncation_policy": {"mode": "bytes", "limit": 10_000},
|
||||
"supports_parallel_tool_calls": false,
|
||||
"supports_image_detail_original": false,
|
||||
"multi_agent_version": "v2",
|
||||
"context_window": 272_000,
|
||||
"max_context_window": 272_000,
|
||||
"experimental_supported_tools": [],
|
||||
}))?;
|
||||
let remote_models = [json!("2030-01-01T00:00:00Z"), serde_json::Value::Null]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(priority, retirement_at)| {
|
||||
serde_json::from_value::<ModelInfo>(json!({
|
||||
"slug": format!("chatgpt-remote-only-{priority}"),
|
||||
"display_name": "ChatGPT Remote Only",
|
||||
"description": "Remote-only model for app-server model/list coverage",
|
||||
"model_specialty": MODEL_SPECIALTY_CYBER,
|
||||
"default_reasoning_level": "max",
|
||||
"supported_reasoning_levels": [
|
||||
{"effort": "max", "description": "Maximum"},
|
||||
{"effort": "low", "description": "Low"},
|
||||
{"effort": "focused", "description": "Focused"}
|
||||
],
|
||||
"shell_type": "shell_command",
|
||||
"visibility": "list",
|
||||
"minimal_client_version": [0, 1, 0],
|
||||
"supported_in_api": true,
|
||||
"priority": priority,
|
||||
"upgrade": {
|
||||
"model": "replacement-model",
|
||||
"migration_markdown": "Use the replacement model.",
|
||||
"retirement_at": retirement_at,
|
||||
},
|
||||
"support_verbosity": false,
|
||||
"default_verbosity": null,
|
||||
"apply_patch_tool_type": null,
|
||||
"truncation_policy": {"mode": "bytes", "limit": 10_000},
|
||||
"supports_parallel_tool_calls": false,
|
||||
"supports_image_detail_original": false,
|
||||
"multi_agent_version": "v2",
|
||||
"context_window": 272_000,
|
||||
"max_context_window": 272_000,
|
||||
"experimental_supported_tools": [],
|
||||
}))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let models_mock = mount_models_once(
|
||||
&server,
|
||||
ModelsResponse {
|
||||
models: vec![remote_model.clone()],
|
||||
models: remote_models.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@@ -216,20 +230,30 @@ openai_base_url = "{server_uri}/v1"
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build_initialized()
|
||||
.await?;
|
||||
let request_id = mcp
|
||||
.send_list_models_request(ModelListParams {
|
||||
limit: Some(100),
|
||||
cursor: None,
|
||||
include_hidden: None,
|
||||
})
|
||||
.await?;
|
||||
let response = mcp
|
||||
.read_stream_until_response_message(RequestId::Integer(request_id))
|
||||
.await?;
|
||||
assert_eq!(
|
||||
response.result["data"][0]["upgradeInfo"]["retirementAt"],
|
||||
json!(1_893_456_000)
|
||||
);
|
||||
assert_eq!(
|
||||
response.result["data"][1]["upgradeInfo"]["retirementAt"],
|
||||
serde_json::Value::Null
|
||||
);
|
||||
let ModelListResponse {
|
||||
data: items,
|
||||
next_cursor,
|
||||
} = mcp
|
||||
.request(|request_id| ClientRequest::ModelList {
|
||||
request_id,
|
||||
params: ModelListParams {
|
||||
limit: Some(100),
|
||||
cursor: None,
|
||||
include_hidden: None,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
let mut expected_presets: Vec<ModelPreset> = vec![remote_model.into()];
|
||||
} = serde_json::from_value(response.result)?;
|
||||
let mut expected_presets: Vec<ModelPreset> =
|
||||
remote_models.into_iter().map(Into::into).collect();
|
||||
ModelPreset::mark_default_by_picker_visibility(&mut expected_presets);
|
||||
let mut expected_items = expected_presets
|
||||
.iter()
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use schemars::JsonSchema;
|
||||
use schemars::r#gen::SchemaGenerator;
|
||||
use schemars::schema::InstanceType;
|
||||
@@ -187,6 +189,15 @@ pub struct ModelUpgrade {
|
||||
pub model_link: Option<String>,
|
||||
pub upgrade_copy: Option<String>,
|
||||
pub migration_markdown: Option<String>,
|
||||
/// Informational time when the model associated with this upgrade is scheduled to retire.
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
deserialize_with = "deserialize_optional_rfc3339_timestamp"
|
||||
)]
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[ts(type = "string", optional)]
|
||||
pub retirement_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
|
||||
@@ -616,6 +627,29 @@ impl ModelInstructionsVariables {
|
||||
pub struct ModelInfoUpgrade {
|
||||
pub model: String,
|
||||
pub migration_markdown: String,
|
||||
/// Informational time when the model associated with this upgrade is scheduled to retire.
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
deserialize_with = "deserialize_optional_rfc3339_timestamp"
|
||||
)]
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[ts(type = "string", optional)]
|
||||
pub retirement_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
fn deserialize_optional_rfc3339_timestamp<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<DateTime<Utc>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
|
||||
Ok(value
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.and_then(|value| DateTime::parse_from_rfc3339(value).ok())
|
||||
.map(|value| value.with_timezone(&Utc)))
|
||||
}
|
||||
|
||||
impl From<&ModelUpgrade> for ModelInfoUpgrade {
|
||||
@@ -623,6 +657,7 @@ impl From<&ModelUpgrade> for ModelInfoUpgrade {
|
||||
ModelInfoUpgrade {
|
||||
model: upgrade.id.clone(),
|
||||
migration_markdown: upgrade.migration_markdown.clone().unwrap_or_default(),
|
||||
retirement_at: upgrade.retirement_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -750,6 +785,7 @@ impl From<ModelInfo> for ModelPreset {
|
||||
model_link: None,
|
||||
upgrade_copy: None,
|
||||
migration_markdown: Some(upgrade.migration_markdown.clone()),
|
||||
retirement_at: upgrade.retirement_at,
|
||||
}),
|
||||
show_in_picker: info.visibility == ModelVisibility::List,
|
||||
multi_agent_version: info.multi_agent_version,
|
||||
@@ -1448,6 +1484,102 @@ mod tests {
|
||||
assert_eq!(model.tool_mode, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_info_deserializes_optional_upgrade_retirement_at() {
|
||||
let base = serde_json::to_value(test_model(/*spec*/ None))
|
||||
.expect("serialize test model without retirement time");
|
||||
|
||||
let mut absent = base.clone();
|
||||
absent
|
||||
.as_object_mut()
|
||||
.expect("model info should be an object")
|
||||
.insert(
|
||||
"upgrade".to_string(),
|
||||
serde_json::json!({
|
||||
"model": "replacement-model",
|
||||
"migration_markdown": "Use the replacement model."
|
||||
}),
|
||||
);
|
||||
let absent = serde_json::from_value::<ModelInfo>(absent)
|
||||
.expect("deserialize model info without upgrade retirement time");
|
||||
assert_eq!(
|
||||
absent
|
||||
.upgrade
|
||||
.as_ref()
|
||||
.and_then(|upgrade| upgrade.retirement_at.as_ref())
|
||||
.map(DateTime::timestamp),
|
||||
None
|
||||
);
|
||||
|
||||
let mut null = base.clone();
|
||||
null.as_object_mut()
|
||||
.expect("model info should be an object")
|
||||
.insert(
|
||||
"upgrade".to_string(),
|
||||
serde_json::json!({
|
||||
"model": "replacement-model",
|
||||
"migration_markdown": "Use the replacement model.",
|
||||
"retirement_at": null
|
||||
}),
|
||||
);
|
||||
let null = serde_json::from_value::<ModelInfo>(null)
|
||||
.expect("deserialize model info with null upgrade retirement time");
|
||||
assert_eq!(
|
||||
null.upgrade
|
||||
.as_ref()
|
||||
.and_then(|upgrade| upgrade.retirement_at.as_ref())
|
||||
.map(DateTime::timestamp),
|
||||
None
|
||||
);
|
||||
|
||||
let mut populated = base;
|
||||
populated
|
||||
.as_object_mut()
|
||||
.expect("model info should be an object")
|
||||
.insert(
|
||||
"upgrade".to_string(),
|
||||
serde_json::json!({
|
||||
"model": "replacement-model",
|
||||
"migration_markdown": "Use the replacement model.",
|
||||
"retirement_at": "2030-01-01T00:00:00Z"
|
||||
}),
|
||||
);
|
||||
let populated = serde_json::from_value::<ModelInfo>(populated)
|
||||
.expect("deserialize model info with upgrade retirement time");
|
||||
assert_eq!(
|
||||
populated
|
||||
.upgrade
|
||||
.as_ref()
|
||||
.and_then(|upgrade| upgrade.retirement_at.as_ref())
|
||||
.map(DateTime::timestamp),
|
||||
Some(1_893_456_000)
|
||||
);
|
||||
|
||||
let mut malformed = serde_json::to_value(test_model(/*spec*/ None))
|
||||
.expect("serialize test model for malformed retirement time");
|
||||
malformed
|
||||
.as_object_mut()
|
||||
.expect("model info should be an object")
|
||||
.insert(
|
||||
"upgrade".to_string(),
|
||||
serde_json::json!({
|
||||
"model": "replacement-model",
|
||||
"migration_markdown": "Use the replacement model.",
|
||||
"retirement_at": "not-a-timestamp"
|
||||
}),
|
||||
);
|
||||
let malformed = serde_json::from_value::<ModelInfo>(malformed)
|
||||
.expect("tolerate malformed upgrade retirement time");
|
||||
assert_eq!(
|
||||
malformed
|
||||
.upgrade
|
||||
.as_ref()
|
||||
.and_then(|upgrade| upgrade.retirement_at.as_ref())
|
||||
.map(DateTime::timestamp),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_info_preserves_explicit_apps_guidance_opt_out() {
|
||||
let value = serde_json::to_value(test_model(/*spec*/ None))
|
||||
|
||||
@@ -284,6 +284,7 @@ pub(super) async fn handle_model_migration_prompt_if_needed(
|
||||
model_link,
|
||||
upgrade_copy,
|
||||
migration_markdown,
|
||||
..
|
||||
}) = upgrade
|
||||
{
|
||||
if migration_prompt_hidden(config, migration_config_key.as_str()) {
|
||||
|
||||
@@ -22,6 +22,7 @@ fn model_presets_with_test_upgrades() -> Vec<ModelPreset> {
|
||||
model_link: None,
|
||||
upgrade_copy: None,
|
||||
migration_markdown: None,
|
||||
retirement_at: None,
|
||||
});
|
||||
}
|
||||
presets
|
||||
@@ -305,6 +306,7 @@ async fn model_migration_prompt_skips_when_target_missing_or_hidden() {
|
||||
model_link: None,
|
||||
upgrade_copy: None,
|
||||
migration_markdown: None,
|
||||
retirement_at: None,
|
||||
});
|
||||
available.retain(|preset| preset.model != "gpt-5.2");
|
||||
available.push(current.clone());
|
||||
|
||||
@@ -1519,6 +1519,13 @@ fn model_preset_from_api_model(model: ApiModel) -> ModelPreset {
|
||||
.as_ref()
|
||||
.and_then(|info| info.upgrade_copy.clone()),
|
||||
migration_markdown: upgrade_info.and_then(|info| info.migration_markdown),
|
||||
retirement_at: model
|
||||
.upgrade_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.retirement_at)
|
||||
.and_then(|retirement_at| {
|
||||
chrono::DateTime::<chrono::Utc>::from_timestamp(retirement_at, 0)
|
||||
}),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2156,6 +2163,72 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn api_model_with_upgrade_retirement_at(retirement_at: Option<i64>) -> ApiModel {
|
||||
ApiModel {
|
||||
id: "model-id".to_string(),
|
||||
model: "current-model".to_string(),
|
||||
upgrade: Some("replacement-model".to_string()),
|
||||
upgrade_info: Some(codex_app_server_protocol::ModelUpgradeInfo {
|
||||
model: "replacement-model".to_string(),
|
||||
upgrade_copy: None,
|
||||
model_link: None,
|
||||
migration_markdown: None,
|
||||
retirement_at,
|
||||
}),
|
||||
availability_nux: None,
|
||||
display_name: "Current model".to_string(),
|
||||
description: "A test model".to_string(),
|
||||
model_specialty: None,
|
||||
hidden: false,
|
||||
supported_reasoning_efforts: Vec::new(),
|
||||
default_reasoning_effort: ReasoningEffort::Medium,
|
||||
input_modalities: Vec::new(),
|
||||
supports_personality: false,
|
||||
multi_agent_version: None,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
is_default: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_preset_from_api_model_preserves_upgrade_retirement_at() {
|
||||
let retirement_at = chrono::DateTime::parse_from_rfc3339("2030-01-01T00:00:00Z")
|
||||
.expect("valid RFC 3339 timestamp")
|
||||
.with_timezone(&chrono::Utc);
|
||||
let expected_upgrade = |retirement_at| {
|
||||
Some(ModelUpgrade {
|
||||
id: "replacement-model".to_string(),
|
||||
migration_config_key: "current-model".to_string(),
|
||||
model_link: None,
|
||||
upgrade_copy: None,
|
||||
migration_markdown: None,
|
||||
retirement_at,
|
||||
})
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
vec![
|
||||
model_preset_from_api_model(api_model_with_upgrade_retirement_at(Some(
|
||||
retirement_at.timestamp(),
|
||||
)))
|
||||
.upgrade,
|
||||
model_preset_from_api_model(api_model_with_upgrade_retirement_at(
|
||||
/*retirement_at*/ None,
|
||||
))
|
||||
.upgrade,
|
||||
model_preset_from_api_model(api_model_with_upgrade_retirement_at(Some(i64::MAX)))
|
||||
.upgrade,
|
||||
],
|
||||
vec![
|
||||
expected_upgrade(Some(retirement_at)),
|
||||
expected_upgrade(None),
|
||||
expected_upgrade(None),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_rate_limit_snapshots_deduplicates_top_level_limit_from_map() {
|
||||
let response = GetAccountRateLimitsResponse {
|
||||
|
||||
Reference in New Issue
Block a user