mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-11 03:39:49 +08:00
fix(ws): harden Responses connection lifecycle
Revalidate control policy per turn, isolate downstream credentials, and make planner/turn ownership cancellation-safe. Preserve opaque protocol events, align configurable timeout semantics, and extend end-to-end security and settlement coverage.
This commit is contained in:
@@ -37,7 +37,11 @@ impl ResponsesStructuredTerminalObserver {
|
||||
/// 第一个被拒绝的事件就停止推进并把摘要标成 parser_error:解析器的状态机是
|
||||
/// 有顺序的,跳过一个事件继续喂后面的只会得到更没意义的摘要。
|
||||
pub(super) fn observe_events(&mut self, report_context: &Value, events: &[&Value]) {
|
||||
for event in events {
|
||||
for event in events
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|event| event_is_relevant_to_terminal_observation(event))
|
||||
{
|
||||
if let Err(error) = self.inner.push_event(report_context, event) {
|
||||
self.inner.disable_with_error(error.to_string());
|
||||
break;
|
||||
@@ -61,6 +65,28 @@ impl ResponsesStructuredTerminalObserver {
|
||||
}
|
||||
}
|
||||
|
||||
/// The WebSocket relay is not a Responses schema gateway. It forwards all
|
||||
/// events opaquely, while this observer consumes only identity/terminal
|
||||
/// snapshots needed for usage and settlement. In particular, a future
|
||||
/// `response.*` delta must not become an observation failure merely because
|
||||
/// Aether's canonical streaming parser does not know it yet.
|
||||
fn event_is_relevant_to_terminal_observation(event: &Value) -> bool {
|
||||
matches!(
|
||||
event.get("type").and_then(Value::as_str),
|
||||
Some(
|
||||
"response.created"
|
||||
| "response.in_progress"
|
||||
| "response.queued"
|
||||
| "response.completed"
|
||||
| "response.done"
|
||||
| "response.failed"
|
||||
| "response.incomplete"
|
||||
| "response.cancelled"
|
||||
| "error"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
@@ -141,6 +167,45 @@ mod tests {
|
||||
assert_eq!(usage.dimensions.get("total_tokens"), Some(&json!(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn future_and_provider_private_events_are_ignored_only_by_the_side_observer() {
|
||||
let context = report_context();
|
||||
let private = json!({
|
||||
"type": "codex.response.metadata",
|
||||
"private_future_field": {"shape": "unknown"},
|
||||
});
|
||||
let future = json!({
|
||||
"type": "response.future_capability.delta",
|
||||
"future_capability": {"nested": [1, 2, 3]},
|
||||
});
|
||||
let completed = json!({
|
||||
"type": "response.completed",
|
||||
"response": {
|
||||
"id": "resp_future",
|
||||
"model": "future-model",
|
||||
"status": "completed",
|
||||
"future_response_field": {"also": "unknown"},
|
||||
"usage": {"input_tokens": 5, "output_tokens": 2, "total_tokens": 7},
|
||||
},
|
||||
});
|
||||
|
||||
let mut observer = ResponsesStructuredTerminalObserver::default();
|
||||
observer.observe_events(&context, &[&private, &future, &completed]);
|
||||
let summary = observer.finish(&context);
|
||||
|
||||
assert!(summary.observed_finish);
|
||||
assert_eq!(summary.response_id.as_deref(), Some("resp_future"));
|
||||
assert_eq!(summary.unknown_event_count, 0);
|
||||
assert_eq!(
|
||||
summary
|
||||
.standardized_usage
|
||||
.as_ref()
|
||||
.map(|usage| (usage.input_tokens, usage.output_tokens)),
|
||||
Some((5, 2))
|
||||
);
|
||||
assert!(summary.parser_error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_disabled_observer_reports_the_parser_error() {
|
||||
let context = report_context();
|
||||
|
||||
Reference in New Issue
Block a user