Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .repository-projection.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
"projection": "deixic-code",
"projectionSchemaVersion": 1,
"sourceRepository": "dx-corp/mono",
"sourceSha": "e93942ba70f7585b80a55a24065ebbd31e57708d",
"sourceSha": "c30dc2f64dce3300144e0e0d0dc97eb97b8744ee",
"destinationRepository": "dx-corp/code",
"priorProjectedBase": "a6d447c5960e7b738a3c3a27fa564f8dcae7f448",
"priorProjectedBase": "358d47f82eb9a64f88ecfa2865002aa704e0baa5",
"definitionDigest": "82936441c776e3e8edb5d215a75007ec9714a233f489d460075d79d5ef5ba32f",
"toolDigest": "c244d99199a7ae3eb8ff644a99462163c23b0bb6a83ef50af01efbdca0b81d04",
"contentDigest": "13746ab7cd6f508ce691afd179ce7d3f11b2bb8d901b5db0ba91153d55c25366",
"contentDigest": "fc3edcce20cf8d94ea1cd95fce135752740dc77c7bd79715e242599e385cb806",
"publicationEligible": true
}
28 changes: 27 additions & 1 deletion packages/dex-host-rs/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,13 @@ impl ChunkTranslator {
StreamEvent::Usage {
input_tokens,
output_tokens,
..
cache_read_tokens,
cache_creation_tokens,
} => vec![Ok(ModelChunk::Usage(Usage {
input_tokens,
output_tokens,
cache_read_input_tokens: cache_read_tokens.unwrap_or_default(),
cache_creation_input_tokens: cache_creation_tokens.unwrap_or_default(),
Comment on lines 198 to +201

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include cached input in the token budget

When AiRsModel receives OpenAI-compatible or Google usage, StreamEvent::input_tokens is already the uncached prompt count (see packages/ai-rs/src/openai.rs and packages/ai-rs/src/google.rs), but this mapping retains that value as Usage.input_tokens and stores cache counts separately. Usage::tokens() only sums input_tokens + output_tokens, so a largely cached request can consume far more than Budget::max_tokens without exhausting it, contrary to the new Usage.input_tokens contract. Fold the cache buckets into the budget total (or make input_tokens total prompt tokens) before emitting the chunk.

Useful? React with 👍 / 👎.

// Not attributed here; see the module doc comment.
cost_micros: 0,
}))],
Expand Down Expand Up @@ -255,6 +258,28 @@ mod tests {
}
}

#[test]
fn retains_provider_reported_cache_usage() {
let chunks = ChunkTranslator::default().translate(StreamEvent::Usage {
input_tokens: 10,
output_tokens: 3,
cache_read_tokens: Some(20),
cache_creation_tokens: Some(7),
});
assert!(matches!(&chunks[0], Ok(ModelChunk::Usage(usage))
if usage.input_tokens == 10 && usage.output_tokens == 3
&& usage.cache_read_input_tokens == 20
&& usage.cache_creation_input_tokens == 7));
let absent = ChunkTranslator::default().translate(StreamEvent::Usage {
input_tokens: 10,
output_tokens: 3,
cache_read_tokens: None,
cache_creation_tokens: None,
});
assert!(matches!(&absent[0], Ok(ModelChunk::Usage(usage))
if usage.cache_read_input_tokens == 0 && usage.cache_creation_input_tokens == 0));
}

#[tokio::test]
async fn translates_text_then_tool_call_then_usage() {
let scripted = ScriptedClient::new(
Expand Down Expand Up @@ -291,6 +316,7 @@ mod tests {
)],
);
let spec = ToolSpec {
description: String::new(),
name: ToolName::new("fs.read_file"),
label: "Read a file".into(),
schema: serde_json::json!({"type": "object"}),
Expand Down
2 changes: 2 additions & 0 deletions packages/dex-host-rs/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub const WRITE_FILE: &str = "fs.write_file";
fn catalog() -> Vec<ToolSpec> {
vec![
ToolSpec {
description: "Read a text file from the workspace.".into(),
Comment thread
evalops-mirror[bot] marked this conversation as resolved.
name: ToolName::new(READ_FILE),
label: "Read a workspace file".into(),
schema: serde_json::json!({
Expand All @@ -42,6 +43,7 @@ fn catalog() -> Vec<ToolSpec> {
executor: ExecutorKind::InProcess,
},
ToolSpec {
description: "Write text content to a file in the workspace.".into(),
name: ToolName::new(WRITE_FILE),
label: "Write a workspace file".into(),
schema: serde_json::json!({
Expand Down
62 changes: 61 additions & 1 deletion packages/runtime-rs/src/agent/credential_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,22 @@ impl CredentialVault {
Ok(state.attestation())
}

/// Protocol identifiers are not prose: a one-character password inside
/// a longer identifier is an incidental spelling overlap. Exact values,
/// standalone characters, longer credentials and unowned references still
/// fail. Content values retain the stricter full substring check.
pub(crate) fn attest_provider_identifier(
&self,
text: &str,
) -> Result<CredentialAttestation, &'static str> {
let state = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
attest_provider_identifier_with_store(&state.store, text)?;
Ok(state.attestation())
}

/// Check JSON before serialization can escape credential bytes. Object
/// keys are provider content too, even though vault_in_json leaves them
/// intact to preserve the schema.
Expand All @@ -848,7 +864,7 @@ impl CredentialVault {
}
serde_json::Value::Object(entries) => {
for (key, value) in entries {
attest_provider_text_with_store(store, key)?;
attest_provider_identifier_with_store(store, key)?;
visit(store, value)?;
}
}
Expand Down Expand Up @@ -1004,6 +1020,50 @@ fn attest_provider_text_with_store(
Ok(())
}

fn attest_provider_identifier_with_store(
store: &CredentialStore,
text: &str,
) -> Result<(), &'static str> {
let references = store.references();
for (start, end) in credential_reference_like_ranges(text) {
if !references.contains(&text[start..end]) {
return Err("provider identifier contains an unowned credential reference");
}
}
let protected = REFERENCE_PATTERN
.find_iter(text)
.map(|reference| (reference.start(), reference.end()))
.collect::<Vec<_>>();
for credential in store.credentials.values() {
let value = credential.value.as_str();
if value.is_empty() {
continue;
}
for (start, _) in text.match_indices(value) {
let end = start + value.len();
if protected
.iter()
.any(|(left, right)| start >= *left && end <= *right)
{
continue;
}
let single_character = value.chars().count() == 1;
let within_word = text[..start]
.chars()
.next_back()
.is_some_and(char::is_alphanumeric)
|| text[end..]
.chars()
.next()
.is_some_and(char::is_alphanumeric);
if !single_character || !within_word {
return Err("provider identifier contains a plaintext vaulted credential");
}
}
}
Ok(())
}

impl fmt::Debug for CredentialStore {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
Expand Down
46 changes: 22 additions & 24 deletions packages/runtime-rs/src/agent/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ mod deferred_tool_tests;
mod model_dynamics;
mod provider_history;
mod provider_loop;
mod provider_payload;
mod read_only_tools;
mod side_questions;
#[cfg(test)]
Expand Down Expand Up @@ -3794,12 +3795,7 @@ fn vault_provider_history(
messages: &[Message],
credential_vault: &CredentialVault,
) -> Result<Vec<Message>> {
// Scan the final provider projection as a last boundary check. This also
// catches raw material introduced by prompts, hooks, or extensions after
// tool output was first vaulted. Existing references remain opaque.
let serialized = serde_json::to_value(messages).context("serialize provider history")?;
let safe = credential_vault.vault_in_json(&serialized);
serde_json::from_value(safe).context("deserialize vaulted provider history")
provider_payload::vault_history(messages, credential_vault)
}

fn vault_provider_history_shared(
Expand Down Expand Up @@ -3857,10 +3853,7 @@ impl ProviderSafeRequest {
// Config preparation may discover a credential present in history.
let messages = vault_provider_history_shared(&messages, vault)?;
config.system = config.system.map(|system| vault.vault_in_text(&system));
let history_json = serde_json::to_value(messages.as_ref())?;
let attestation = vault
.attest_provider_json(&history_json)
.map_err(anyhow::Error::msg)?;
let attestation = provider_payload::attest_history(&messages, vault)?;
if let Some(system) = &config.system {
let system_attestation = vault
.attest_provider_text(system)
Expand All @@ -3870,10 +3863,7 @@ impl ProviderSafeRequest {
"credential vault changed during provider request preparation"
);
}
let tools_json = serde_json::to_value(config.tools.as_ref())?;
let tools_attestation = vault
.attest_provider_json(&tools_json)
.map_err(anyhow::Error::msg)?;
let tools_attestation = provider_payload::attest_tools(&config.tools, vault)?;
anyhow::ensure!(
attestation == tools_attestation,
"credential vault changed during provider request preparation"
Expand All @@ -3900,9 +3890,9 @@ fn vault_provider_tools(
) -> Result<Arc<Vec<Tool>>> {
let mut safe_tools = Vec::with_capacity(tools.len());
for tool in tools {
if credential_vault.vault_in_text(&tool.name) != tool.name {
anyhow::bail!("credential detected in provider tool name");
}
credential_vault
.attest_provider_identifier(&tool.name)
.map_err(anyhow::Error::msg)?;
let mut safe = tool.clone();
safe.description = credential_vault.vault_in_text(&tool.description);
safe.input_schema = vault_provider_schema(&tool.input_schema, credential_vault)?;
Expand All @@ -3912,9 +3902,9 @@ fn vault_provider_tools(
// only as a schema key or tool name. Validate structural names again with
// the complete vault before handing the definitions to the provider.
for safe in &mut safe_tools {
if credential_vault.vault_in_text(&safe.name) != safe.name {
anyhow::bail!("credential detected in provider tool name");
}
credential_vault
.attest_provider_identifier(&safe.name)
.map_err(anyhow::Error::msg)?;
safe.description = credential_vault.vault_in_text(&safe.description);
safe.input_schema = vault_provider_schema(&safe.input_schema, credential_vault)?;
}
Expand All @@ -3933,10 +3923,18 @@ fn vault_provider_schema(value: &Value, credential_vault: &CredentialVault) -> R
Value::Object(entries) => {
let mut safe = serde_json::Map::new();
for (key, value) in entries {
if credential_vault.vault_in_text(key) != *key {
anyhow::bail!("credential detected in provider tool schema key");
}
safe.insert(key.clone(), vault_provider_schema(value, credential_vault)?);
credential_vault
.attest_provider_identifier(key)
.map_err(anyhow::Error::msg)?;
safe.insert(
key.clone(),
if provider_payload::schema_protocol_keyword(key) {
provider_payload::attest_protocol_json(value, credential_vault)?;
value.clone()
} else {
vault_provider_schema(value, credential_vault)?
},
);
}
Ok(Value::Object(safe))
}
Expand Down
Loading
Loading