refactor(plugins): remove provider-specific request parameters

Use request, attempt, and outbound middleware for plugin-owned identity
selection and complete request rewriting. Preserve native account identity
and partition WebSocket connections through existing outbound opening changes.

Remove the construction parameter channel and its obsolete tests and docs.
Keep middleware at v6 without a compatibility shim.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-10-25 03:30:52 +08:00
co-authored by Codex
parent 9ccd84f1a0
commit 3c05bc8f07
27 changed files with 63 additions and 685 deletions
@@ -104,7 +104,6 @@ impl fmt::Debug for MiddlewareHeader {
#[derive(Clone, PartialEq, Eq)]
pub struct MiddlewareRequest {
settings: Option<RequestSettings>,
provider_parameters: Option<serde_json::Map<String, serde_json::Value>>,
protocol: String,
headers: Vec<MiddlewareHeader>,
body: Bytes,
@@ -126,7 +125,6 @@ impl MiddlewareRequest {
let protocol = protocol.into();
Self {
settings: None,
provider_parameters: None,
original_protocol: protocol.clone(),
original_body: body.clone(),
capabilities: None,
@@ -147,27 +145,6 @@ impl MiddlewareRequest {
self.settings.as_ref()
}
/// Provider 提供的本次构造参数;Core 不解释其中的业务字段
#[must_use]
pub fn provider_parameters(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
self.provider_parameters.as_ref()
}
#[must_use]
pub fn with_provider_parameters(
mut self,
parameters: serde_json::Map<String, serde_json::Value>,
) -> Self {
self.provider_parameters = Some(parameters);
self
}
pub fn take_provider_parameters(
&mut self,
) -> Option<serde_json::Map<String, serde_json::Value>> {
self.provider_parameters.take()
}
#[must_use]
pub fn protocol(&self) -> &str {
&self.protocol
@@ -755,7 +755,6 @@ impl AttemptContext {
provider: ProviderKind,
model: Option<String>,
account_id: ProviderAccountId,
parameters: Option<serde_json::Map<String, serde_json::Value>>,
terminal: provider::ProviderMiddlewareTerminal,
) -> Result<provider::ProviderStream, ProviderError> {
// 入口已校验原请求,此处复查中间件最终提交的 operation,每次重试均生效
@@ -763,7 +762,7 @@ impl AttemptContext {
.account_scope()
.is_some_and(|scope| !scope.allows_image_generation())
{
Box::new(move |operation, headers, parameters| {
Box::new(move |operation, headers| {
Box::pin(async move {
image_generation::check_image_generation(false, &operation).map_err(|_| {
ProviderError::new(
@@ -772,7 +771,7 @@ impl AttemptContext {
)
.with_retry_prohibited()
})?;
terminal(operation, headers, parameters).await
terminal(operation, headers).await
})
})
} else {
@@ -790,7 +789,6 @@ impl AttemptContext {
context,
operation,
self.request.client_transport,
parameters,
terminal,
)
.await
@@ -471,7 +471,6 @@ pub type ProviderMiddlewareTerminal = Box<
dyn FnOnce(
Operation,
Vec<MiddlewareHeader>,
Option<serde_json::Map<String, serde_json::Value>>,
) -> BoxFuture<'static, Result<ProviderStream, ProviderError>>
+ Send
+ 'static,
@@ -490,19 +489,18 @@ fn provider_middleware_next(
activate: oneshot::Receiver<()>,
observation: Arc<Mutex<MiddlewareSendObservation>>,
) -> MiddlewareNext {
crate::middleware::compose(Vec::new(), move |mut request| {
crate::middleware::compose(Vec::new(), move |request| {
Box::pin(async move {
let fallback_protocol = operation.protocol().to_owned();
if request.has_capability_declaration() {
return Err(MiddlewareError::InvalidState);
}
let parameters = request.take_provider_parameters();
let (protocol, headers, body) = request.into_parts();
validate_attempt_headers(&headers)?;
let operation = operation
.replace_middleware_wire(protocol, body)
.map_err(|_| MiddlewareError::InvalidState)?;
let stream = terminal(operation, headers, parameters)
let stream = terminal(operation, headers)
.await
.map_err(MiddlewareError::Provider)?;
let metadata = stream.metadata().clone();
@@ -736,22 +734,18 @@ pub async fn execute_attempt_middleware(
context: MiddlewareContext,
operation: Operation,
transport: ClientTransport,
parameters: Option<serde_json::Map<String, serde_json::Value>>,
terminal: ProviderMiddlewareTerminal,
) -> Result<ProviderStream, ProviderError> {
let Some(plan) = plan else {
return terminal(operation, Vec::new(), parameters).await;
return terminal(operation, Vec::new()).await;
};
let mut request = MiddlewareRequest::new(
let request = MiddlewareRequest::new(
operation.protocol(),
Vec::<MiddlewareHeader>::new(),
operation
.middleware_body()
.map_err(|_| middleware_protocol_error(UpstreamSendState::NotSent))?,
);
if let Some(parameters) = parameters {
request = request.with_provider_parameters(parameters);
}
let (prepared, metadata) = oneshot::channel();
let (activate, activation) = oneshot::channel();
let observation = Arc::new(Mutex::new(MiddlewareSendObservation {
@@ -103,8 +103,7 @@ impl Provider for PreparingProvider {
provider,
model,
account,
None,
Box::new(move |_, _, _| {
Box::new(move |_, _| {
Box::pin(async move {
let events = futures::stream::once(async move {
Err::<ProviderEvent, _>(
@@ -88,7 +88,7 @@ fn plan(action: Action) -> FrozenMiddlewarePlan {
}
fn terminal(polls: Arc<AtomicUsize>, released: Arc<AtomicUsize>) -> ProviderMiddlewareTerminal {
Box::new(move |_, _, _| {
Box::new(move |_, _| {
Box::pin(async move {
let metadata = ProviderCallMetadata::new(
ProviderKind::new("openai").unwrap(),
@@ -127,7 +127,6 @@ fn inspecting_a_response_waits_for_the_outer_stream_and_keeps_send_facts() {
context(),
operation(),
ClientTransport::HttpJson,
None,
terminal(polls.clone(), released.clone()),
)
.await
@@ -166,7 +165,6 @@ fn rejecting_metadata_drops_the_unactivated_handler_and_account_lease() {
context(),
operation(),
ClientTransport::HttpJson,
None,
terminal(polls.clone(), released.clone()),
)
.await
@@ -196,8 +194,7 @@ fn middleware_rejection_before_next_does_not_wait_for_activation() {
context(),
operation(),
ClientTransport::HttpJson,
None,
Box::new(|_, _, _| panic!("rejected middleware cannot prepare upstream")),
Box::new(|_, _| panic!("rejected middleware cannot prepare upstream")),
)
.await
.err()
@@ -340,7 +340,7 @@ fn attempt_middleware_consumes_owned_next_once_and_preserves_host_envelopes() {
let terminal: ProviderMiddlewareTerminal = {
let terminal_calls = Arc::clone(&terminal_calls);
Box::new(move |operation, headers, _| {
Box::new(move |operation, headers| {
terminal_calls.fetch_add(1, Ordering::SeqCst);
Box::pin(async move {
assert!(headers.is_empty());
@@ -392,7 +392,6 @@ fn attempt_middleware_consumes_owned_next_once_and_preserves_host_envelopes() {
context(),
operation(),
ClientTransport::HttpJson,
None,
terminal,
)
.await
@@ -487,7 +486,7 @@ fn attempt_passthrough_preserves_parsed_wire_for_every_client_transport() {
payload.clone(),
)
.unwrap();
let terminal: ProviderMiddlewareTerminal = Box::new(move |_, _, _| {
let terminal: ProviderMiddlewareTerminal = Box::new(move |_, _| {
Box::pin(async move {
Ok(ProviderStream::new(
empty_provider_stream().metadata().clone(),
@@ -501,7 +500,6 @@ fn attempt_passthrough_preserves_parsed_wire_for_every_client_transport() {
context(),
operation(),
transport,
None,
terminal,
)
.await
@@ -569,7 +567,7 @@ fn attempt_sse_encoding_keeps_opaque_metadata_from_creating_extra_frames() {
Some(10),
)
.unwrap();
let terminal: ProviderMiddlewareTerminal = Box::new(move |_, _, _| {
let terminal: ProviderMiddlewareTerminal = Box::new(move |_, _| {
Box::pin(async move {
Ok(ProviderStream::new(
empty_provider_stream().metadata().clone(),
@@ -584,7 +582,6 @@ fn attempt_sse_encoding_keeps_opaque_metadata_from_creating_extra_frames() {
context(),
operation(),
ClientTransport::HttpSse,
None,
terminal,
)
.await
@@ -599,7 +596,7 @@ fn attempt_middleware_preserves_multi_value_and_authentication_headers() {
let allowed_calls = Arc::new(AtomicUsize::new(0));
let allowed_terminal: ProviderMiddlewareTerminal = {
let allowed_calls = Arc::clone(&allowed_calls);
Box::new(move |_operation, headers, _| {
Box::new(move |_operation, headers| {
allowed_calls.fetch_add(1, Ordering::SeqCst);
Box::pin(async move {
assert_eq!(headers.len(), 2);
@@ -627,7 +624,6 @@ fn attempt_middleware_preserves_multi_value_and_authentication_headers() {
context(),
operation(),
ClientTransport::HttpJson,
None,
allowed_terminal,
)
.await
@@ -637,7 +633,7 @@ fn attempt_middleware_preserves_multi_value_and_authentication_headers() {
let calls = Arc::new(AtomicUsize::new(0));
let terminal: ProviderMiddlewareTerminal = {
let calls = calls.clone();
Box::new(move |_, headers, _| {
Box::new(move |_, headers| {
calls.fetch_add(1, Ordering::SeqCst);
Box::pin(async move {
assert_eq!(headers[0].name(), "authorization");
@@ -661,7 +657,6 @@ fn attempt_middleware_preserves_multi_value_and_authentication_headers() {
context(),
operation(),
ClientTransport::HttpJson,
None,
terminal,
)
.await
@@ -741,8 +736,7 @@ fn attempt_middleware_cannot_add_image_generation_to_a_denied_scope() {
ProviderKind::new("openai").unwrap(),
Some("gpt-test".into()),
ProviderAccountId::new("acct_middleware").unwrap(),
None,
Box::new(move |operation, _, _| {
Box::new(move |operation, _| {
Box::pin(async move {
assert!(operation.image_generation_requested());
count.fetch_add(1, Ordering::SeqCst);
@@ -98,7 +98,6 @@ pub(super) async fn invoke(
scope,
)?;
let head = MiddlewareRequestHead {
provider_parameters: invocation.provider_parameters(),
settings_sources: invocation.settings_sources(),
settings: invocation
.settings()
@@ -135,15 +135,6 @@ pub(crate) enum MiddlewareCompletionBody {
}
impl MiddlewareInvocation {
pub(crate) fn provider_parameters(&self) -> Option<serde_json::Map<String, serde_json::Value>> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.original_request
.provider_parameters()
.cloned()
}
pub(crate) fn settings(&self) -> Result<serde_json::Value, PluginFault> {
let state = self
.state
@@ -343,12 +334,6 @@ impl MiddlewareInvocation {
request: MiddlewareNextRequest,
payload: Vec<u8>,
) -> Result<MiddlewareRequest, PluginFault> {
if let Some(parameters) = request.provider_parameters {
if original.provider_parameters().is_none() {
return Err(invalid());
}
original = original.with_provider_parameters(parameters);
}
if let Some(settings) = request.settings {
let current = original.settings().ok_or_else(|| {
PluginFault::new(
@@ -71,8 +71,7 @@ async fn reading_websocket_facts_preserves_original_text_and_opaque_messages() {
attempt_middleware_context_for_transport(Arc::default(), ClientTransport::WebSocket),
operation(),
ClientTransport::WebSocket,
None,
Box::new(move |_, _, _| {
Box::new(move |_, _| {
Box::pin(async move {
Ok(ProviderStream::new(
ProviderCallMetadata::new(
@@ -228,8 +227,7 @@ async fn facts_are_readable_without_replacing_or_duplicating_the_original_execut
attempt_middleware_context(Arc::default()),
operation(),
ClientTransport::HttpJson,
None,
Box::new(move |_, _, _| {
Box::new(move |_, _| {
Box::pin(async move {
Ok(ProviderStream::new(
metadata,
@@ -3,72 +3,6 @@
use super::*;
use gateway_admin::ports::plugins::PluginPreparation;
#[tokio::test]
async fn provider_parameters_compose_through_sdk_without_merging_deleted_fields() {
let worker = std::fs::read(env!("CARGO_BIN_EXE_gateway-plugin-test-middleware")).unwrap();
let package = crate::support::package_with_contributions(
&worker,
Contributions::from([crate::support::contribution(
Capability::Middleware,
vec![Stage::Attempt],
vec!["openai".into()],
vec!["openai".into()],
)]),
);
let original = serde_json::json!({"installation_id":"account-default", "future":{"keep":true}});
let first = serde_json::json!({"installation_id":"plugin-first"});
let last = serde_json::json!({});
let instances = [
("parameters-first", original.clone(), first.clone()),
("parameters-last", first, serde_json::Value::Null),
]
.into_iter()
.enumerate()
.map(|(index, (id, expected, parameters))| InstanceFixture {
id,
configuration: serde_json::json!({"mode":"parameters", "attempt":true,
"expected_parameters":expected, "parameters":parameters}),
bindings: vec![binding(
MIDDLEWARE_CONTRIBUTION,
"attempt",
index as i32,
PluginFailurePolicy::Reject,
)],
})
.collect();
let (cache, runtime) = setup_package(instances, package).await;
let generation = prepare(&runtime).await;
let plan = runtime
.execution_registry()
.middleware(&generation)
.unwrap();
let downstream = Downstream {
calls: Arc::default(),
closes: Arc::default(),
reads: Arc::default(),
error: false,
};
let response = plan
.handle(
attempt_middleware_context(Arc::default()),
budget_request().with_provider_parameters(original.as_object().unwrap().clone()),
gateway_core::middleware::compose(Vec::new(), move |request| {
assert_eq!(
request.provider_parameters().unwrap(),
last.as_object().unwrap()
);
downstream.into_next().run(request)
}),
)
.await
.unwrap();
let (_, _, _, body, _) = response.into_parts();
body.close().await;
drop(plan);
drop(generation);
super::super::wait_until_empty(cache.path()).await;
}
#[tokio::test]
async fn partial_update_preserves_inflight_middleware_and_uses_new_configuration_for_new_calls() {
let (cache, store, runtime) = super::super::setup_with_contributions(Contributions::from([
@@ -545,19 +545,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
call.request.head.settings = settings.clone();
}
}
if mode == "parameters" {
assert_eq!(
serde_json::to_value(&call.request.head.provider_parameters).unwrap(),
configuration["expected_parameters"],
);
if let Some(parameters) = configuration.get("parameters") {
call.request.head.provider_parameters = if parameters.is_null() {
None
} else {
Some(parameters.as_object().unwrap().clone())
};
}
}
if mode == "inspect" && !call.request.body.is_empty() {
// 读取和解析只能作用于副本,不能把解析结果写回未改写的正文
assert!(serde_json::from_slice::<serde_json::Value>(&call.request.body).is_ok());
@@ -1046,7 +1046,6 @@ impl Peer {
id,
NEXT_METHOD,
serde_json::to_value(MiddlewareNextRequest {
provider_parameters: None,
settings: None,
protocol: None,
header_mutations: Vec::new(),
@@ -402,7 +402,12 @@ Ping 的自动 Pong 与关闭握手属于底层 WebSocket 协议事实;插件
所有外层处理结束后,Provider 才把结果交给 transport 发送。插件错误、非法传输值或正文超限在原生发送前结束,
不自动跳过、重放或切换传输;取消、请求截止与回调资源沿用当前 attempt
身份策略可在 `request` / `attempt` 读取原始输入,以真实 Key 与账号标识调用私有状态服务维护映射,
再在 `outbound` 自行改写完整请求中的身份及关联字段。UUID 生成、并发创建、持久化、会话与缓存标识派生均由插件负责;
通用中间件层不解释 installation ID、device ID 等策略字段,也不在插件输出后重新派生业务字段
WebSocket 的 `body` 是准备发送的一条完整 UTF-8 文本消息,`headers` 和 `uri` 是候选 opening。
新响应链按出站改写产生的 URI / 握手头差异分区,精确续接仍使用持有原响应链的连接。
复用连接时不能改写已发生的握手;宿主不为插件主动重连、重放消息或重建续接。
插件需要握手身份与消息内容一致时,应结合实际连接与续接语义选择改写
@@ -429,17 +434,6 @@ request 与 attempt 阶段通过 `append_header` 添加的请求头会进入上
OpenAI 与 xAI 的上游 headers 先由账号和画像生成,插件同名字段覆盖基线,同名多值按插件顺序保留;不存在按认证、Cookie 或会话名称划分的保护列表。
实际账号租约、凭据版本与计费归属仍记录真实选中事实,不随插件修改 header 名称而伪造
attempt 的 `request.head.provider_parameters` 携带 Provider 提供的本次构造参数;没有该能力的执行不提供此对象。
OpenAI 原生 Responses、Images、Search 与 Live 创建请求提供 `installation_id` 字符串,默认取自选中账号,
用于原生安装身份、已有 turn metadata 身份字段及会话、缓存关联标识的派生。独立 `upstream_adapter` 自行构造其请求。
插件直接修改此对象,SDK 在值改变时提交完整替换;空对象明确删除全部参数,将原本存在的对象设为 `None` 同样提交空对象。
线协议的 `next.provider_parameters` 省略时保留原值,显式 `null` 或非对象类型拒绝。
后续插件读取前层结果,Core / Runtime 不解释业务字段、不深合并;缺失必需字段或类型错误在发送前失败且禁止重试,不补回账号默认身份。
身份选择、Key 与账号配对、UUID 生成与持久化由插件负责,可复用已有状态服务;参数不写回账号凭据、全局设置或真实计费事实。
WS 新响应链按构造身份区分连接,精确续接仍使用实际持有该响应链的原连接;Live sideband / hangup 不继承创建请求的临时参数
request 阶段 `request.head.settings` 是本次调用的有效设置对象,包含 `runtime`、`fast_mode`、`client_limits` 和 `timeout_ms`。
`fast_mode` 使用 `default`、`enabled`、`disabled` 三态,具体档位语义见[账号分组](../../../../../docs/api.md#6-账号分组)。
`runtime` 使用宿主快照的设置字段,画像已合并 Client Key 设置;可以修改映射、画像、调度、位置及计价等执行参数。
@@ -70,9 +70,6 @@ pub struct MiddlewareRequestHead {
/// 当前请求的完整有效执行设置;尚未建立可改写设置的边界使用 null
#[serde(default)]
pub settings: serde_json::Value,
/// 本次 Provider 构造参数;仅原生 attempt 提供
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_parameters: Option<serde_json::Map<String, serde_json::Value>>,
pub request_id: String,
pub client_key_id: String,
pub account_group_ids: Vec<String>,
@@ -131,13 +128,6 @@ pub struct MiddlewareNextRequest {
deserialize_with = "deserialize_replacement"
)]
pub settings: Option<serde_json::Value>,
/// 完整替换构造参数;空对象仍表示显式替换,不隐式合并或补回删除字段
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_replacement"
)]
pub provider_parameters: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub protocol: Option<String>,
#[serde(default)]
@@ -39,7 +39,6 @@ pub struct MiddlewareRequest {
pub body: Vec<u8>,
original_protocol: String,
original_settings: serde_json::Value,
original_provider_parameters: Option<serde_json::Map<String, serde_json::Value>>,
original_headers: Vec<MiddlewareHeader>,
original_body: Vec<u8>,
body_replaced: bool,
@@ -99,9 +98,6 @@ impl MiddlewareRequest {
};
(
MiddlewareNextRequest {
provider_parameters: (self.head.provider_parameters
!= self.original_provider_parameters)
.then(|| self.head.provider_parameters.unwrap_or_default()),
settings: (self.head.settings != self.original_settings)
.then_some(self.head.settings),
protocol,
@@ -345,7 +341,6 @@ impl RequestCall {
context: call.context,
request: MiddlewareRequest {
original_settings: head.settings.clone(),
original_provider_parameters: head.provider_parameters.clone(),
body: call.payload,
original_body,
body_replaced: false,
@@ -13,37 +13,6 @@ use gateway_plugin_sdk::{
};
use serde_json::json;
#[test]
fn provider_parameters_distinguish_preserve_empty_and_invalid_replacement() {
let preserved: MiddlewareNextRequest =
serde_json::from_value(json!({"body":"preserve"})).unwrap();
assert!(preserved.provider_parameters.is_none());
for replacement in [json!({}), json!({"installation_id":null})] {
let request: MiddlewareNextRequest = serde_json::from_value(json!({
"body":"preserve", "provider_parameters":replacement,
}))
.unwrap();
assert_eq!(
request.provider_parameters.as_ref(),
replacement.as_object()
);
}
for invalid in [
json!(null),
json!([]),
json!("invalid"),
json!(42),
json!(false),
] {
assert!(
serde_json::from_value::<MiddlewareNextRequest>(json!({
"body":"preserve", "provider_parameters":invalid,
}))
.is_err()
);
}
}
#[test]
fn settings_replacement_preserves_explicit_null_for_host_validation() {
let preserved: MiddlewareNextRequest =
@@ -75,7 +44,6 @@ fn middleware_capability_and_mounts_have_one_stable_wire_vocabulary() {
#[test]
fn middleware_request_keeps_body_out_of_json_and_rejects_unknown_fields() {
let request = MiddlewareRequestHead {
provider_parameters: None,
settings_sources: serde_json::Value::Null,
settings: serde_json::Value::Null,
client_key_id: "fixture-key".into(),
@@ -113,7 +81,6 @@ fn middleware_request_keeps_body_out_of_json_and_rejects_unknown_fields() {
#[test]
fn next_distinguishes_preserve_from_replace_empty_and_uses_incremental_headers() {
let preserve = MiddlewareNextRequest {
provider_parameters: None,
settings: None,
protocol: None,
header_mutations: Vec::new(),
@@ -126,7 +93,6 @@ fn next_distinguishes_preserve_from_replace_empty_and_uses_incremental_headers()
);
let replace = MiddlewareNextRequest {
provider_parameters: None,
settings: None,
protocol: Some("xai".into()),
header_mutations: vec![
@@ -391,7 +391,6 @@ fn middleware_context(id: u64) -> CallContext {
pub(super) fn middleware_request() -> MiddlewareRequestHead {
MiddlewareRequestHead {
provider_parameters: None,
settings_sources: serde_json::Value::Null,
settings: serde_json::Value::Null,
client_key_id: "fixture-key".into(),
@@ -179,8 +179,7 @@ impl CodexProvider {
provider_kind,
requested_model.map(|model| model.as_str().to_owned()),
account_id,
Some(RequestParameters::baseline(lease.installation_id())),
Box::new(move |operation, middleware_headers, parameters| {
Box::new(move |operation, middleware_headers| {
Box::pin(async move {
provider
.execute_selected_raw_json_endpoint(
@@ -188,11 +187,8 @@ impl CodexProvider {
operation,
middleware_headers,
request,
SelectedRequest {
parameters: RequestParameters::decode(parameters)?,
lease,
account_selection_wait_ms,
},
lease,
account_selection_wait_ms,
)
.await
})
@@ -207,13 +203,9 @@ impl CodexProvider {
operation: Operation,
middleware_headers: Vec<MiddlewareHeader>,
mut request: RawJsonEndpointRequest,
selected: SelectedRequest,
mut lease: CodexCredentialLease,
account_selection_wait_ms: u64,
) -> Result<ProviderStream, ProviderError> {
let SelectedRequest {
parameters,
mut lease,
account_selection_wait_ms,
} = selected;
let affinity = match &operation {
Operation::GenerateImage(image) => {
let model = endpoint_requested_model(image.payload());
@@ -337,7 +329,7 @@ impl CodexProvider {
lease.capacity_snapshot(),
));
// Standalone Provider 端点没有可证明的账号 owner;Images/Search metadata 必须按
// 跨账号输入收敛到本次构造参数,不能沿用下游声明的账号身份
// 跨账号输入收敛到当前 lease,不能沿用下游声明的账号或 installation identity
let turn_metadata = payload
.context()
.get("turn_metadata")
@@ -345,12 +337,11 @@ impl CodexProvider {
.and_then(|metadata| {
crate::transport::request::scope_turn_metadata(
metadata,
&parameters.installation_id,
lease.installation_id(),
true,
)
});
let events = cold_json_response_stream(ColdJsonResponse {
parameters,
client: self
.client_for_request(&context)?
.for_account(lease.account())
@@ -392,7 +383,6 @@ struct RawJsonEndpointRequest {
}
pub(super) struct ColdResponse {
pub(super) parameters: RequestParameters,
pub(super) client: CodexBackendClient,
pub(super) response_origin: Url,
pub(super) request: CodexResponsesRequest,
@@ -412,7 +402,6 @@ pub(super) struct ColdResponse {
}
pub(super) struct ColdJsonResponse {
pub(super) parameters: RequestParameters,
pub(super) client: CodexBackendClient,
pub(super) response_origin: Url,
pub(super) endpoint_path: &'static str,
@@ -657,7 +646,7 @@ pub(super) fn cold_json_response_stream(request: ColdJsonResponse) -> EventStrea
let response = create_json_attempt(
&request,
&active_account,
&request.parameters.installation_id,
request.lease.installation_id(),
&authorization,
cookie_header.as_ref(),
account_selection,
@@ -776,7 +765,6 @@ fn image_response_metering(
pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream {
let ColdResponse {
parameters,
client,
response_origin,
request,
@@ -833,7 +821,7 @@ pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream {
&request,
&request_id,
&active_account,
&parameters.installation_id,
lease.installation_id(),
&authorization,
cookie_header.as_ref(),
account_selection,
@@ -280,8 +280,7 @@ impl CodexProvider {
provider_kind,
Some(upstream_model.as_str().to_owned()),
account_id,
Some(RequestParameters::baseline(lease.installation_id())),
Box::new(move |operation, middleware_headers, parameters| {
Box::new(move |operation, middleware_headers| {
Box::pin(async move {
provider
.execute_selected_live_call(
@@ -289,11 +288,8 @@ impl CodexProvider {
operation,
upstream_model,
middleware_headers,
SelectedRequest {
parameters: RequestParameters::decode(parameters)?,
lease,
account_selection_wait_ms,
},
lease,
account_selection_wait_ms,
)
.await
})
@@ -308,13 +304,9 @@ impl CodexProvider {
operation: Operation,
upstream_model: UpstreamModelId,
middleware_headers: Vec<MiddlewareHeader>,
selected: SelectedRequest,
mut lease: CodexCredentialLease,
account_selection_wait_ms: u64,
) -> Result<ProviderStream, ProviderError> {
let SelectedRequest {
parameters,
mut lease,
account_selection_wait_ms,
} = selected;
let Operation::ProviderHttp(request) = operation else {
return Err(provider_error(
ProviderErrorKind::InvalidRequest,
@@ -374,7 +366,6 @@ impl CodexProvider {
.cloned()
.collect::<Vec<_>>();
let events = cold_live_call_stream(ColdLiveCall {
parameters,
client: self
.client_for_request(&context)?
.for_account(lease.account())
@@ -407,7 +398,6 @@ impl CodexProvider {
}
struct ColdLiveCall {
parameters: RequestParameters,
client: CodexBackendClient,
registry: Arc<CodexLiveRegistry>,
response_origin: Url,
@@ -451,7 +441,7 @@ fn cold_live_call_stream(request: ColdLiveCall) -> EventStream {
authorization.expose_secret(),
active_account.upstream_account_id(),
&request_id,
Some(&request.parameters.installation_id),
Some(request.lease.installation_id()),
);
request_context.trace = Some(&trace);
request_context.account_selection = CodexAccountSelectionTelemetry::new(
@@ -107,7 +107,6 @@ mod execution;
mod failure;
mod live;
mod observation;
mod parameters;
mod upstream_adapter;
mod workers;
pub(crate) use live::{CodexLiveGateway, CodexLiveRegistry};
@@ -118,7 +117,6 @@ use execution::*;
pub use failure::openai_failure_affects_account_score;
use failure::*;
use observation::*;
use parameters::RequestParameters;
pub(crate) use workers::worker_contributions;
const PROVIDER_NAME: &str = "openai";
@@ -221,14 +219,9 @@ impl PreparedGenerateRequest {
}
struct SelectedGenerate {
request: SelectedRequest,
frozen_requirements: CapabilityRequirements,
}
struct SelectedRequest {
lease: CodexCredentialLease,
account_selection_wait_ms: u64,
parameters: RequestParameters,
frozen_requirements: CapabilityRequirements,
}
impl CodexProvider {
@@ -756,10 +749,7 @@ impl Provider for CodexProvider {
provider_kind,
Some(upstream_model.as_str().to_owned()),
account_id,
adapter
.is_none()
.then(|| RequestParameters::baseline(lease.installation_id())),
Box::new(move |operation, middleware_headers, parameters| {
Box::new(move |operation, middleware_headers| {
Box::pin(async move {
if let Some(adapter) = adapter {
return provider.execute_upstream_adapter(
@@ -780,11 +770,8 @@ impl Provider for CodexProvider {
terminal_model,
terminal_context,
SelectedGenerate {
request: SelectedRequest {
parameters: RequestParameters::decode(parameters)?,
lease,
account_selection_wait_ms,
},
lease,
account_selection_wait_ms,
frozen_requirements,
},
)
@@ -806,12 +793,8 @@ impl CodexProvider {
selected: SelectedGenerate,
) -> Result<ProviderStream, ProviderError> {
let SelectedGenerate {
request:
SelectedRequest {
parameters,
mut lease,
account_selection_wait_ms,
},
mut lease,
account_selection_wait_ms,
frozen_requirements,
} = selected;
let Operation::Generate(generate) = operation else {
@@ -951,7 +934,7 @@ impl CodexProvider {
.then(|| crate::transport::diagnostics::evidence::request_state(&upstream_request));
scope_request_to_account(
&mut upstream_request,
&parameters.installation_id,
lease.installation_id(),
account_scope,
);
if let Some(before) = state_before {
@@ -1106,7 +1089,6 @@ impl CodexProvider {
AttemptTransport::Default | AttemptTransport::Fallback => 0,
};
let events = cold_response_stream(ColdResponse {
parameters,
client: self
.client_for_request(&context)?
.for_account(lease.account())
@@ -1,35 +0,0 @@
//! OpenAI 原生请求的构造参数;只解释本次值,不维护插件身份策略
use gateway_core::{
error::{ProviderError, ProviderErrorKind},
upstream::UpstreamSendState,
};
use serde::Deserialize;
use serde_json::{Map, Value};
#[derive(Deserialize)]
pub(super) struct RequestParameters {
pub(super) installation_id: String,
}
impl RequestParameters {
pub(super) fn baseline(installation_id: &str) -> Map<String, Value> {
Map::from_iter([(
"installation_id".into(),
Value::String(installation_id.to_owned()),
)])
}
pub(super) fn decode(parameters: Option<Map<String, Value>>) -> Result<Self, ProviderError> {
let invalid = || {
ProviderError::new(
ProviderErrorKind::InvalidRequest,
UpstreamSendState::NotSent,
)
.with_retry_prohibited()
};
// 参数基线已在选号后提供,显式删除和类型错误不能回退为账号默认身份
serde_json::from_value(Value::Object(parameters.ok_or_else(invalid)?))
.map_err(|_| invalid())
}
}
@@ -622,10 +622,7 @@ impl CodexBackendClient {
let mut key = CodexWebSocketPoolKey::new(&self.base_url, account_id, conversation_id)
.with_egress_key(&self.egress_key)
.with_outbound_proxy(self.outbound_proxy.as_ref())
.with_connection_profile(format!(
"{connection_profile}\0{}",
context.installation_id.unwrap_or_default(),
));
.with_connection_profile(connection_profile);
if let Some(connection_id) = request.downstream_websocket_connection_id.as_deref() {
key = key.with_downstream_connection_id(connection_id);
}
@@ -2,337 +2,6 @@
use super::*;
#[derive(Debug)]
struct ParametersMiddleware(Map<String, Value>);
impl MiddlewarePlan for ParametersMiddleware {
fn handle(
&self,
context: MiddlewareContext,
request: MiddlewareRequest,
next: MiddlewareNext,
) -> BoxFuture<'static, Result<MiddlewareResponse, MiddlewareError>> {
assert_eq!(context.mount(), MiddlewareMount::Attempt);
assert!(request.provider_parameters().unwrap()["installation_id"].is_string());
next.run(request.with_provider_parameters(self.0.clone()))
}
}
#[tokio::test]
async fn request_parameters_drive_native_identity_and_derivation_across_http_and_websocket() {
let store = Arc::new(MemoryAccountStore::default());
create_account(&store, "acct_provider_contract").await;
let identities = [
"f77b9b8e-563a-4d84-a66f-f2a0c98d5a8e",
"f6e974b5-708d-4940-b619-1ab2c58c2f2c",
];
let mut captures = Vec::new();
for identity in identities {
for websocket in [false, true] {
let (base_url, capture): (
String,
BoxFuture<'static, (reqwest::header::HeaderMap, Value)>,
) = if websocket {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
let task = tokio::spawn(async move {
let (socket, _) = listener.accept().await.unwrap();
let mut headers = None;
let mut socket =
crate::transport::accept_codex_test_websocket_with(socket, |request, _| {
headers = Some(request.headers().clone())
})
.await;
let frame = socket.next().await.unwrap().unwrap();
let body = serde_json::from_str(frame.to_text().unwrap()).unwrap();
socket.send(Message::Text(json!({"type":"response.completed", "response":{
"id":"resp_parameters", "model":"gpt-5.4", "status":"completed", "output":[]
}}).to_string().into())).await.unwrap();
(headers.unwrap(), body)
});
(url, Box::pin(async { task.await.unwrap() }))
} else {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/codex/responses"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "text/event-stream")
.set_body_string(CAPTURE_COMPLETED_SSE),
)
.expect(1)
.mount(&server)
.await;
(
server.uri(),
Box::pin(async move {
let requests = server.received_requests().await.unwrap();
(
requests[0].headers.clone(),
captured_request_body(&requests[0]),
)
}),
)
};
let payload = payload(websocket);
let mut request_context = payload.context().clone();
let mut turn_metadata = metadata();
turn_metadata["installation_id"] = json!("client-installation");
request_context.insert("turn_metadata".into(), json!(turn_metadata.to_string()));
let mut stream = provider_with_base_url(&store, base_url)
.execute(
planned_request(
"openai",
Operation::Generate(GenerateRequest::from_protocol_payload(
payload.with_context(request_context),
)),
),
context_with_middleware(
"req_parameters",
Arc::new(ParametersMiddleware(Map::from_iter([(
"installation_id".into(),
json!(identity),
)]))),
FastMode::Default,
),
)
.await
.unwrap();
assert_eq!(
stream.metadata().provider_account_id().as_str(),
"acct_provider_contract"
);
while let Some(event) = stream.next().await {
event.unwrap();
}
let (headers, body) = capture.await;
assert_eq!(body["client_metadata"]["x-codex-installation-id"], identity);
let metadata: Value =
serde_json::from_str(headers["x-codex-turn-metadata"].to_str().unwrap()).unwrap();
assert_eq!(metadata["installation_id"], identity);
assert_eq!(
headers["session-id"].to_str().unwrap(),
body["client_metadata"]["session_id"]
);
captures.push((
body["client_metadata"]["session_id"].clone(),
body["prompt_cache_key"].clone(),
));
}
}
assert_eq!(captures[0], captures[1]);
assert_eq!(captures[2], captures[3]);
assert_ne!(captures[0].0, captures[2].0);
assert_ne!(captures[0].1, captures[2].1);
}
#[tokio::test]
async fn request_parameters_partition_new_websocket_chains_without_preventing_reuse() {
let store = Arc::new(MemoryAccountStore::default());
create_account(&store, "acct_provider_contract").await;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
let identities = ["parameters-first", "parameters-second"];
let server = tokio::spawn(async move {
let mut retained = Vec::new();
for (index, identity) in identities.into_iter().enumerate() {
let (socket, _) = tokio::time::timeout(Duration::from_secs(3), listener.accept())
.await
.unwrap()
.unwrap();
let mut socket =
crate::transport::accept_codex_test_websocket_with(socket, |request, _| {
let metadata: Value = serde_json::from_str(
request.headers()["x-codex-turn-metadata"].to_str().unwrap(),
)
.unwrap();
assert_eq!(metadata["installation_id"], identity);
})
.await;
for turn in 0..=index {
let text = tokio::time::timeout(Duration::from_secs(3), async {
loop {
match socket.next().await.unwrap().unwrap() {
Message::Text(text) => break text,
Message::Ping(payload) => {
socket.send(Message::Pong(payload)).await.unwrap()
}
message => panic!("unexpected websocket frame: {message:?}"),
}
}
})
.await
.unwrap();
let body: Value = serde_json::from_str(&text).unwrap();
assert_eq!(body["client_metadata"]["x-codex-installation-id"], identity);
socket.send(Message::Text(json!({"type":"response.completed", "response":{
"id":format!("resp_parameters_{index}_{turn}"), "model":"gpt-5.4", "status":"completed", "output":[]
}}).to_string().into())).await.unwrap();
}
// 保持旧连接可复用,证明新身份的隔离不是由测试服务端断开触发
retained.push(socket);
}
});
let provider = provider_with_base_url(&store, url);
for identity in [identities[0], identities[1], identities[1]] {
let payload = payload(true);
let mut request_context = payload.context().clone();
request_context.insert(
"turn_metadata".into(),
json!(json!({"installation_id":"client-installation"}).to_string()),
);
let mut stream = Arc::clone(&provider)
.execute(
planned_request(
"openai",
Operation::Generate(
GenerateRequest::from_protocol_payload(
payload.with_context(request_context),
)
.with_provider_session_state(
ProviderSessionState::new(
"openai",
Map::from_iter([
("account_id".into(), json!("acct_provider_contract")),
("conversation_id".into(), json!("parameters-conversation")),
("continuation_scope".into(), json!("persisted")),
]),
)
.unwrap(),
),
),
),
context_with_middleware(
"req_parameters_pool",
Arc::new(ParametersMiddleware(Map::from_iter([(
"installation_id".into(),
json!(identity),
)]))),
FastMode::Default,
),
)
.await
.unwrap();
tokio::time::timeout(Duration::from_secs(5), async {
while let Some(event) = stream.next().await {
event.unwrap();
}
})
.await
.unwrap();
}
server.await.unwrap();
}
#[tokio::test]
async fn request_parameters_apply_to_images_and_search_without_reencoding_opaque_bodies() {
let store = Arc::new(MemoryAccountStore::default());
create_account(&store, "acct_provider_contract").await;
let server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"data":[]})))
.expect(3)
.mount(&server)
.await;
let provider = provider_with_base_url(&store, server.uri());
let body = Bytes::from_static(br#"{ "future":1, "future":2 }"#);
let raw = RawJsonPayload::new("openai", body.clone())
.unwrap()
.with_context(Map::from_iter([(
"turn_metadata".into(),
json!(json!({"installation_id":"client-installation"}).to_string()),
)]));
for operation in [
Operation::GenerateImage(ImageRequest::from_raw_json(
ImageRequestKind::Generation,
raw.clone(),
)),
Operation::GenerateImage(ImageRequest::from_raw_json(
ImageRequestKind::Edit,
raw.clone(),
)),
Operation::Search(StandaloneSearchRequest::from_raw_json(raw)),
] {
let mut stream = Arc::clone(&provider)
.execute(
planned_provider_endpoint_request("openai", operation),
context_with_middleware(
"req_parameters_json",
Arc::new(ParametersMiddleware(Map::from_iter([(
"installation_id".into(),
json!("plugin-installation"),
)]))),
FastMode::Default,
),
)
.await
.unwrap();
while let Some(event) = stream.next().await {
event.unwrap();
}
}
for request in server.received_requests().await.unwrap() {
assert_eq!(request.body.as_slice(), body.as_ref());
let metadata: Value =
serde_json::from_str(request.headers["x-codex-turn-metadata"].to_str().unwrap())
.unwrap();
assert_eq!(metadata["installation_id"], "plugin-installation");
}
}
#[tokio::test]
async fn invalid_request_parameters_fail_without_account_identity_fallback_or_send() {
let store = Arc::new(MemoryAccountStore::default());
create_account(&store, "acct_provider_contract").await;
let server = MockServer::start().await;
let provider = provider_with_base_url(&store, server.uri());
for parameters in [
json!({}),
json!({"installation_id":null}),
json!({"installation_id":42}),
] {
let error = provider
.clone()
.execute(
planned_request("openai", http_generate_operation()),
context_with_middleware(
"req_invalid_parameters",
Arc::new(ParametersMiddleware(
parameters.as_object().unwrap().clone(),
)),
FastMode::Default,
),
)
.await
.err()
.expect("invalid parameters must fail");
assert_eq!(error.kind(), ProviderErrorKind::InvalidRequest);
assert_eq!(error.send_state(), UpstreamSendState::NotSent);
assert!(error.retry_is_prohibited());
}
assert!(server.received_requests().await.unwrap().is_empty());
Mock::given(method("POST"))
.and(path("/codex/responses"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "text/event-stream")
.set_body_string(CAPTURE_COMPLETED_SSE),
)
.expect(1)
.mount(&server)
.await;
let mut stream = provider
.execute(
planned_request("openai", http_generate_operation()),
context("req_after_invalid_parameters", CancellationToken::new()),
)
.await
.unwrap();
while let Some(event) = stream.next().await {
event.unwrap();
}
}
fn metadata() -> Value {
json!({"session_id":"source-session", "thread_id":"source-thread", "turn_id":"source-turn",
"parent_thread_id":"source-session", "forked_from_thread_id":"source-session",
@@ -9,8 +9,8 @@ use gateway_core::account::{
AccountModelAccess, AccountModelAccessMode, FastMode, ProviderAccountId, ProviderAccountStore,
};
use gateway_core::engine::middleware::{
MiddlewareContext, MiddlewareError, MiddlewareNext, MiddlewarePlan, MiddlewareRequest,
MiddlewareResponse,
MiddlewareContext, MiddlewareError, MiddlewareHeader, MiddlewareNext, MiddlewarePlan,
MiddlewareRequest, MiddlewareResponse,
};
use gateway_core::engine::provider::{Provider, ProviderRequest};
use gateway_core::lifecycle::CancellationToken;
@@ -202,28 +202,8 @@ async fn live_bootstrap_preserves_model_through_middleware_and_metadata() {
upstream.verify().await;
}
#[derive(Debug)]
struct ParametersMiddleware;
impl MiddlewarePlan for ParametersMiddleware {
fn handle(
&self,
_context: MiddlewareContext,
request: MiddlewareRequest,
next: MiddlewareNext,
) -> BoxFuture<'static, Result<MiddlewareResponse, MiddlewareError>> {
assert!(request.provider_parameters().unwrap()["installation_id"].is_string());
next.run(
request.with_provider_parameters(serde_json::Map::from_iter([(
"installation_id".into(),
json!("live-plugin-installation"),
)])),
)
}
}
#[tokio::test]
async fn live_create_uses_request_parameters_without_changing_call_ownership() {
async fn live_create_uses_outbound_rewrite_without_changing_call_ownership() {
let store = Arc::new(MemoryAccountStore::default());
create_account(&store, ACCOUNT).await;
let server = MockServer::start().await;
@@ -247,11 +227,16 @@ async fn live_create_uses_request_parameters_without_changing_call_ownership() {
let mut stream = provider
.execute(
request,
context_with_middleware(
"req_live_parameters",
Arc::new(ParametersMiddleware),
FastMode::Default,
),
super::contract::outbound_context("req_live_outbound", |request| {
request
.headers
.retain(|header| header.name() != "x-codex-turn-metadata");
request.headers.push(MiddlewareHeader::new(
"x-codex-turn-metadata",
Bytes::from(json!({"installation_id":"live-plugin-installation"}).to_string()),
));
Ok(())
}),
)
.await
.unwrap();
@@ -296,8 +296,7 @@ impl GrokBuildProvider {
provider_kind,
Some(model),
account_id,
None,
Box::new(move |operation, middleware_headers, _| {
Box::new(move |operation, middleware_headers| {
Box::pin(async move {
if let Some(adapter) = adapter {
return provider.execute_upstream_adapter(
+3 -3
View File
@@ -316,8 +316,8 @@ Responses 也不透传 `x-stainless-*`、`Origin`、`Referer`、`sec-ch-ua*` 和
Images 的 `x-codex-image-turn-id` 和 Desktop 自动 Git 上下文的 `clientUserMessageId`。
输入项元数据保持不变。Search 的会话 `id` 按同一安装身份域映射,只替换该值的原始字节,其余正文保持不变。
`prompt_cache_key` 同样按安装身份映射,保留同一身份内的缓存共享关系;未提供时不补造。
受信任插件可通过 attempt 构造参数选择本次安装身份;参数只影响出站构造,不改变实际选号与计费归属,
详见 [SDK 请求构造参数](../backend/crates/gateway-plugin/sdk/docs/capabilities.md#模型请求与-attempt)。
受信任插件可读取原始请求,自行生成身份并在 `outbound` 改写完整请求与关联字段;宿主不重新派生或补回业务字段,
实际选号与计费归属保持不变,详见 [SDK 出站请求准备](../backend/crates/gateway-plugin/sdk/docs/capabilities.md#上游出站准备)。
续接 response ID、历史 item/call ID、业务正文和未知扩展不参与映射,
该处理减少显式追踪标识的跨账号复用,不保证会话内容或业务 metadata 无法关联
@@ -475,7 +475,7 @@ Images 排除已知为 Free 套餐的 OAuth 账号;套餐缺失或未知时不
`/v1/alpha/search` 使用相同的 OpenAI Provider 原生端点边界:模型不映射,正文除会话 `id` 的账号归一及显式请求策略外原样转发,
Provider 旁路读取请求中的 `model`,按[账号模型限制](#账号模型限制)选号。
`x-codex-turn-metadata` 在移除客户端账号身份并按本次构造参数重写 installation ID 后转发;上游账号
`x-codex-turn-metadata` 在移除客户端账号身份并按选中账号重写 installation ID 后转发;上游账号
Authorization、Cookie、account ID、originator 和 User-Agent 均由代理安全重建。
Images 与 Search 保留通过现有过滤规则的普通业务请求头及多值字节;Responses 原连接的 turn state
不随独立端点转发,turn metadata 仍由所属端点按当前账号处理。
+1 -3
View File
@@ -172,8 +172,6 @@ flowchart LR
握手、socket 与断连由 API 持有,会话处理通过原 RPC 资源流保活;HTTP 返回链替换响应时释放未提交的会话。
原生与插件接管的下游 WebSocket 共用消息组合和取消入口,在默认解析前和实际写入前处理完整消息;读写并发进行,插件调用期间不占用 transport 写锁。
`request` 包裹逻辑模型请求,`attempt` 包裹每次已选号的执行;请求顺序进入、响应逆序返回。
attempt 可携带 Provider 的本次构造参数,Core / Runtime 只传递完整替换后的对象,解释和缺失校验由 Provider 负责;
参数不改变真实租约、凭据版本和计费归属,也不承载宿主对插件策略的推断、补值或持久化。
`outbound` 在原生默认值、隐私处理、序列化及内容压缩之后组合完整请求,续体只准备值,Provider 在整条返回链结束后发送。
无绑定或未命中当前请求范围时保留原生请求构造,不进入出站准备链。
它复用本次 attempt 的冻结计划、取消、期限和副作用水位;Provider 不再次应用业务默认值或重写正文,插件负责报文与内容编码的一致性。
@@ -467,7 +465,7 @@ OpenAI 模型目录用于发现,不因目录缺项拒绝请求;管理员配
- OpenAI 是透明边界。正文兼容、身份归一和显式请求策略之外的业务字段保留原始语义;
业务响应按原始字节转发,客户端错误兼容与原生续写额度恢复按明确例外处理,见 [OpenAI 透传](api.md#openai-透传)。
`transport/correlation` 在出站副本上按本次安装身份统一映射关联标识并剥离客户端 trace;
安装身份默认取自账号,可由 attempt 的构造参数改写,缺失或非法结果不回退默认值。
原生安装身份取自账号;插件的身份生成、持久化及关联字段改写通过既有中间件链路自行完成。
内部选号、亲和、连接池与本地诊断继续读取原始身份;具体字段边界见 [API 协议](api.md#请求头与身份)
canonical facts 从同一数据旁路提取,用于路由、恢复判断、观测和计费。
WebSocket transport 按完整文本消息交付,wire 同时保留原文与可选解析视图;同协议出口使用原文,