mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
refactor(compat): remove expired upgrade and plugin adapters
Use the current host manifest format and require restart confirmation on all management UI paths. Document v3.18.1 as the minimum online upgrade source while retaining old release manifest reads for supported rollbacks. Retire middleware v3 and upstream_adapter v1 Fast projections after their compatibility window. Align SDK and host declarations with the current contracts, keep risk warnings and per-instance failure isolation, and exercise native fast_mode values through real plugin subprocesses. Keep deployment, API and SDK documentation focused on current behavior. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
## 清单与处理器
|
||||
|
||||
- `publisher` 与机器短名 `name` 派生插件 ID,`displayName` 用于展示;复制示例后同时调整扩展项 ID 与注册结果
|
||||
- `contributes` 每种能力最多一项,普通贡献项可省略派生 ID、能力版本和固定阶段;中间件必须声明版本 `3` 并显式选择阶段。自定义完整扩展项 ID 属于自己的插件命名空间,运行注册与规范化清单一致
|
||||
- `contributes` 每种能力最多一项,普通贡献项可省略派生 ID、能力版本和固定阶段;中间件必须声明版本 `4` 并显式选择阶段,上游适配器必须声明版本 `2`。自定义完整扩展项 ID 属于自己的插件命名空间,运行注册与规范化清单一致
|
||||
- 作者清单用 `Manifest::from_author_slice` 规范化,不手写 `package`、固定阶段或重复注册;CLI 生成平台、协议与资源摘要,版本范围不能使用全版本通配
|
||||
- 安装后宿主会按 `configurationSchema` 准备默认配置,配置完整即可启用;默认绑定的空范围不限制请求。需要业务参数时声明真实必填项,通过 `secretFields` 声明敏感字段,不依赖用户再走一遍自建安装向导
|
||||
- Rust 插件只依赖公开 `gateway-plugin-sdk`;需要异步会话辅助时开启 `io`,使用 `PluginSession` 管理握手、回调关联、流控、取消和关闭
|
||||
|
||||
@@ -177,8 +177,6 @@ jobs:
|
||||
host_path = "backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json"
|
||||
host = JSON.parse(File.read(host_path))
|
||||
abort "Invalid plugin host compatibility declaration: #{host_path}" unless host["schema_version"] == 2
|
||||
# 保留旧更新器可解析的传输格式,实际支持范围仍取自当前宿主合同。
|
||||
host = host.merge("schema_version" => 1, "permissions" => [])
|
||||
# 清单只依赖版本与兼容声明,平台归档和镜像消费同一份封口结果。
|
||||
manifest = {
|
||||
"schema_version" => 1,
|
||||
|
||||
@@ -114,7 +114,6 @@ struct SystemUpdateDetailView {
|
||||
notes: Option<String>,
|
||||
cached: bool,
|
||||
update_supported: bool,
|
||||
restart_confirmation_supported: bool,
|
||||
unsupported_reason: Option<String>,
|
||||
warning: Option<String>,
|
||||
}
|
||||
@@ -134,7 +133,6 @@ impl From<SystemUpdateDetail> for SystemUpdateDetailView {
|
||||
notes: detail.notes,
|
||||
cached: detail.cached,
|
||||
update_supported: detail.update_supported,
|
||||
restart_confirmation_supported: true,
|
||||
unsupported_reason: detail.unsupported_reason,
|
||||
warning: detail.warning,
|
||||
}
|
||||
|
||||
@@ -1,20 +1 @@
|
||||
[
|
||||
{
|
||||
"capability": "middleware",
|
||||
"version": 3,
|
||||
"replacement_version": 4,
|
||||
"introduced_in": "3.20.0",
|
||||
"last_counted_in": "3.22.2",
|
||||
"remaining_releases": 0,
|
||||
"migration": "将执行设置和来源中的 disable_fast 改为 fast_mode,使用 default、enabled、disabled 三态,并将 middleware 声明升级到 v4"
|
||||
},
|
||||
{
|
||||
"capability": "upstream_adapter",
|
||||
"version": 1,
|
||||
"replacement_version": 2,
|
||||
"introduced_in": "3.20.0",
|
||||
"last_counted_in": "3.22.2",
|
||||
"remaining_releases": 0,
|
||||
"migration": "使用新版 SDK 读取 fast_mode 三态,并将 upstream_adapter 声明升级到 v2"
|
||||
}
|
||||
]
|
||||
[]
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
{ "capability": "model_router", "versions": [1] },
|
||||
{ "capability": "model_catalog", "versions": [1] },
|
||||
{ "capability": "retry_policy", "versions": [1] },
|
||||
{ "capability": "middleware", "versions": [3, 4] },
|
||||
{ "capability": "upstream_adapter", "versions": [1, 2] },
|
||||
{ "capability": "middleware", "versions": [4] },
|
||||
{ "capability": "upstream_adapter", "versions": [2] },
|
||||
{ "capability": "observer", "versions": [1] },
|
||||
{ "capability": "command_line", "versions": [1] },
|
||||
{ "capability": "management", "versions": [1] },
|
||||
|
||||
@@ -33,12 +33,7 @@ pub(super) async fn invoke(
|
||||
.extensions()
|
||||
.get::<core::Settings>()
|
||||
.and_then(|settings| settings.runtime.as_ref())
|
||||
.map(|settings| {
|
||||
crate::compatibility::FastSettings::middleware(&ports.session).sources(settings)
|
||||
})
|
||||
.transpose()
|
||||
.map_err(|_| MiddlewareError::InvalidState)?
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
.map_or(serde_json::Value::Null, |settings| settings.inspect());
|
||||
let (invocation, request) = Invocation::new(request, next, entry.instance_id.clone())?;
|
||||
let mut call = ports.session.context(Stage::Http, timeout);
|
||||
call.request_id = Some(context.request_id.clone());
|
||||
|
||||
@@ -52,13 +52,7 @@ pub(super) async fn invoke(
|
||||
if remaining.is_zero() || context.cancellation().is_cancelled() {
|
||||
return Err(MiddlewareError::Fault);
|
||||
}
|
||||
let invocation = MiddlewareInvocation::new(
|
||||
&context,
|
||||
request,
|
||||
next,
|
||||
entry.instance_id.clone(),
|
||||
crate::compatibility::FastSettings::middleware(&invocation_ports.session),
|
||||
);
|
||||
let invocation = MiddlewareInvocation::new(&context, request, next, entry.instance_id.clone());
|
||||
let (protocol, headers, payload) = match invocation.request_parts() {
|
||||
Ok(projection) => projection,
|
||||
Err(_) => return recover_rpc(&entry, &invocation, RpcError::Context(None)).await,
|
||||
@@ -96,9 +90,7 @@ pub(super) async fn invoke(
|
||||
scope,
|
||||
)?;
|
||||
let head = MiddlewareRequestHead {
|
||||
settings_sources: invocation
|
||||
.settings_sources()
|
||||
.map_err(|_| MiddlewareError::InvalidState)?,
|
||||
settings_sources: invocation.settings_sources(),
|
||||
settings: invocation
|
||||
.settings()
|
||||
.map_err(|_| MiddlewareError::InvalidState)?,
|
||||
|
||||
@@ -235,16 +235,15 @@ impl Execution {
|
||||
.collect(),
|
||||
continuation: previous.map(|state| state.continuation),
|
||||
};
|
||||
let payload = crate::compatibility::FastSettings::upstream_request(
|
||||
&self.adapter.session,
|
||||
request,
|
||||
invocation
|
||||
.operation
|
||||
.middleware_body()
|
||||
.map_err(|source| invalid().with_source(source))?
|
||||
.to_vec(),
|
||||
)
|
||||
.map_err(|source| invalid().with_source(source))?;
|
||||
let payload = request
|
||||
.encode(
|
||||
invocation
|
||||
.operation
|
||||
.middleware_body()
|
||||
.map_err(|source| invalid().with_source(source))?
|
||||
.to_vec(),
|
||||
)
|
||||
.map_err(|source| invalid().with_source(source))?;
|
||||
invocation.context.trace().record("plugin.upstream", serde_json::json!({ "instanceId": self.adapter.instance_id, "adapterId": declaration.id, "generation": context.generation, "transport": declaration.transport.as_str() }));
|
||||
let stream = tokio::select! {
|
||||
biased;
|
||||
|
||||
@@ -86,7 +86,6 @@ fn request_feature(
|
||||
|
||||
pub(crate) struct MiddlewareInvocation {
|
||||
instance_id: String,
|
||||
settings_contract: crate::compatibility::FastSettings,
|
||||
capabilities_allowed: bool,
|
||||
transport: ClientTransport,
|
||||
state: Mutex<InvocationState>,
|
||||
@@ -141,28 +140,27 @@ impl MiddlewareInvocation {
|
||||
.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
state
|
||||
.original_request
|
||||
.settings()
|
||||
.map(|settings| self.settings_contract.execution(settings))
|
||||
.unwrap_or(Ok(serde_json::Value::Null))
|
||||
serde_json::to_value(
|
||||
state
|
||||
.original_request
|
||||
.settings()
|
||||
.and_then(|settings| settings.execution_values()),
|
||||
)
|
||||
.map_err(|_| invalid())
|
||||
}
|
||||
|
||||
pub(crate) fn settings_sources(&self) -> Result<serde_json::Value, PluginFault> {
|
||||
pub(crate) fn settings_sources(&self) -> serde_json::Value {
|
||||
self.request_settings()
|
||||
.map(|settings| self.settings_contract.sources(&settings))
|
||||
.unwrap_or(Ok(serde_json::Value::Null))
|
||||
.map_or(serde_json::Value::Null, |settings| settings.inspect())
|
||||
}
|
||||
pub(crate) fn new(
|
||||
context: &MiddlewareContext,
|
||||
request: MiddlewareRequest,
|
||||
next: MiddlewareNext,
|
||||
instance_id: String,
|
||||
settings_contract: crate::compatibility::FastSettings,
|
||||
) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
instance_id,
|
||||
settings_contract,
|
||||
capabilities_allowed: context.mount()
|
||||
== gateway_core::engine::middleware::MiddlewareMount::Request
|
||||
&& context.operation() == Some(gateway_core::operation::OperationKind::Generate),
|
||||
@@ -343,7 +341,7 @@ impl MiddlewareInvocation {
|
||||
"execution settings are not available at this middleware boundary",
|
||||
)
|
||||
})?;
|
||||
let values = self.settings_contract.decode(settings, current)?;
|
||||
let values = serde_json::from_value(settings).map_err(|_| invalid())?;
|
||||
let updated = current
|
||||
.replace_execution(&values, &self.instance_id)
|
||||
.map_err(|_| invalid())?;
|
||||
|
||||
+2
-6
@@ -1,8 +1,4 @@
|
||||
//! 有期限的插件合同适配;旧字段只在此边界出现,不进入 Core 或持久化模型
|
||||
|
||||
mod fast;
|
||||
|
||||
pub(crate) use fast::FastSettings;
|
||||
//! 插件合同弃用声明、管理端提示与加载日志
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
@@ -26,7 +22,7 @@ struct Deprecation {
|
||||
fn declarations() -> Result<&'static [Deprecation], AdminError> {
|
||||
static DECLARATIONS: OnceLock<Result<Vec<Deprecation>, serde_json::Error>> = OnceLock::new();
|
||||
DECLARATIONS
|
||||
.get_or_init(|| serde_json::from_str(include_str!("../../plugin-api-deprecations.json")))
|
||||
.get_or_init(|| serde_json::from_str(include_str!("../plugin-api-deprecations.json")))
|
||||
.as_deref()
|
||||
.map_err(|_| AdminError::internal("插件接口弃用声明不合法"))
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
//! 在旧插件合同边界投影 Fast 设置并保留宿主三态语义
|
||||
|
||||
use gateway_core::{
|
||||
account::FastMode,
|
||||
routing::request_settings::{ExecutionSettings, RequestSettings},
|
||||
};
|
||||
use gateway_plugin_sdk::{
|
||||
Capability, ErrorCode, PluginFault,
|
||||
call::{model::ExecutionEncodingError, upstream_adapter::UpstreamAdapterRequest},
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::RpcSession;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct FastSettings {
|
||||
legacy: bool,
|
||||
}
|
||||
|
||||
impl FastSettings {
|
||||
pub(crate) fn middleware(session: &RpcSession) -> Self {
|
||||
Self {
|
||||
legacy: session.capability_version(Capability::Middleware) == Some(3),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn execution(self, settings: &RequestSettings) -> Result<Value, PluginFault> {
|
||||
let mut value = serde_json::to_value(settings.execution_values()).map_err(|_| invalid())?;
|
||||
if self.legacy {
|
||||
project_fast(&mut value)?;
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub(crate) fn sources(self, settings: &RequestSettings) -> Result<Value, PluginFault> {
|
||||
let mut value = settings.inspect();
|
||||
if self.legacy
|
||||
&& let Some(execution) = value["execution"].as_object_mut()
|
||||
{
|
||||
project_fast(execution.get_mut("input").ok_or_else(invalid)?)?;
|
||||
let mut source = execution.remove("fast_mode").ok_or_else(invalid)?;
|
||||
if !source.is_null() {
|
||||
source["value"] = disabled(&source["value"])?;
|
||||
}
|
||||
execution.insert("disable_fast".into(), source);
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub(crate) fn decode(
|
||||
self,
|
||||
mut value: Value,
|
||||
current: &RequestSettings,
|
||||
) -> Result<ExecutionSettings, PluginFault> {
|
||||
if self.legacy {
|
||||
let object = value.as_object_mut().ok_or_else(invalid)?;
|
||||
if object.contains_key("fast_mode") {
|
||||
return Err(invalid());
|
||||
}
|
||||
let next = object
|
||||
.remove("disable_fast")
|
||||
.and_then(|value| value.as_bool())
|
||||
.ok_or_else(invalid)?;
|
||||
let current = current.execution_values().ok_or_else(invalid)?.fast_mode;
|
||||
// 原样回传 false 不能把三态的 enabled 降为 default,只有实际变化才覆盖
|
||||
let mode = if next == (current == FastMode::Disabled) {
|
||||
current
|
||||
} else if next {
|
||||
FastMode::Disabled
|
||||
} else {
|
||||
FastMode::Default
|
||||
};
|
||||
object.insert("fast_mode".into(), Value::String(mode.as_str().into()));
|
||||
}
|
||||
serde_json::from_value(value).map_err(|_| invalid())
|
||||
}
|
||||
|
||||
pub(crate) fn upstream_request(
|
||||
session: &RpcSession,
|
||||
request: UpstreamAdapterRequest,
|
||||
body: Vec<u8>,
|
||||
) -> Result<Vec<u8>, ExecutionEncodingError> {
|
||||
if session.capability_version(Capability::UpstreamAdapter) != Some(1) {
|
||||
return request.encode(body);
|
||||
}
|
||||
let mut metadata = serde_json::to_value(request).map_err(|_| ExecutionEncodingError)?;
|
||||
project_fast(&mut metadata).map_err(|_| ExecutionEncodingError)?;
|
||||
gateway_plugin_sdk::call::upstream_adapter::encode_request_metadata(&metadata, body)
|
||||
}
|
||||
}
|
||||
|
||||
fn project_fast(value: &mut Value) -> Result<(), PluginFault> {
|
||||
if value.is_null() {
|
||||
return Ok(());
|
||||
}
|
||||
let object = value.as_object_mut().ok_or_else(invalid)?;
|
||||
let mode = object.remove("fast_mode").ok_or_else(invalid)?;
|
||||
object.insert("disable_fast".into(), disabled(&mode)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn disabled(value: &Value) -> Result<Value, PluginFault> {
|
||||
let mode: FastMode = serde_json::from_value(value.clone()).map_err(|_| invalid())?;
|
||||
Ok(Value::Bool(mode == FastMode::Disabled))
|
||||
}
|
||||
|
||||
fn invalid() -> PluginFault {
|
||||
PluginFault::new(ErrorCode::InvalidInput, "invalid plugin execution settings")
|
||||
}
|
||||
@@ -205,16 +205,6 @@ impl RpcSession {
|
||||
error
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn capability_version(
|
||||
&self,
|
||||
capability: gateway_plugin_sdk::Capability,
|
||||
) -> Option<u32> {
|
||||
self.handshake
|
||||
.contributes
|
||||
.get(&capability)
|
||||
.map(|declaration| declaration.version)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Shared {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
//! 验证旧中间件的 Fast 设置适配与不兼容插件的实例故障隔离
|
||||
//! 验证中间件 Fast 三态设置与不兼容插件的实例故障隔离
|
||||
|
||||
use super::*;
|
||||
use gateway_core::{policy::ClientPolicy, routing::request_settings::RequestSettings};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_middleware_preserves_three_state_fast_and_projects_sources() {
|
||||
for (mode, write, expected) in [
|
||||
(FastMode::Default, false, FastMode::Default),
|
||||
(FastMode::Enabled, false, FastMode::Enabled),
|
||||
(FastMode::Disabled, true, FastMode::Disabled),
|
||||
(FastMode::Enabled, true, FastMode::Disabled),
|
||||
(FastMode::Disabled, false, FastMode::Default),
|
||||
async fn middleware_preserves_three_state_fast_and_sources() {
|
||||
for (mode, expected) in [
|
||||
(FastMode::Default, FastMode::Default),
|
||||
(FastMode::Enabled, FastMode::Enabled),
|
||||
(FastMode::Disabled, FastMode::Disabled),
|
||||
(FastMode::Enabled, FastMode::Disabled),
|
||||
(FastMode::Disabled, FastMode::Default),
|
||||
] {
|
||||
let snapshot = gateway_core::routing::RuntimeSnapshot::new(
|
||||
ConfigRevision::new(1).unwrap(),
|
||||
@@ -46,34 +46,40 @@ async fn legacy_middleware_preserves_three_state_fast_and_projects_sources() {
|
||||
let mut seed = settings.execution_values().unwrap();
|
||||
seed.fast_mode = mode;
|
||||
let settings = settings.replace_execution(&seed, "seed").unwrap();
|
||||
let mut wire = serde_json::to_value(&seed).unwrap();
|
||||
wire.as_object_mut().unwrap().remove("fast_mode");
|
||||
wire["disable_fast"] = json!(mode == FastMode::Disabled);
|
||||
let wire = serde_json::to_value(&seed).unwrap();
|
||||
let mut updated = wire.clone();
|
||||
updated["disable_fast"] = json!(write);
|
||||
updated["fast_mode"] = json!(expected.as_str());
|
||||
updated["timeout_ms"] = json!(90_000);
|
||||
let mut contribution = crate::support::contribution(
|
||||
let contribution = crate::support::contribution(
|
||||
Capability::Middleware,
|
||||
vec![Stage::Request],
|
||||
vec!["openai".into()],
|
||||
vec!["openai".into()],
|
||||
);
|
||||
contribution.1.version = 3;
|
||||
let worker = std::fs::read(env!("CARGO_BIN_EXE_gateway-plugin-test-middleware")).unwrap();
|
||||
let package = crate::support::package_with_contributions(
|
||||
&worker,
|
||||
Contributions::from([contribution]),
|
||||
);
|
||||
let (cache, runtime) = setup_package(vec![InstanceFixture {
|
||||
id: "legacy-settings",
|
||||
configuration: json!({
|
||||
"mode":"settings", "middleware_version":3, "expected_settings":wire, "settings":updated,
|
||||
"expected_legacy_source": if mode == FastMode::Default { Value::Null } else {
|
||||
json!({"instance_id":"seed", "order":1, "value":mode == FastMode::Disabled})
|
||||
},
|
||||
}),
|
||||
bindings: vec![binding(MIDDLEWARE_CONTRIBUTION,"request",0,PluginFailurePolicy::Reject)],
|
||||
}], package).await;
|
||||
let (cache, runtime) = setup_package(
|
||||
vec![InstanceFixture {
|
||||
id: "fast-settings",
|
||||
configuration: json!({
|
||||
"mode":"settings", "expected_settings":wire, "settings":updated,
|
||||
"expected_fast_source": if mode == FastMode::Default { Value::Null } else {
|
||||
json!({"instance_id":"seed", "order":1, "value":mode.as_str()})
|
||||
},
|
||||
}),
|
||||
bindings: vec![binding(
|
||||
MIDDLEWARE_CONTRIBUTION,
|
||||
"request",
|
||||
0,
|
||||
PluginFailurePolicy::Reject,
|
||||
)],
|
||||
}],
|
||||
package,
|
||||
)
|
||||
.await;
|
||||
let generation = prepare(&runtime).await;
|
||||
let plan = runtime
|
||||
.execution_registry()
|
||||
@@ -105,7 +111,7 @@ async fn legacy_middleware_preserves_three_state_fast_and_projects_sources() {
|
||||
);
|
||||
let sources = settings.inspect();
|
||||
assert!(sources["execution"].get("disable_fast").is_none());
|
||||
if mode == FastMode::Enabled && !write {
|
||||
if mode != FastMode::Default && mode == expected {
|
||||
assert_eq!(sources["execution"]["fast_mode"]["instance_id"], "seed");
|
||||
}
|
||||
next.run(request).await
|
||||
|
||||
@@ -102,18 +102,12 @@ async fn setup(
|
||||
Arc<super::Store>,
|
||||
gateway_plugin_runtime::PluginRuntime,
|
||||
) {
|
||||
let mut contributes = Contributions::from([crate::support::contribution(
|
||||
let contributes = Contributions::from([crate::support::contribution(
|
||||
Capability::UpstreamAdapter,
|
||||
vec![Stage::Upstream],
|
||||
vec!["openai".into()],
|
||||
vec!["openai".into()],
|
||||
)]);
|
||||
if let Some(version) = config["upstream_version"].as_u64() {
|
||||
contributes
|
||||
.get_mut(&Capability::UpstreamAdapter)
|
||||
.unwrap()
|
||||
.version = version.try_into().unwrap();
|
||||
}
|
||||
let (cache, store, runtime) =
|
||||
super::setup_with_contributions_and_restart_circuit(contributes, Default::default()).await;
|
||||
{
|
||||
@@ -226,7 +220,7 @@ fn execute_with_context(
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_adapter_decodes_exact_v1_metadata_for_all_fast_modes() {
|
||||
async fn adapter_preserves_three_state_fast_in_request_metadata() {
|
||||
use gateway_core::account::FastMode;
|
||||
for mode in [FastMode::Default, FastMode::Enabled, FastMode::Disabled] {
|
||||
let server = MockServer::start().await;
|
||||
@@ -237,8 +231,7 @@ async fn legacy_adapter_decodes_exact_v1_metadata_for_all_fast_modes() {
|
||||
.mount(&server)
|
||||
.await;
|
||||
let mut config = configuration(&server.uri(), false);
|
||||
config["upstream_version"] = json!(1);
|
||||
config["expected_disable_fast"] = json!(mode == FastMode::Disabled);
|
||||
config["expected_fast_mode"] = json!(mode.as_str());
|
||||
let (cache, _, runtime) = setup(config).await;
|
||||
let generation =
|
||||
ExtensionPreparationPort::prepare(&runtime, ConfigRevision::new(1).unwrap())
|
||||
|
||||
@@ -43,9 +43,9 @@ fn host_compatibility_requires_the_trusted_middleware_contract() {
|
||||
.expect("compatibility JSON");
|
||||
assert!(!compatibility.supports_capability("middleware", 1));
|
||||
assert!(!compatibility.supports_capability("middleware", 2));
|
||||
assert!(compatibility.supports_capability("middleware", 3));
|
||||
assert!(!compatibility.supports_capability("middleware", 3));
|
||||
assert!(compatibility.supports_capability("middleware", 4));
|
||||
assert!(compatibility.supports_capability("upstream_adapter", 1));
|
||||
assert!(!compatibility.supports_capability("upstream_adapter", 1));
|
||||
assert!(compatibility.supports_capability("upstream_adapter", 2));
|
||||
assert!(!compatibility.supports_capability("openai", 1));
|
||||
}
|
||||
@@ -81,7 +81,7 @@ async fn package_inspector_returns_static_requirements_without_starting_the_plug
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deprecated_contracts_are_loadable_and_only_old_versions_receive_notices() {
|
||||
async fn retired_contracts_remain_loadable_with_compatibility_warnings() {
|
||||
for legacy in [false, true] {
|
||||
let mut middleware = crate::support::contribution(
|
||||
Capability::Middleware,
|
||||
@@ -105,13 +105,19 @@ async fn deprecated_contracts_are_loadable_and_only_old_versions_receive_notices
|
||||
);
|
||||
let inspector =
|
||||
PackageInspector::new(PackageLimits::default(), semver::Version::new(1, 0, 0));
|
||||
let artifact = inspector.inspect(archive, None).await.unwrap();
|
||||
let artifact = inspector.inspect(archive.clone(), None).await.unwrap();
|
||||
let notices = inspector.api_deprecations(&artifact.metadata).unwrap();
|
||||
assert_eq!(notices.len(), if legacy { 2 } else { 0 });
|
||||
for notice in notices {
|
||||
assert!(notice.replacement_version > notice.version);
|
||||
assert!(!notice.migration.is_empty());
|
||||
assert!(notice.remaining_releases <= 7);
|
||||
assert!(notices.is_empty());
|
||||
let warning = inspector
|
||||
.compatibility_warning(archive, artifact.metadata.sha256)
|
||||
.await
|
||||
.unwrap();
|
||||
if legacy {
|
||||
let warning = warning.expect("retired contracts must remain visible");
|
||||
assert!(warning.contains("middleware v3"));
|
||||
assert!(warning.contains("upstream_adapter v1"));
|
||||
} else {
|
||||
assert!(warning.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,12 +489,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
call.request.head.settings,
|
||||
configuration["expected_settings"]
|
||||
);
|
||||
if let Some(expected) = configuration.get("expected_legacy_source") {
|
||||
if let Some(expected) = configuration.get("expected_fast_source") {
|
||||
let sources = &call.request.head.settings_sources["execution"];
|
||||
assert!(sources.get("fast_mode").is_none());
|
||||
assert!(sources["input"].get("fast_mode").is_none());
|
||||
assert_eq!(sources["input"]["disable_fast"], false);
|
||||
assert_eq!(&sources["disable_fast"], expected);
|
||||
assert!(sources.get("disable_fast").is_none());
|
||||
assert!(sources["input"].get("disable_fast").is_none());
|
||||
assert_eq!(sources["input"]["fast_mode"], "default");
|
||||
assert_eq!(&sources["fast_mode"], expected);
|
||||
}
|
||||
if let Some(settings) = configuration.get("settings") {
|
||||
call.request.head.settings = settings.clone();
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
//! 为测试子进程严格解码上游适配器 v1 的请求元数据
|
||||
|
||||
use gateway_plugin_sdk::call::upstream_adapter::{
|
||||
BuiltinProvider, UpstreamAdapterRequest, UpstreamContinuation,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
// 固定 v1 的严格解码器,不能用新版 DTO 的宽松解析掩盖旧插件加载失败
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Request {
|
||||
adapter_id: String,
|
||||
provider: BuiltinProvider,
|
||||
upstream_model: String,
|
||||
client_key_id: String,
|
||||
account_id: String,
|
||||
credential_revision: u64,
|
||||
protocol: String,
|
||||
client_transport: String,
|
||||
disable_fast: bool,
|
||||
headers: Vec<(String, Vec<u8>)>,
|
||||
continuation: Option<UpstreamContinuation>,
|
||||
}
|
||||
|
||||
pub fn decode(payload: &[u8], expected_disabled: bool) -> (UpstreamAdapterRequest, Vec<u8>) {
|
||||
assert_eq!(&payload[..4], b"GPAQ");
|
||||
let metadata_length = u32::from_be_bytes(payload[4..8].try_into().unwrap()) as usize;
|
||||
let body_length = u32::from_be_bytes(payload[8..12].try_into().unwrap()) as usize;
|
||||
assert_eq!(payload.len(), 12 + metadata_length + body_length);
|
||||
let old: Request = serde_json::from_slice(&payload[12..12 + metadata_length]).unwrap();
|
||||
assert_eq!(old.disable_fast, expected_disabled);
|
||||
(
|
||||
UpstreamAdapterRequest {
|
||||
adapter_id: old.adapter_id,
|
||||
provider: old.provider,
|
||||
upstream_model: old.upstream_model,
|
||||
client_key_id: old.client_key_id,
|
||||
account_id: old.account_id,
|
||||
credential_revision: old.credential_revision,
|
||||
protocol: old.protocol,
|
||||
client_transport: old.client_transport,
|
||||
fast_mode: if old.disable_fast {
|
||||
"disabled"
|
||||
} else {
|
||||
"default"
|
||||
}
|
||||
.into(),
|
||||
headers: old.headers,
|
||||
continuation: old.continuation,
|
||||
},
|
||||
payload[12 + metadata_length..].to_vec(),
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
//! 真实子进程协议对端;只依赖公开 SDK,Cargo 为集成测试构建此辅助二进制
|
||||
|
||||
mod legacy_upstream;
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
io::Write as _,
|
||||
@@ -1481,16 +1479,10 @@ async fn main() {
|
||||
|
||||
impl Peer {
|
||||
async fn upstream_adapter(&self, id: u64, payload: Vec<u8>) -> Result<(), PluginFault> {
|
||||
let (request, body) = if self.configuration["upstream_version"] == 1 {
|
||||
legacy_upstream::decode(
|
||||
&payload,
|
||||
self.configuration["expected_disable_fast"]
|
||||
.as_bool()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
UpstreamAdapterRequest::decode(&payload).unwrap()
|
||||
};
|
||||
let (request, body) = UpstreamAdapterRequest::decode(&payload).unwrap();
|
||||
if let Some(expected) = self.configuration.get("expected_fast_mode") {
|
||||
assert_eq!(request.fast_mode, expected.as_str().unwrap());
|
||||
}
|
||||
// 假凭据用于检测宿主是否把已选账号令牌放入了插件输入
|
||||
assert!(!String::from_utf8_lossy(&payload).contains("fixture-native-token"));
|
||||
self.append_observation_marker("upstream_marker", &json!({"key":request.client_key_id,"account":request.account_id,"continuation":request.continuation}));
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
|
||||
当前支持范围由[宿主兼容声明](../../runtime/plugin-host-compatibility.json)给出,弃用状态、剩余兼容窗口及作者说明以[弃用清单](../../runtime/plugin-api-deprecations.json)为准,管理端和加载日志展示同一提示
|
||||
|
||||
执行设置与来源使用 `fast_mode` 的 `default`、`enabled`、`disabled` 三态,宿主不转换旧 `disable_fast` 字段。
|
||||
使用 `middleware v3` 或 `upstream_adapter v1` 的插件需按当前 SDK 更新字段处理、升级能力版本并重新构建,仅修改版本声明不能替代迁移
|
||||
|
||||
兼容窗口自替代合同首次正式发布起至少覆盖 7 个后续正式版本;主版本、次版本和补丁版本各计一次,预发行与同标签重试不计。
|
||||
窗口内提供适配,窗口外不保证兼容;是否使用过特定字段不改变判断,版本号差值也不能代替清单中的计数
|
||||
|
||||
|
||||
@@ -39,15 +39,7 @@ pub(super) fn encode_request(
|
||||
request: UpstreamAdapterRequest,
|
||||
body: Vec<u8>,
|
||||
) -> Result<Vec<u8>, ExecutionEncodingError> {
|
||||
encode_request_metadata(&request, body)
|
||||
}
|
||||
|
||||
/// 封装按合同版本投影的请求元数据,保持 GPAQ 分帧与载荷限制统一
|
||||
pub fn encode_request_metadata(
|
||||
metadata: &impl Serialize,
|
||||
body: Vec<u8>,
|
||||
) -> Result<Vec<u8>, ExecutionEncodingError> {
|
||||
pack(*b"GPAQ", metadata, [body])
|
||||
pack(*b"GPAQ", &request, [body])
|
||||
}
|
||||
|
||||
pub(super) fn decode_request(
|
||||
|
||||
@@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize};
|
||||
use super::model::ExecutionEvent;
|
||||
|
||||
mod codec;
|
||||
pub use codec::encode_request_metadata;
|
||||
|
||||
pub const REGISTER_METHOD: &str = "upstream_adapter.register";
|
||||
pub const EXECUTE_METHOD: &str = "upstream_adapter.execute";
|
||||
|
||||
@@ -26,7 +26,7 @@ impl Capability {
|
||||
#[must_use]
|
||||
pub const fn contract_versions(self) -> &'static [u32] {
|
||||
match self {
|
||||
Self::Middleware => &[3, 4],
|
||||
Self::Middleware => &[4],
|
||||
Self::FrontendAuthentication
|
||||
| Self::Scheduler
|
||||
| Self::ModelRouter
|
||||
@@ -36,7 +36,7 @@ impl Capability {
|
||||
| Self::CommandLine
|
||||
| Self::Management
|
||||
| Self::Maintenance => &[1],
|
||||
Self::UpstreamAdapter => &[1, 2],
|
||||
Self::UpstreamAdapter => &[2],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ fn installation_checks_protocol_engine_and_target() {
|
||||
fn capability_contract_versions_are_checked_independently_of_rpc_version() {
|
||||
let mut manifest = packaged_manifest();
|
||||
assert!(manifest.validate().is_ok());
|
||||
for version in [0, 1, 2, 5, u32::MAX] {
|
||||
for version in [0, 1, 2, 3, 5, u32::MAX] {
|
||||
manifest
|
||||
.contributes
|
||||
.get_mut(&Capability::Middleware)
|
||||
@@ -155,6 +155,29 @@ fn capability_contract_versions_are_checked_independently_of_rpc_version() {
|
||||
assert_eq!(manifest.validate(), Err(ManifestError::Invalid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retired_upstream_contract_requires_an_author_upgrade() {
|
||||
let mut manifest = source_manifest();
|
||||
manifest.contributes.clear();
|
||||
manifest.contributes.insert(
|
||||
Capability::UpstreamAdapter,
|
||||
ContributionDeclaration {
|
||||
id: "9acme.request-tags.upstream".into(),
|
||||
version: 2,
|
||||
stages: vec![Stage::Upstream],
|
||||
input_formats: vec!["openai".into()],
|
||||
output_formats: vec!["openai".into()],
|
||||
},
|
||||
);
|
||||
assert!(manifest.validate().is_ok());
|
||||
manifest
|
||||
.contributes
|
||||
.get_mut(&Capability::UpstreamAdapter)
|
||||
.unwrap()
|
||||
.version = 1;
|
||||
assert_eq!(manifest.validate(), Err(ManifestError::Invalid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn package_rejects_file_directory_conflicts_and_missing_declared_files() {
|
||||
let mut manifest = packaged_manifest();
|
||||
|
||||
@@ -601,6 +601,8 @@ alpha、beta、rc、exp 在 GitHub 标记为 Pre-release,不覆盖 GitHub Late
|
||||
### 管理端在线更新
|
||||
|
||||
官方构建按上述规则在线更新;源码等非官方构建返回不支持原因,不提供可用更新。
|
||||
最低受支持的在线升级起点为 **v3.18.1**。更旧版本需手动安装 v3.18.1 或新版完整发行包/镜像,
|
||||
不能依赖旧更新页面直接升级到最新版或选择已不是最新版的过渡版本。
|
||||
点击「下载并更新」后,操作日志显示下载、校验与安装过程,完成后显示「待重启」及待生效版本,重启后才使用新程序。
|
||||
「当前版本」始终表示正在运行的版本,「通道最新」表示所选通道的检查结果
|
||||
|
||||
|
||||
+1
-2
@@ -1933,8 +1933,7 @@ Provider metadata 分别保留 `requestedServiceTier` 与 `upstreamServiceTier`
|
||||
下载并解包后校验发行身份,插件版本不匹配不阻止更新或回滚,兼容性风险在重启前提示。
|
||||
两条路径在文件交换前后复核全局配置版本,文件替换失败或取消时成组恢复二进制、Web 资源和官方插件目录
|
||||
|
||||
详情响应的 `restartConfirmationSupported=true` 表示运行进程支持重启前确认。
|
||||
`restart/check` 返回 `targetVersion`、`releaseManifestSha256`、`configRevision` 和 `incompatiblePlugins`,
|
||||
客户端重启前调用 `restart/check`,返回 `targetVersion`、`releaseManifestSha256`、`configRevision` 和 `incompatiblePlugins`,
|
||||
每项包含 `instanceId`、`name`、`reason`,检查不修改插件状态。源码运行的目标版本与发行摘要为空,检查当前宿主兼容性。
|
||||
存在不兼容插件时,客户端展示列表并取得确认后,将完整检查结果作为 `confirmation` 提交重启请求。
|
||||
服务端在重启锁内重新检查,目标或配置变化返回 `40901`,须重新检查并确认。确认后保留启用配置,重启时逐个尝试加载插件。
|
||||
|
||||
@@ -34,7 +34,6 @@ export interface SystemUpdateDetail {
|
||||
notes: string | null
|
||||
cached: boolean
|
||||
updateSupported: boolean
|
||||
restartConfirmationSupported?: boolean
|
||||
unsupportedReason: string | null
|
||||
warning: string | null
|
||||
}
|
||||
|
||||
@@ -196,11 +196,6 @@ async function handleRestart() {
|
||||
try {
|
||||
if (!updateInfo.value)
|
||||
throw new Error('请等待系统更新信息加载完成')
|
||||
// 文件已更新但旧进程仍在运行时,旧 API 继续使用安装前的兼容预检。
|
||||
if (!updateInfo.value.restartConfirmationSupported) {
|
||||
await restartNow()
|
||||
return
|
||||
}
|
||||
restartPlan.value = await checkSystemRestart()
|
||||
if (restartPlan.value.incompatiblePlugins.length) {
|
||||
restartConfirmOpen.value = true
|
||||
|
||||
@@ -452,7 +452,7 @@ export const useSystemUpdateStore = defineStore('system-update', () => {
|
||||
setPhase({ kind: 'failed' })
|
||||
}
|
||||
|
||||
async function restartNow(confirmation?: SystemRestartPlan) {
|
||||
async function restartNow(confirmation: SystemRestartPlan) {
|
||||
if (restarting.value)
|
||||
return
|
||||
|
||||
|
||||
+1
-1
Submodule modules/plugins updated: cfd37537f9...ff9ab2fe4e
Reference in New Issue
Block a user