mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
Preserve model-facing ID allocations in saved history (#60771)
## Why Restoring model-facing ID sequences requires allocation ordinals alongside the rendered IDs. Responses API payloads still require string IDs. ## What changed - Add allocation metadata to `ResponseItemId` while keeping equality, hashing, and ordering based on the rendered ID. - Preserve optional `id_allocation` metadata in rollout items, inter-agent messages, compaction replacement history, and Guardian checkpoints. Continue accepting legacy string IDs. - Support structured context-window allocations alongside UUIDv7 strings, validate the `window` salt on restoration, and keep request metadata IDs as strings. ## Testing Add tests for allocation round trips, legacy ID compatibility, string-only raw response item IDs, and context-window allocation validation. GitOrigin-RevId: ce1fd964bde9a4e7c527432be99aea87ada30169
This commit is contained in:
committed by
copyberry
parent
32234ddac4
commit
340a18ed7c
@@ -1,37 +1,57 @@
|
||||
//! Context window identity used for compaction, rollout restoration, and request metadata.
|
||||
//! Rollouts store a UUIDv7 string or a window allocation. Request metadata uses its display form.
|
||||
|
||||
use codex_utils_model_facing_id::ModelFacingId;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(derive_more::Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[debug("{_0}")]
|
||||
#[serde(transparent)]
|
||||
pub struct ContextWindowId(String);
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum ContextWindowId {
|
||||
Text(String),
|
||||
Generated(Box<ModelFacingId>),
|
||||
}
|
||||
|
||||
impl ContextWindowId {
|
||||
pub fn new_uuid_v7() -> Self {
|
||||
Self(Uuid::now_v7().to_string())
|
||||
Self::Text(Uuid::now_v7().to_string())
|
||||
}
|
||||
|
||||
/// Reads a persisted window ID, ignoring IDs outside the supported format.
|
||||
pub fn allocation(&self) -> Option<&ModelFacingId> {
|
||||
match self {
|
||||
Self::Text(_) => None,
|
||||
Self::Generated(id) => (id.salt() == "window").then_some(id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a UUIDv7 window ID from a rollout string.
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
Uuid::parse_str(value)
|
||||
.ok()
|
||||
.filter(|uuid| uuid.get_version_num() == 7)
|
||||
.map(|uuid| Self(uuid.to_string()))
|
||||
.map(|uuid| Self::Text(uuid.to_string()))
|
||||
}
|
||||
|
||||
/// Validate a deserialized window ID before restoring it.
|
||||
/// Restore the window without discarding its allocation ordinal.
|
||||
pub fn validated(&self) -> Option<Self> {
|
||||
Self::parse(&self.0)
|
||||
match self {
|
||||
Self::Text(text) => Self::parse(text),
|
||||
Self::Generated(_) => self.allocation().map(|_| self.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContextWindowId {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(formatter)
|
||||
match self {
|
||||
Self::Text(text) => formatter.write_str(text),
|
||||
Self::Generated(id) => id.fmt(formatter),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "context_window_id_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use super::ContextWindowId;
|
||||
use codex_utils_model_facing_id::ModelFacingIdGenerator;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn restores_allocations_from_the_window_generator() {
|
||||
let window = ContextWindowId::Generated(Box::new(
|
||||
ModelFacingIdGenerator::new(/*seed*/ 0, "window").fetch_next(),
|
||||
));
|
||||
assert_eq!(window.validated(), Some(window));
|
||||
|
||||
let item = ModelFacingIdGenerator::new(/*seed*/ 0, "item").fetch_next();
|
||||
assert_eq!(ContextWindowId::Generated(Box::new(item)).validated(), None);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ use item_id_generator::uuid_v7_item_id_generator_for_test;
|
||||
pub use permission_profile_intersection::PermissionIntersectionError;
|
||||
pub use permission_profile_intersection::intersect_effective_permission_profiles;
|
||||
pub use response_item_id::ResponseItemId;
|
||||
pub use response_item_id::ResponseItemIdAllocation;
|
||||
pub use response_usage::ResponseUsageMetadata;
|
||||
pub use sanitized_git_url::SanitizedGitUrl;
|
||||
pub use session_id::SessionId;
|
||||
|
||||
@@ -31,6 +31,7 @@ use schemars::JsonSchema;
|
||||
|
||||
use crate::ResponseItemId;
|
||||
use crate::mcp::CallToolResult;
|
||||
use crate::response_item_id::OptionalWireItemId;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
mod configuration_update;
|
||||
@@ -1096,7 +1097,8 @@ impl InternalChatMessageMetadataPassthrough {
|
||||
pub enum ResponseItem {
|
||||
AdditionalTools {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
role: String,
|
||||
#[ts(type = "unknown[]")]
|
||||
@@ -1111,7 +1113,8 @@ pub enum ResponseItem {
|
||||
#[ts(optional)]
|
||||
status: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
role: String,
|
||||
content: Vec<ContentItem>,
|
||||
@@ -1127,7 +1130,8 @@ pub enum ResponseItem {
|
||||
},
|
||||
AgentMessage {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
author: String,
|
||||
recipient: String,
|
||||
@@ -1138,7 +1142,8 @@ pub enum ResponseItem {
|
||||
},
|
||||
Reasoning {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
summary: Vec<ReasoningItemReasoningSummary>,
|
||||
#[serde(default, skip_serializing_if = "should_serialize_reasoning_content")]
|
||||
@@ -1155,7 +1160,8 @@ pub enum ResponseItem {
|
||||
encrypted_content: Option<String>,
|
||||
/// Legacy id field retained for compatibility with older payloads.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
/// Set when using the Responses API.
|
||||
call_id: Option<String>,
|
||||
@@ -1173,7 +1179,8 @@ pub enum ResponseItem {
|
||||
#[ts(optional)]
|
||||
status: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -1196,7 +1203,8 @@ pub enum ResponseItem {
|
||||
#[ts(optional)]
|
||||
encrypted_content: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
call_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -1216,7 +1224,8 @@ pub enum ResponseItem {
|
||||
// We keep this behavior centralized in `FunctionCallOutputPayload`.
|
||||
FunctionCallOutput {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -1239,7 +1248,8 @@ pub enum ResponseItem {
|
||||
#[ts(optional)]
|
||||
encrypted_content: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -1260,7 +1270,8 @@ pub enum ResponseItem {
|
||||
// text or structured content items.
|
||||
CustomToolCallOutput {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
call_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -1275,7 +1286,8 @@ pub enum ResponseItem {
|
||||
},
|
||||
ToolSearchOutput {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
call_id: Option<String>,
|
||||
status: String,
|
||||
@@ -1299,7 +1311,8 @@ pub enum ResponseItem {
|
||||
#[ts(optional)]
|
||||
encrypted_content: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -1325,7 +1338,8 @@ pub enum ResponseItem {
|
||||
#[ts(optional)]
|
||||
encrypted_content: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
status: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -1339,7 +1353,8 @@ pub enum ResponseItem {
|
||||
#[serde(alias = "compaction_summary")]
|
||||
Compaction {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
encrypted_content: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -1354,7 +1369,8 @@ pub enum ResponseItem {
|
||||
CompactionTrigger {},
|
||||
ContextCompaction {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
id: Option<ResponseItemId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
|
||||
@@ -61,6 +61,7 @@ use crate::plan_tool::UpdatePlanArgs;
|
||||
use crate::request_permissions::RequestPermissionsEvent;
|
||||
use crate::request_permissions::RequestPermissionsResponse;
|
||||
use crate::request_user_input::RequestUserInputResponse;
|
||||
use crate::response_item_id::OptionalWireItemId;
|
||||
use crate::turn_input::CyberAccessProgram;
|
||||
use crate::turn_input::SuspendTurnOutcome;
|
||||
use crate::turn_input::TurnInputMode;
|
||||
@@ -994,7 +995,8 @@ impl FromStr for ThreadHistoryMode {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct InterAgentCommunication {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
#[serde(with = "OptionalWireItemId")]
|
||||
#[ts(optional, as = "Option<ResponseItemId>")]
|
||||
pub id: Option<ResponseItemId>,
|
||||
pub author: AgentPath,
|
||||
pub recipient: AgentPath,
|
||||
@@ -3450,7 +3452,7 @@ pub enum MultiAgentVersion {
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
pub struct SessionContextWindow {
|
||||
/// UUIDv7 identity of this context window.
|
||||
/// Persisted model-facing window identity.
|
||||
pub window_id: crate::ContextWindowId,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
use std::hash::Hash;
|
||||
use std::hash::Hasher;
|
||||
use std::ops::Deref;
|
||||
|
||||
use codex_utils_model_facing_id::ModelFacingId;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -8,17 +12,85 @@ use ts_rs::TS;
|
||||
|
||||
/// A Responses API item ID. New IDs require an explicit prefix; deserialization
|
||||
/// remains permissive so legacy rollouts can still be read.
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, TS,
|
||||
)]
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(untagged)]
|
||||
pub enum ResponseItemId {
|
||||
Text(String),
|
||||
// TypeScript describes the API, where both variants are strings.
|
||||
#[ts(skip)]
|
||||
Generated {
|
||||
id: String,
|
||||
id_allocation: Box<ResponseItemIdAllocation>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The generator allocation and the Responses-specific parts of a structured item ID.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ResponseItemIdAllocation {
|
||||
/// Item kind, e.g. `msg` in `msg_item042`. Bare request reservations omit it.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prefix: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub model_id: ModelFacingId,
|
||||
/// Provider-assigned tail, including its separator.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub suffix: Option<String>,
|
||||
}
|
||||
|
||||
/// Responses and raw item events exchange optional string IDs.
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
#[schemars(with = "String")]
|
||||
#[ts(type = "string")]
|
||||
pub struct ResponseItemId(String);
|
||||
pub(crate) struct OptionalWireItemId(Option<String>);
|
||||
|
||||
impl OptionalWireItemId {
|
||||
pub(crate) fn serialize<S: serde::Serializer>(
|
||||
id: &Option<ResponseItemId>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
id.as_deref().serialize(serializer)
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D: serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<ResponseItemId>, D::Error> {
|
||||
Ok(<Self as Deserialize>::deserialize(deserializer)?
|
||||
.0
|
||||
.map(ResponseItemId::from_server))
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseItemId {
|
||||
pub fn with_suffix(prefix: &str, suffix: impl fmt::Display) -> Self {
|
||||
Self(format!("{prefix}_{suffix}"))
|
||||
Self::Text(format!("{prefix}_{suffix}"))
|
||||
}
|
||||
|
||||
/// Keep the allocation that produced this ID while rendering a string on the Responses wire.
|
||||
pub fn generated(kind: &str, model_id: ModelFacingId) -> Self {
|
||||
let (prefix, id) = if kind == model_id.salt() {
|
||||
(None, model_id.to_string())
|
||||
} else {
|
||||
(Some(kind.to_owned()), format!("{kind}_{model_id}"))
|
||||
};
|
||||
Self::Text(id).with_allocation(ResponseItemIdAllocation {
|
||||
prefix,
|
||||
model_id,
|
||||
suffix: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Restore a known allocation while keeping the exact wire ID.
|
||||
pub fn with_allocation(self, allocation: ResponseItemIdAllocation) -> Self {
|
||||
Self::Generated {
|
||||
id: self.into(),
|
||||
id_allocation: Box::new(allocation),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocation(&self) -> Option<&ResponseItemIdAllocation> {
|
||||
match self {
|
||||
Self::Text(_) => None,
|
||||
Self::Generated { id_allocation, .. } => Some(id_allocation),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reserved for physical user inputs admitted by the host. Raw item
|
||||
@@ -28,15 +100,17 @@ impl ResponseItemId {
|
||||
}
|
||||
|
||||
pub fn is_admitted_user_input(&self) -> bool {
|
||||
self.0.starts_with("msg_input_")
|
||||
self.starts_with("msg_input_")
|
||||
}
|
||||
|
||||
pub fn from_server(value: String) -> Self {
|
||||
Self(value)
|
||||
Self::Text(value)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
match self {
|
||||
Self::Text(id) | Self::Generated { id, .. } => id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_prefixed(&self) -> bool {
|
||||
@@ -45,6 +119,29 @@ impl ResponseItemId {
|
||||
}
|
||||
}
|
||||
|
||||
// Lookups by a wire ID must also find an item restored with allocation provenance.
|
||||
impl PartialEq for ResponseItemId {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.as_str() == other.as_str()
|
||||
}
|
||||
}
|
||||
impl Eq for ResponseItemId {}
|
||||
impl Hash for ResponseItemId {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.as_str().hash(state);
|
||||
}
|
||||
}
|
||||
impl Ord for ResponseItemId {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.as_str().cmp(other.as_str())
|
||||
}
|
||||
}
|
||||
impl PartialOrd for ResponseItemId {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ResponseItemId {
|
||||
type Target = str;
|
||||
|
||||
@@ -65,9 +162,20 @@ impl fmt::Display for ResponseItemId {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ResponseItemId {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_tuple("ResponseItemId")
|
||||
.field(&self.as_str())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResponseItemId> for String {
|
||||
fn from(value: ResponseItemId) -> Self {
|
||||
value.0
|
||||
match value {
|
||||
ResponseItemId::Text(id) | ResponseItemId::Generated { id, .. } => id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::ResponseItemId;
|
||||
use crate::models::ResponseItem;
|
||||
|
||||
#[test]
|
||||
fn creates_prefixed_uuid_v7_ids() {
|
||||
@@ -37,6 +39,31 @@ fn deserializes_arbitrary_ids_as_strings() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structured_ids_round_trip_but_raw_items_only_accept_strings() {
|
||||
let model_id = serde_json::from_value(json!({"salt":"item","ordinal":7,"value":42})).unwrap();
|
||||
let id = ResponseItemId::generated("msg", model_id);
|
||||
let mut stored = serde_json::to_value(id).unwrap();
|
||||
assert_eq!(
|
||||
stored,
|
||||
json!({"id":"msg_item042","id_allocation":{
|
||||
"prefix":"msg","salt":"item","ordinal":7,"value":42
|
||||
}})
|
||||
);
|
||||
stored["id"] = json!("msg_item042_2");
|
||||
stored["id_allocation"]["suffix"] = json!("_2");
|
||||
let id: ResponseItemId = serde_json::from_value(stored.clone()).unwrap();
|
||||
assert_eq!(id.as_str(), "msg_item042_2");
|
||||
assert_eq!(serde_json::to_value(&id).unwrap(), stored);
|
||||
|
||||
let mut raw = json!({"type": "message", "role": "assistant", "content": [], "id": stored});
|
||||
assert!(serde_json::from_value::<ResponseItem>(raw.clone()).is_err());
|
||||
raw["id"] = json!(id.as_str());
|
||||
let item: ResponseItem = serde_json::from_value(raw.clone()).unwrap();
|
||||
assert_eq!(item.id().unwrap().allocation(), None);
|
||||
assert_eq!(serde_json::to_value(item).unwrap(), raw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognizes_non_empty_prefix_and_suffix() {
|
||||
for (value, expected) in [
|
||||
|
||||
Reference in New Issue
Block a user