Add a token-efficient model-facing ID generator (#59540)

## Why

Decimal IDs of up to three digits fit in a single token. Issuing shorter IDs first keeps identifiers compact in model context.

## What changed

Add `codex-utils-model-facing-id` with deterministic, seed-based ID generation. Each salt prefixes the displayed ID and derives a separate sequence from the session seed. The generator shuffles decimal pools, exhausting `000`–`999` before issuing six-digit suffixes, then expanding in groups of three digits.

Support serializable, versioned snapshots that resume at the next unissued ID, plus suffix-only formatting for protocols that supply their own prefix.

## Testing

Add integration tests for pool exhaustion, stable outputs across pool boundaries, serialization and restore, salt-based seed derivation, invalid snapshots, and wraparound after the last `u64` ordinal.

GitOrigin-RevId: 80ffbf861106980b731d34a61ca2b9448d3bbb0d
This commit is contained in:
Adam Perry @ OpenAI
2026-11-05 01:34:00 +00:00
committed by copyberry
parent de6e72addd
commit b386e2d9e3
6 changed files with 433 additions and 0 deletions
+13
View File
@@ -5400,6 +5400,19 @@ dependencies = [
"toml 0.9.11+spec-1.1.0",
]
[[package]]
name = "codex-utils-model-facing-id"
version = "0.0.0"
dependencies = [
"blake3",
"pretty_assertions",
"schemars 0.8.22",
"serde",
"serde_json",
"thiserror 2.0.18",
"tracing",
]
[[package]]
name = "codex-utils-oss"
version = "0.0.0"
+1
View File
@@ -147,6 +147,7 @@ members = [
"utils/cli",
"utils/elapsed",
"utils/sandbox-summary",
"utils/model-facing-id",
"utils/sleep-inhibitor",
"utils/approval-presets",
"utils/oss",
@@ -0,0 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "model-facing-id",
crate_name = "codex_utils_model_facing_id",
)
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "codex-utils-model-facing-id"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[lib]
doctest = false
test = false
[dependencies]
blake3.workspace = true
schemars.workspace = true
serde = { workspace = true, features = ["derive"] }
thiserror.workspace = true
tracing.workspace = true
[dev-dependencies]
serde_json.workspace = true
pretty_assertions.workspace = true
+197
View File
@@ -0,0 +1,197 @@
//! Issues a salt followed by shuffled decimal IDs from `000`–`999`, then `001000`–`999999`.
//! Each pool uses a four-round Feistel permutation with BLAKE3 and cycle-walking.
//! The seed can be varied across sessions to change the order in which IDs are issued.
use std::borrow::Cow;
use std::fmt;
use std::ops::RangeInclusive;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use thiserror::Error;
const VERSION: u32 = 1;
// Our tokenizers encode every decimal ID of up to three digits as a single token.
const GROUP_SIZE: u64 = 1_000;
/// Stores the salt and numeric value separately. Display pads the value to groups of three digits.
/// With salt `environment`, zero renders as `environment000` and 1000 as `environment001000`.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ModelFacingId {
salt: Cow<'static, str>,
value: u64,
}
impl fmt::Display for ModelFacingId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.salt, self.digits())
}
}
impl ModelFacingId {
/// Formats the padded digits for protocols that supply their own type prefix.
pub fn digits(&self) -> impl fmt::Display + '_ {
fmt::from_fn(|f| {
write!(
f,
"{:0width$}",
self.value,
width = decimal_width(self.value)
)
})
}
}
fn decimal_width(value: u64) -> usize {
(value.checked_ilog10().unwrap_or(0) as usize + 1).next_multiple_of(3)
}
/// Generates token-efficient identifiers for model context.
#[derive(Debug)]
pub struct ModelFacingIdGenerator {
state: ModelFacingIdGeneratorState,
}
impl ModelFacingIdGenerator {
/// Starts a sequence for one ID kind using a shared session seed.
/// The salt also prefixes each displayed ID. Use the [tokenizer](https://platform.openai.com/tokenizer)
/// to choose a stable, single-token salt such as `environment`.
///
/// # Panics
///
/// Panics if the salt is empty.
pub fn new(seed: u64, salt: &'static str) -> Self {
assert!(!salt.is_empty(), "a model ID salt must be nonempty");
let mut hasher = blake3::Hasher::new();
hasher.update(&seed.to_le_bytes());
hasher.update(salt.as_bytes());
let mut seed_bytes = [0; 8];
hasher.finalize_xof().fill(&mut seed_bytes);
let derived_seed = u64::from_le_bytes(seed_bytes);
Self {
state: ModelFacingIdGeneratorState::new(
VERSION,
derived_seed,
salt,
/*next_ordinal*/ 0,
),
}
}
/// Issues the next ID. After all `u64` values are issued, the sequence repeats from zero.
pub fn fetch_next(&mut self) -> ModelFacingId {
let ordinal = self.state.next_ordinal;
let mut pool = 0..=GROUP_SIZE - 1;
while ordinal > *pool.end() {
let start = pool.end() + 1;
// The final decimal pool ends at u64::MAX, which has no representable successor.
let last = start
.checked_mul(GROUP_SIZE)
.map_or(u64::MAX, |end| end - 1);
pool = start..=last;
}
let id = permute(self.state.seed, pool, ordinal);
let (next, wrapped) = ordinal.overflowing_add(1);
if wrapped {
tracing::error!(
"model-facing ID generator exhausted all u64 values and wrapped to zero"
);
}
self.state.next_ordinal = next;
ModelFacingId {
salt: self.state.salt.clone(),
value: id,
}
}
/// Captures the position of the next allocation.
pub fn snapshot(&self) -> ModelFacingIdGeneratorState {
self.state.clone()
}
/// Continues the snapshot's sequence from its next unissued ordinal.
pub fn restore(state: ModelFacingIdGeneratorState) -> Result<Self, RestoreError> {
if state.version != VERSION {
return Err(RestoreError::UnsupportedVersion(state.version));
}
if state.salt.is_empty() {
return Err(RestoreError::InvalidSalt);
}
Ok(Self { state })
}
}
/// Persisted sequence version, derived seed, salt, and zero-based next allocation ordinal.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ModelFacingIdGeneratorState {
version: u32,
seed: u64,
salt: Cow<'static, str>,
next_ordinal: u64,
}
impl ModelFacingIdGeneratorState {
/// Returns the namespace this snapshot belongs to.
pub fn salt(&self) -> &str {
&self.salt
}
/// Rebuilds a snapshot from its stored fields. [`ModelFacingIdGenerator::restore`] validates it.
pub fn new(
version: u32,
seed: u64,
salt: impl Into<Cow<'static, str>>,
next_ordinal: u64,
) -> Self {
Self {
version,
seed,
salt: salt.into(),
next_ordinal,
}
}
}
/// The snapshot cannot be used to resume an allocation sequence.
#[derive(Debug, PartialEq, Eq, Error)]
pub enum RestoreError {
#[error("unsupported model-facing ID generator version {0}")]
UnsupportedVersion(u32),
#[error("a model ID salt must be nonempty")]
InvalidSalt,
}
// Use the smallest balanced Feistel domain that covers the pool to limit cycle-walking.
// Both the four rounds and their input encoding are part of the snapshot format.
fn permute(seed: u64, pool: RangeInclusive<u64>, ordinal: u64) -> u64 {
let start = *pool.start();
let size = pool.end() - start + 1;
let bits = u64::BITS - (size - 1).leading_zeros();
let half_bits = bits.div_ceil(2);
let half_mask = (1_u64 << half_bits) - 1;
// The seed, pool index, and round occupy 17 bytes. The pool width fixes the right half's length.
let input_len = 17 + half_bits.div_ceil(8) as usize;
let mut input = [0; 25];
input[..8].copy_from_slice(&seed.to_le_bytes());
input[8..16].copy_from_slice(&(start / GROUP_SIZE).to_le_bytes());
let mut slot = ordinal - start;
loop {
let (mut left, mut right) = (slot >> half_bits, slot & half_mask);
for round in 0..4 {
input[16] = round;
input[17..].copy_from_slice(&right.to_le_bytes());
let hash = blake3::hash(&input[..input_len]);
let mut bytes = [0; 8];
bytes.copy_from_slice(&hash.as_bytes()[..8]);
let f = u64::from_le_bytes(bytes) & half_mask;
(left, right) = (right, left ^ f);
}
slot = (left << half_bits) | right;
// Following the permutation cycle must eventually return to the starting slot.
if slot < size {
return start + slot;
}
}
}
@@ -0,0 +1,193 @@
//! Checks pool boundaries, snapshot restore, and the v1 sequence through the public API.
use std::collections::BTreeSet;
use std::error::Error;
use codex_utils_model_facing_id::ModelFacingId;
use codex_utils_model_facing_id::ModelFacingIdGenerator;
use codex_utils_model_facing_id::ModelFacingIdGeneratorState;
use codex_utils_model_facing_id::RestoreError;
use pretty_assertions::assert_eq;
use serde_json::json;
/// Every three-digit suffix is issued before the first ID drawn from the six-digit pool.
#[test]
fn exhausts_three_digit_ids_before_opening_the_six_digit_pool() {
let mut generator = ModelFacingIdGenerator::new(/*seed*/ 42, "environment");
let mut actual = BTreeSet::new();
for _ in 0..1_000 {
actual.insert(generator.fetch_next().to_string());
}
let expected: BTreeSet<_> = (0..1_000).map(|id| format!("environment{id:03}")).collect();
assert_eq!(actual, expected);
assert_eq!(generator.fetch_next().to_string(), "environment825109");
}
/// IDs can have the same display text and still round-trip with distinct salts and values.
#[test]
fn persisted_ids_keep_the_salt_and_value_separate() -> Result<(), serde_json::Error> {
for (salt, value) in [("env123", 0), ("env", 123_000)] {
let saved = json!({ "salt": salt, "value": value });
let id: ModelFacingId = serde_json::from_value(saved.clone())?;
assert_eq!(id.to_string(), "env123000");
assert_eq!(serde_json::to_value(id)?, saved);
}
Ok(())
}
/// A saved cursor resumes the uninterrupted sequence both inside and between pools.
#[test]
fn serialized_snapshots_resume_the_sequence() -> Result<(), Box<dyn Error>> {
let derived_seed = 0x9e6e_7c74_2945_897e;
for next_ordinal in [437, 1_000, 1_437] {
let mut original = ModelFacingIdGenerator::new(u64::MAX, "env2");
for _ in 0..next_ordinal {
original.fetch_next();
}
let snapshot = original.snapshot();
assert_eq!(
ModelFacingIdGeneratorState::new(
/*version*/ 1,
derived_seed,
"env2",
next_ordinal
),
snapshot
);
let saved = serde_json::to_value(snapshot)?;
assert_eq!(
saved,
json!({"version": 1, "seed": derived_seed, "salt": "env2", "next_ordinal": next_ordinal})
);
let mut resumed = ModelFacingIdGenerator::restore(serde_json::from_value(saved)?)?;
for _ in 0..1_001 {
assert_eq!(resumed.fetch_next(), original.fetch_next());
}
}
Ok(())
}
/// V1 must keep the initial outputs and repeated cycle walks stable.
#[test]
fn v1_known_outputs() -> Result<(), Box<dyn Error>> {
let seed = 0x1234_5678_9abc_def0;
let state = ModelFacingIdGeneratorState::new(
/*version*/ 1,
seed,
"environment",
/*next_ordinal*/ 0,
);
let mut generator = ModelFacingIdGenerator::restore(state)?;
for id in ["895", "930", "155", "141", "916", "912", "678", "430"] {
assert_eq!(
generator.fetch_next().to_string(),
format!("environment{id}")
);
}
// Ordinal 1,605 visits two excluded slots before returning to the six-digit pool.
let state = ModelFacingIdGeneratorState::new(
/*version*/ 1,
seed,
"environment",
/*next_ordinal*/ 1_605,
);
let mut resumed = ModelFacingIdGenerator::restore(state)?;
assert_eq!(resumed.fetch_next().to_string(), "environment765577");
Ok(())
}
/// The last ID at each width and the first at the next width stay stable across restores.
#[test]
fn v1_known_outputs_at_every_pool_boundary() -> Result<(), Box<dyn Error>> {
let seed = 0x1234_5678_9abc_def0;
for (boundary, last, first) in [
(1_000, "553", "661326"),
(1_000_000, "753874", "286136338"),
(1_000_000_000, "839882170", "813648058018"),
(1_000_000_000_000, "634462456979", "134726697070558"),
(
1_000_000_000_000_000,
"326088534089890",
"563366069705075074",
),
(
1_000_000_000_000_000_000,
"633936232025849076",
"012827849051678362461",
),
] {
let state = ModelFacingIdGeneratorState::new(/*version*/ 1, seed, "env", boundary - 1);
let mut generator = ModelFacingIdGenerator::restore(state)?;
assert_eq!(generator.fetch_next().to_string(), format!("env{last}"));
let mut resumed = ModelFacingIdGenerator::restore(generator.snapshot())?;
let next = generator.fetch_next();
assert_eq!(next.to_string(), format!("env{first}"));
assert_eq!(resumed.fetch_next(), next);
}
Ok(())
}
/// The same session seed can drive several ID kinds, each with its own resumable seed.
#[test]
fn salts_derive_the_seed_stored_in_snapshots() {
for (seed, salt, derived_seed) in [
(42, "environment", 0xc9b5_d0ac_9722_ea42),
(42, "context-window", 0x2747_db8a_6513_485b),
(43, "environment", 0x70aa_a9fd_4bf2_6e41),
] {
assert_eq!(
ModelFacingIdGenerator::new(seed, salt).snapshot(),
ModelFacingIdGeneratorState::new(
/*version*/ 1,
derived_seed,
salt,
/*next_ordinal*/ 0
)
);
}
}
/// The final ordinal belongs to the last decimal pool. Its successor restarts the sequence.
#[test]
fn wraps_after_the_last_u64_ordinal() -> Result<(), Box<dyn Error>> {
let seed = 17;
let last = ModelFacingIdGeneratorState::new(/*version*/ 1, seed, "env", u64::MAX);
let mut generator = ModelFacingIdGenerator::restore(last)?;
let text = generator.fetch_next().to_string();
let last_id = text
.strip_prefix("env")
.expect("restored salt")
.parse::<u64>()?;
assert!(last_id >= 1_000_000_000_000_000_000);
let first =
ModelFacingIdGeneratorState::new(/*version*/ 1, seed, "env", /*next_ordinal*/ 0);
assert_eq!(generator.snapshot(), first);
let mut fresh = ModelFacingIdGenerator::restore(first)?;
let mut resumed = ModelFacingIdGenerator::restore(generator.snapshot())?;
assert_eq!(resumed.fetch_next(), fresh.fetch_next());
assert_eq!(resumed.snapshot(), fresh.snapshot());
Ok(())
}
#[test]
fn rejects_unsupported_snapshot_versions() {
let saved = ModelFacingIdGeneratorState::new(
/*version*/ 2, /*seed*/ 0, "env", /*next_ordinal*/ 0,
);
assert_eq!(
ModelFacingIdGenerator::restore(saved).unwrap_err(),
RestoreError::UnsupportedVersion(2)
);
}
#[test]
fn rejects_empty_salts_on_restore() {
let state = ModelFacingIdGeneratorState::new(
/*version*/ 1, /*seed*/ 42, "", /*next_ordinal*/ 0,
);
assert_eq!(
ModelFacingIdGenerator::restore(state).expect_err("empty salt"),
RestoreError::InvalidSalt
);
}