diff --git a/backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json b/backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json index 8f1fcda63..f1627519b 100644 --- a/backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json +++ b/backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json @@ -6,7 +6,7 @@ { "capability": "frontend_authentication", "versions": [2] }, { "capability": "scheduler", "versions": [3] }, { "capability": "model_router", "versions": [2] }, - { "capability": "model_catalog", "versions": [1, 2] }, + { "capability": "model_catalog", "versions": [2] }, { "capability": "retry_policy", "versions": [2] }, { "capability": "middleware", "versions": [6] }, { "capability": "upstream_adapter", "versions": [5] }, diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/catalog.rs b/backend/crates/gateway-plugin/runtime/src/adapter/catalog.rs index f1f3ff529..ce730c333 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/catalog.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/catalog.rs @@ -14,10 +14,7 @@ use gateway_core::{ }; use gateway_plugin_sdk::{ Capability, Manifest, Stage, - call::{ - catalog::{ModelCatalogRegistration, ModelCatalogRegistrationV2}, - middleware::RequestFeature, - }, + call::{catalog::ModelCatalogRegistrationV2, middleware::RequestFeature}, }; use crate::RpcSession; @@ -52,15 +49,8 @@ pub(crate) async fn prepare( if reply.payload.len() > 64 * 1024 { return Err(AdminError::invalid("插件模型目录信封无效或过大")); } - let (aliases, models) = if manifest.contributes[&Capability::ModelCatalog].version == 2 { - let registration: ModelCatalogRegistrationV2 = serde_json::from_slice(&reply.payload) - .map_err(|_| AdminError::invalid("插件模型目录声明无效"))?; - (registration.aliases, registration.models) - } else { - let registration: ModelCatalogRegistration = serde_json::from_slice(&reply.payload) - .map_err(|_| AdminError::invalid("插件模型目录声明无效"))?; - (registration.models, Vec::new()) - }; + let ModelCatalogRegistrationV2 { aliases, models } = serde_json::from_slice(&reply.payload) + .map_err(|_| AdminError::invalid("插件模型目录声明无效"))?; if aliases.len().saturating_add(models.len()) > 256 { return Err(AdminError::invalid("单个插件最多贡献 256 个模型条目")); } diff --git a/backend/crates/gateway-plugin/runtime/tests/generation/prepare/snapshot.rs b/backend/crates/gateway-plugin/runtime/tests/generation/prepare/snapshot.rs index ddfd6fb2c..6ab5fbf3a 100644 --- a/backend/crates/gateway-plugin/runtime/tests/generation/prepare/snapshot.rs +++ b/backend/crates/gateway-plugin/runtime/tests/generation/prepare/snapshot.rs @@ -12,13 +12,12 @@ use gateway_plugin_sdk::Stage; #[tokio::test] async fn adapted_catalog_freezes_adapter_accounts_and_preserves_the_active_generation() { use gateway_admin::model::plugins::instances::{PluginCapabilityBinding, PluginFailurePolicy}; - let mut catalog = crate::support::contribution( + let catalog = crate::support::contribution( Capability::ModelCatalog, vec![Stage::Registration], vec![], vec![], ); - catalog.1.version = 2; let adapter = crate::support::contribution( Capability::UpstreamAdapter, vec![Stage::Upstream], @@ -46,7 +45,7 @@ async fn adapted_catalog_freezes_adapter_accounts_and_preserves_the_active_gener "paths":[{"path":"responses","purpose":"inference"}], "authentication_kinds":["oauth"], "account_ids":["acct_b"], "models":["shared-model"], "transport":"http_sse","protocol":"openai" }]}, - "model_catalog":{"aliases":[],"models":[{ + "model_catalog":{"aliases":[{"id":"native-alias","provider":"openai","model":"native-model"}],"models":[{ "id":"shared-model","adapter_id":"example","features":["tools"],"presentation":{"display_name":"Shared"} }]} }); @@ -59,7 +58,8 @@ async fn adapted_catalog_freezes_adapter_accounts_and_preserves_the_active_gener active.models()[0].accounts.iter().next().unwrap().as_str(), "acct_b" ); - assert!(active.model_aliases().is_empty()); + assert_eq!(active.model_aliases().len(), 1); + assert_eq!(active.model_aliases()[0].id.as_str(), "native-alias"); for scenario in ["accounts", "model", "binding", "key", "group"] { let mut invalid = snapshot.clone(); invalid.config_revision = Revision::new(2).unwrap(); @@ -168,13 +168,12 @@ async fn adapted_catalog_freezes_adapter_accounts_and_preserves_the_active_gener #[tokio::test] async fn adapted_catalog_requires_its_registered_adapter() { - let mut catalog = crate::support::contribution( + let catalog = crate::support::contribution( Capability::ModelCatalog, vec![Stage::Registration], vec![], vec![], ); - catalog.1.version = 2; let (cache, store, runtime) = super::super::setup_with_contributions(Contributions::from([catalog])).await; let mut snapshot = store.snapshot.lock().unwrap().clone(); @@ -214,7 +213,7 @@ async fn initial_registration_http_uses_the_prepared_settings_without_a_publicat .await; store.snapshot.lock().unwrap().instances[0].configuration = serde_json::json!({ "registration_http": {"params": {"method":"POST", "url":server.uri(), "headers":[]}, "body":"x".repeat(64)}, - "model_catalog":{"models":[{"id":"fixture-alias","provider":"openai","model":"fixture-upstream"}]} + "model_catalog":{"aliases":[{"id":"fixture-alias","provider":"openai","model":"fixture-upstream"}],"models":[]} }); let prepared = ExtensionPreparationPort::prepare_with_request_body_limit( &runtime, @@ -242,7 +241,7 @@ async fn model_catalog_is_frozen_without_entering_the_request_chain() { ])) .await; store.snapshot.lock().unwrap().instances[0].configuration = serde_json::json!({ - "model_catalog": {"models": [{"id": "my-model", "provider": "openai", "model": "upstream-model"}]} + "model_catalog": {"aliases": [{"id": "my-model", "provider": "openai", "model": "upstream-model"}], "models": []} }); let generation = ExtensionPreparationPort::prepare(&runtime, ConfigRevision::new(1).unwrap()) .await @@ -297,7 +296,7 @@ async fn model_catalog_is_frozen_without_entering_the_request_chain() { #[tokio::test] async fn model_catalog_prepares_plugin_aliases_without_semantic_validation() { - for models in [ + for aliases in [ serde_json::json!([{"id":"alias","provider":"custom","model":"target"}]), serde_json::json!([{"id":"alias","provider":"openai","model":"alias"}]), serde_json::json!([ @@ -318,8 +317,8 @@ async fn model_catalog_prepares_plugin_aliases_without_semantic_validation() { .await; let mut snapshot = store.snapshot.lock().unwrap().clone(); snapshot.instances[0].configuration = - serde_json::json!({"model_catalog":{"models":models}}); - let expected = models.as_array().unwrap().len(); + serde_json::json!({"model_catalog":{"aliases":aliases,"models":[]}}); + let expected = aliases.as_array().unwrap().len(); let prepared = PluginPreparation::prepare(&runtime, snapshot) .await .unwrap(); @@ -340,7 +339,7 @@ async fn model_catalog_prepares_plugin_aliases_without_semantic_validation() { .await; let mut snapshot = store.snapshot.lock().unwrap().clone(); snapshot.instances[0].configuration = serde_json::json!({ - "model_catalog":{"models":[{"id":"alias","provider":"openai","model":"target"}]} + "model_catalog":{"aliases":[{"id":"alias","provider":"openai","model":"target"}],"models":[]} }); let mut other = snapshot.instances[0].clone(); other.id = "instance-two".into(); diff --git a/backend/crates/gateway-plugin/runtime/tests/package/compatibility.rs b/backend/crates/gateway-plugin/runtime/tests/package/compatibility.rs index cb409f736..0e7d96c37 100644 --- a/backend/crates/gateway-plugin/runtime/tests/package/compatibility.rs +++ b/backend/crates/gateway-plugin/runtime/tests/package/compatibility.rs @@ -81,13 +81,20 @@ async fn retired_contracts_remain_loadable_with_compatibility_warnings() { vec!["openai".into()], vec!["openai".into()], ); + let mut catalog = crate::support::contribution( + Capability::ModelCatalog, + vec![Stage::Registration], + vec![], + vec![], + ); if legacy { middleware.1.version = 3; upstream.1.version = 1; + catalog.1.version = 1; } let archive = crate::support::package_with_contributions( b"not-an-executable", - Contributions::from([middleware, upstream]), + Contributions::from([middleware, upstream, catalog]), ); let inspector = PackageInspector::new(PackageLimits::default(), semver::Version::new(1, 0, 0)); @@ -102,6 +109,7 @@ async fn retired_contracts_remain_loadable_with_compatibility_warnings() { let warning = warning.expect("retired contracts must remain visible"); assert!(warning.contains("middleware v3")); assert!(warning.contains("upstream_adapter v1")); + assert!(warning.contains("model_catalog v1")); } else { assert!(warning.is_none()); } diff --git a/backend/crates/gateway-plugin/sdk/docs/capabilities.md b/backend/crates/gateway-plugin/sdk/docs/capabilities.md index d702c7e06..a47fec5ca 100644 --- a/backend/crates/gateway-plugin/sdk/docs/capabilities.md +++ b/backend/crates/gateway-plugin/sdk/docs/capabilities.md @@ -61,7 +61,7 @@ Provider 固定为宿主内置的 OpenAI 与 xAI。宿主开放以下 11 类扩 | `middleware` | `PluginBuilder::middleware` / `middleware.handle` | | `upstream_adapter` | `methods::UPSTREAM_ADAPTER_REGISTER`、`methods::UPSTREAM_ADAPTER_EXECUTE`,见[受管上游](upstream-adapters.md) | | `model_router` | `methods::ROUTE_MODEL` | -| `model_catalog` | `PluginBuilder::model_catalog` / `methods::MODEL_CATALOG_REGISTER` | +| `model_catalog` | `PluginBuilder::model_catalog` / `methods::MODEL_CATALOG_REGISTER_V2` | | `retry_policy` | `methods::RETRY_DECISION` | | `scheduler` | `methods::SCHEDULE_ACCOUNT` | | `observer` | `methods::OBSERVE` / `observer.observe`,按事件类型订阅 | @@ -530,26 +530,29 @@ restore_and_validate_response(response).await ### 模型目录 -`model_catalog` v1 在 registration 阶段一次提交 `ModelCatalogRegistration { models: Vec }`,不需要请求级 binding。 -每条 `ModelAlias { id, provider, model }` 定义公开 ID、Provider 及直接上游目标,条目正确性由插件负责。 -每实例最多 256 条,整个二进制 JSON 最多 64 KiB;条目重复、目标不存在或 Provider 未注册不阻止宿主快照发布。 +`model_catalog` 仅支持 v2,在 registration 阶段通过 `model_catalog.register` 一次提交 +`ModelCatalogRegistrationV2 { aliases, models }`,不需要请求级 binding。 +`aliases` 的每条 `ModelAlias { id, provider, model }` 定义公开 ID、Provider 及直接上游目标,条目正确性由插件负责。 +别名条目重复、目标不存在或 Provider 未注册不阻止宿主快照发布。 请求按发布顺序取首个匹配条目,插件别名优先于静态映射,目标直接用于上游选路,不递归展开别名或目标上的静态映射 ```rust,ignore -use gateway_plugin_sdk::call::catalog::{ModelAlias, ModelCatalogRegistration}; +use gateway_plugin_sdk::call::catalog::{ModelAlias, ModelCatalogRegistrationV2}; let plugin = PluginBuilder::from_json(include_bytes!("../plugin.json"))? - .model_catalog(ModelCatalogRegistration { models: vec![ModelAlias { - id: "team-default".into(), provider: "openai".into(), model: "gpt-target".into(), - }] })? + .model_catalog(ModelCatalogRegistrationV2 { + aliases: vec![ModelAlias { + id: "team-default".into(), provider: "openai".into(), model: "gpt-target".into(), + }], + models: vec![], + })? .build()?; ``` 有效目录、路由和目标能力随同一个不可变快照发布,`/v1/models`、详情、原生目录和 `host.models.list` 共用该事实及现有 Key/账号模型范围。 目标存在时,别名复用目标的可公开对象;禁用后新请求不再使用该别名,在途请求持有旧代次。 -v1 是直接别名,动态选择可组合 `model_router`;实际路由仍须通过宿主的权限和能力校验,不提供多目标虚拟模型注册合同 +别名仅表达直接映射,动态选择可组合 `model_router`;实际路由仍须通过宿主的权限和能力校验,不提供多目标虚拟模型注册合同 -`model_catalog` v2 使用同一 RPC 方法,返回 `ModelCatalogRegistrationV2 { aliases, models }`。 -`aliases` 沿用 v1 直接别名语义;`models` 的每条 `AdaptedModel { id, adapter_id, features, max_output_tokens, presentation }` +`models` 的每条 `AdaptedModel { id, adapter_id, features, max_output_tokens, presentation }` 声明一个受管适配器提供的公开模型,不要求内置 Provider 的原生目录存在同名条目 - `adapter_id` 必须指向同实例已绑定的 `upstream_adapter`,Provider 与非空账号集合取自该适配器声明 @@ -557,10 +560,11 @@ v1 是直接别名,动态选择可组合 `model_router`;实际路由仍须 - `features` 使用 `RequestFeature`,仅为生成操作声明工具、图片输入、推理或 JSON Schema 支持;没有声明的能力不能借用同名原生模型的支持事实 - `presentation` 是独立通用画像,字段见 `call::catalog::ModelPresentation`;省略的布尔能力为 false,未声明推理档位不补猜 - 同名条目可关联不同账号,但 Provider、能力及画像须一致,账号集合不能重叠,也不能与插件直接别名同名,以免展示与实际选择产生歧义 -- v2 仍受每实例合计 256 条、64 KiB 信封限制;目录与适配器随同一代次发布,账号最终资格取当前 Key、分组及账号模型政策的交集 +- 别名与适配模型每实例合计最多 256 条,注册响应载荷最多 64 KiB;目录与适配器随同一代次发布,账号最终资格取当前 Key、分组及账号模型政策的交集 -使用 `PluginBuilder::on(methods::MODEL_CATALOG_REGISTER_V2, handler)` 返回 `TypedReply`, -并在清单声明 `model_catalog.version = 2`。原有 `.model_catalog(...)` 作者入口继续使用 v1 合同 +清单须显式声明 `model_catalog.version = 2`,可使用上述 `.model_catalog(...)` 固定目录, +或通过 `PluginBuilder::on(methods::MODEL_CATALOG_REGISTER_V2, handler)` 返回 `TypedReply`。 +使用 v1 的插件需将原 `models` 别名列表移入 `aliases`,补充 `models: []`,更新能力声明并重新构建、打包;仅修改清单版本号不能迁移旧结构 原生 Codex 目录成功返回后,宿主保留其中的完整对象,只追加可见且尚未出现的适配模型画像;原生读取错误仍向调用方返回错误。 适配模型的 wire 模型名、协议编解码及摘要由插件负责,宿主只使用公开 ID、能力和账号范围参与现有选号,不生成请求正文或转换历史 diff --git a/backend/crates/gateway-plugin/sdk/docs/manifest.md b/backend/crates/gateway-plugin/sdk/docs/manifest.md index 0c1b0235d..d2e5c4275 100644 --- a/backend/crates/gateway-plugin/sdk/docs/manifest.md +++ b/backend/crates/gateway-plugin/sdk/docs/manifest.md @@ -57,7 +57,7 @@ RPC v7 的帧布局、编码与会话生命周期见[通信边界](../README.md# | `scheduler` | `3` | | `upstream_adapter` | `5` | | `frontend_authentication`、`model_router`、`retry_policy`、`observer`、`management` | `2` | -| `model_catalog` | `1`、`2` | +| `model_catalog` | `2` | | `command_line`、`maintenance` | `1` | 例如,仅声明管理页面、命令行和模型请求中间件: diff --git a/backend/crates/gateway-plugin/sdk/src/call/catalog.rs b/backend/crates/gateway-plugin/sdk/src/call/catalog.rs index 91ac02cfb..6643a2d5b 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/catalog.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/catalog.rs @@ -2,12 +2,6 @@ use serde::{Deserialize, Serialize}; -/// 一次实例准备产生的完整目录,随实例配置原子发布 -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ModelCatalogRegistration { - pub models: Vec, -} - /// 指向内置 Provider 上游模型的直接别名,不创建新的 Provider 执行器 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ModelAlias { diff --git a/backend/crates/gateway-plugin/sdk/src/capability.rs b/backend/crates/gateway-plugin/sdk/src/capability.rs index 5ddca2c0a..fc9c9974c 100644 --- a/backend/crates/gateway-plugin/sdk/src/capability.rs +++ b/backend/crates/gateway-plugin/sdk/src/capability.rs @@ -29,12 +29,12 @@ impl Capability { Self::Middleware => &[6], Self::FrontendAuthentication | Self::ModelRouter + | Self::ModelCatalog | Self::RetryPolicy | Self::Observer | Self::Management => &[2], Self::Scheduler => &[3], Self::UpstreamAdapter => &[5], - Self::ModelCatalog => &[1, 2], Self::CommandLine | Self::Maintenance => &[1], } } diff --git a/backend/crates/gateway-plugin/sdk/src/client/plugin/methods.rs b/backend/crates/gateway-plugin/sdk/src/client/plugin/methods.rs index 206ff9f1a..8b137be5a 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/plugin/methods.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/plugin/methods.rs @@ -34,14 +34,6 @@ use super::{ }, }; -pub const MODEL_CATALOG_REGISTER: Method = Method::new( - "model_catalog.register", - &[C::ModelCatalog], - &[S::Registration], - decode_metadata_without_payload, - encode_payload, -); - pub const MODEL_CATALOG_REGISTER_V2: Method = Method::new( "model_catalog.register", diff --git a/backend/crates/gateway-plugin/sdk/src/client/plugin/mod.rs b/backend/crates/gateway-plugin/sdk/src/client/plugin/mod.rs index 90cdbb029..2601b31cd 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/plugin/mod.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/plugin/mod.rs @@ -143,16 +143,16 @@ impl PluginBuilder { Ok(self) } - /// 冻结模型别名目录;目标校验与发布由宿主负责 + /// 冻结直接别名与适配模型目录,随实例配置原子发布 /// /// # Errors /// /// 未声明模型目录能力或重复注册时失败 pub fn model_catalog( self, - registration: crate::call::catalog::ModelCatalogRegistration, + registration: crate::call::catalog::ModelCatalogRegistrationV2, ) -> Result { - self.on(methods::MODEL_CATALOG_REGISTER, move |_| { + self.on(methods::MODEL_CATALOG_REGISTER_V2, move |_| { let registration = registration.clone(); async move { Ok(TypedReply::new(registration)) } }) diff --git a/backend/crates/gateway-plugin/sdk/src/client/plugin/validation.rs b/backend/crates/gateway-plugin/sdk/src/client/plugin/validation.rs index d5d7212fc..3f452d74d 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/plugin/validation.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/plugin/validation.rs @@ -25,7 +25,7 @@ pub(super) fn validate( required(methods::UPSTREAM_ADAPTER_EXECUTE.name)?; } C::ModelRouter => required(methods::ROUTE_MODEL.name)?, - C::ModelCatalog => required(methods::MODEL_CATALOG_REGISTER.name)?, + C::ModelCatalog => required(methods::MODEL_CATALOG_REGISTER_V2.name)?, C::RetryPolicy => required(methods::RETRY_DECISION.name)?, C::Scheduler => required(methods::SCHEDULE_ACCOUNT.name)?, C::Observer => required(methods::OBSERVE.name)?, diff --git a/backend/crates/gateway-plugin/sdk/tests/client/plugin/mod.rs b/backend/crates/gateway-plugin/sdk/tests/client/plugin/mod.rs index 3528d8b23..347f5c36a 100644 --- a/backend/crates/gateway-plugin/sdk/tests/client/plugin/mod.rs +++ b/backend/crates/gateway-plugin/sdk/tests/client/plugin/mod.rs @@ -212,21 +212,28 @@ async fn composed_plugin_registers_and_dispatches_multiple_typed_entries() { #[tokio::test] async fn catalog_and_retry_entries_use_typed_registration_and_stage_validation() { use gateway_plugin_sdk::call::{ - catalog::{ModelAlias, ModelCatalogRegistration}, + catalog::{AdaptedModel, ModelAlias, ModelCatalogRegistrationV2, ModelPresentation}, policy::{RetryAction, RetryDecision, RetryDecisionRequest}, }; let source = author_manifest( - json!({"model_catalog":{},"retry_policy":{"version":2}}), + json!({"model_catalog":{"version":2},"retry_policy":{"version":2}}), json!([]), ); let manifest = gateway_plugin_sdk::Manifest::from_author_slice(&source).unwrap(); let contributions = manifest.contributes.clone(); - let catalog = ModelCatalogRegistration { - models: vec![ModelAlias { + let catalog = ModelCatalogRegistrationV2 { + aliases: vec![ModelAlias { id: "public-model".into(), provider: "openai".into(), model: "upstream-model".into(), }], + models: vec![AdaptedModel { + id: "adapted-model".into(), + adapter_id: "example".into(), + features: vec![], + max_output_tokens: None, + presentation: ModelPresentation::default(), + }], }; let plugin = PluginBuilder::from_manifest(manifest) .unwrap() @@ -257,7 +264,7 @@ async fn catalog_and_retry_entries_use_typed_registration_and_stage_validation() let (result, payload) = unwrap_result(receive(&mut host).await, 1); assert_eq!(result, json!({})); assert_eq!( - serde_json::from_slice::(&payload).unwrap(), + serde_json::from_slice::(&payload).unwrap(), catalog ); let request = serde_json::to_value(RetryDecisionRequest { diff --git a/backend/crates/gateway-plugin/sdk/tests/manifest.rs b/backend/crates/gateway-plugin/sdk/tests/manifest.rs index 6923dda54..b51561bc3 100644 --- a/backend/crates/gateway-plugin/sdk/tests/manifest.rs +++ b/backend/crates/gateway-plugin/sdk/tests/manifest.rs @@ -137,6 +137,7 @@ fn capability_contract_versions_are_checked_independently_of_rpc_version() { (Capability::Management, Stage::Management, 2, false), (Capability::UpstreamAdapter, Stage::Upstream, 5, true), (Capability::Scheduler, Stage::Scheduling, 3, false), + (Capability::ModelCatalog, Stage::Registration, 2, false), ] { for mut manifest in [source_manifest(), packaged_manifest()] { let formats = if needs_formats { diff --git a/docs/architecture.md b/docs/architecture.md index 476a44c01..c799f4142 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -131,9 +131,9 @@ flowchart LR 本节说明模型扩展的执行约束;配置与发布见[制品、配置与发布](#制品配置与发布),调用链见[请求组合与设置](#请求组合与设置),资源回收见[调用资源与状态](#调用资源与状态) -- **模型目录与路由共用事实**:`model_catalog` 的目录贡献保存在发布代次引用中,v1 直接别名由插件负责条目正确性,条目冲突或目标不存在不阻止宿主快照发布。 +- **模型目录与路由共用事实**:`model_catalog` 的目录贡献保存在发布代次引用中,直接别名由插件负责条目正确性,条目冲突或目标不存在不阻止宿主快照发布。 请求按顺序取首个匹配别名并直接使用其目标,不递归解析插件别名;Provider、账号权限与目标能力沿原选路流程处理。 - v2 适配模型从同实例受管适配器继承静态账号范围,Core 按冻结权限与请求能力收窄候选账号,不改变调度评分、亲和或租约。 + 适配模型从同实例受管适配器继承静态账号范围,Core 按冻结权限与请求能力收窄候选账号,不改变调度评分、亲和或租约。 公开列表、详情、原生目录和受管模型查询复用相同目录贡献与账号访问范围,不另维护插件专用目录;合同见 SDK [模型目录](../backend/crates/gateway-plugin/sdk/docs/capabilities.md#模型目录) - **重试由 Core 裁决**:`retry_policy` 只能停止或继续 Core 已允许的恢复路径;绑定按确定顺序委托,故障回退宿主。 发送、交付、续接、外部副作用、预算和取消限制不进入插件控制面,策略返回后继续复核