mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
refactor(plugins)!: remove model catalog v1 support
Keep direct aliases in v2 and update the SDK builder, runtime compatibility declaration, tests and migration guidance. BREAKING CHANGE: model_catalog plugins must declare version 2, move their alias list to aliases and return models for adapted models. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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] },
|
||||
|
||||
@@ -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 个模型条目"));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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<ModelAlias> }`,不需要请求级 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<ModelCatalogRegistrationV2>`,
|
||||
并在清单声明 `model_catalog.version = 2`。原有 `.model_catalog(...)` 作者入口继续使用 v1 合同
|
||||
清单须显式声明 `model_catalog.version = 2`,可使用上述 `.model_catalog(...)` 固定目录,
|
||||
或通过 `PluginBuilder::on(methods::MODEL_CATALOG_REGISTER_V2, handler)` 返回 `TypedReply<ModelCatalogRegistrationV2>`。
|
||||
使用 v1 的插件需将原 `models` 别名列表移入 `aliases`,补充 `models: []`,更新能力声明并重新构建、打包;仅修改清单版本号不能迁移旧结构
|
||||
|
||||
原生 Codex 目录成功返回后,宿主保留其中的完整对象,只追加可见且尚未出现的适配模型画像;原生读取错误仍向调用方返回错误。
|
||||
适配模型的 wire 模型名、协议编解码及摘要由插件负责,宿主只使用公开 ID、能力和账号范围参与现有选号,不生成请求正文或转换历史
|
||||
|
||||
@@ -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` |
|
||||
|
||||
例如,仅声明管理页面、命令行和模型请求中间件:
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 一次实例准备产生的完整目录,随实例配置原子发布
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ModelCatalogRegistration {
|
||||
pub models: Vec<ModelAlias>,
|
||||
}
|
||||
|
||||
/// 指向内置 Provider 上游模型的直接别名,不创建新的 Provider 执行器
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ModelAlias {
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +34,6 @@ use super::{
|
||||
},
|
||||
};
|
||||
|
||||
pub const MODEL_CATALOG_REGISTER: Method<Empty, catalog::ModelCatalogRegistration> = Method::new(
|
||||
"model_catalog.register",
|
||||
&[C::ModelCatalog],
|
||||
&[S::Registration],
|
||||
decode_metadata_without_payload,
|
||||
encode_payload,
|
||||
);
|
||||
|
||||
pub const MODEL_CATALOG_REGISTER_V2: Method<Empty, catalog::ModelCatalogRegistrationV2> =
|
||||
Method::new(
|
||||
"model_catalog.register",
|
||||
|
||||
@@ -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, AuthorError> {
|
||||
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)) }
|
||||
})
|
||||
|
||||
@@ -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)?,
|
||||
|
||||
@@ -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::<ModelCatalogRegistration>(&payload).unwrap(),
|
||||
serde_json::from_slice::<ModelCatalogRegistrationV2>(&payload).unwrap(),
|
||||
catalog
|
||||
);
|
||||
let request = serde_json::to_value(RetryDecisionRequest {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 已允许的恢复路径;绑定按确定顺序委托,故障回退宿主。
|
||||
发送、交付、续接、外部副作用、预算和取消限制不进入插件控制面,策略返回后继续复核
|
||||
|
||||
Reference in New Issue
Block a user