feat(会员): 模型列表返回可用与不可用分桶
- shared-contracts 新增 llm_catalog.rs:LlmModelsResponse/LlmModelSummary/LlmUnavailableModel/LlmModelUnavailableReason(plan_required/disabled/unknown),未停用优先于档位的 disabled 语义留给域层 - AgcAgentMode/AgcModelProtocol 从 module-runtime 迁入 shared-contracts(前后端 + ts-rs 单一真源),module-runtime re-export 保持路径可用 - 整个目录 DTO 与枚举走 ts-rs,生成到 packages/shared/src/contracts/generated,revision 标 number - module-runtime 新增 AgcModelCatalog::unavailable_models_for(access):保持目录顺序,停用优先于档位不足,补单测 - api-server public_model_catalog 产出 models + unavailableModels 两桶,重写 model_catalog_tests
This commit is contained in:
@@ -14,7 +14,7 @@ use platform_llm::{LlmApiKind, LlmMessage, LlmMessageRole, LlmRunRequest};
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::llm::{
|
||||
LlmChatCompletionRequest, LlmChatCompletionResponse, LlmChatMessagePayload, LlmChatMessageRole,
|
||||
LlmModelSummary, LlmModelsResponse,
|
||||
LlmModelSummary, LlmModelsResponse, LlmUnavailableModel,
|
||||
};
|
||||
use spacetime_client::SpacetimeClientError;
|
||||
use std::convert::Infallible;
|
||||
@@ -52,7 +52,7 @@ mod model_catalog_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn public_catalog_exposes_stable_id_and_upstream_alias() {
|
||||
fn public_catalog_splits_available_and_disabled_models() {
|
||||
let mut catalog = module_runtime::AgcModelCatalog::from_upstream_models(
|
||||
vec!["gpt-5.6-sol".to_string(), "gpt-5.6-terra".to_string()],
|
||||
7,
|
||||
@@ -72,15 +72,21 @@ mod model_catalog_tests {
|
||||
payload["models"],
|
||||
json!([{"id": "gpt-5-6-sol", "displayName": "gpt-5.6-sol", "agentMode": "codex", "protocol": "openai_responses"}])
|
||||
);
|
||||
// 后台停用的模型仍下发到不可用桶,带 disabled 原因。
|
||||
assert_eq!(
|
||||
payload["unavailableModels"],
|
||||
json!([{"id": "gpt-5-6-terra", "displayName": "gpt-5.6-terra", "agentMode": "codex", "protocol": "openai_responses", "reason": "disabled"}])
|
||||
);
|
||||
assert_eq!(payload["defaultModelId"], "gpt-5-6-sol");
|
||||
assert_eq!(payload["revision"], json!(7));
|
||||
assert!(payload.get("defaultModel").is_none());
|
||||
assert!(payload["models"][0].get("enabled").is_none());
|
||||
assert!(payload["models"][0].get("modelId").is_none());
|
||||
assert!(payload["unavailableModels"][0].get("modelId").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_catalog_filters_full_models_for_basic_plan() {
|
||||
fn public_catalog_puts_full_models_in_plan_required_bucket_for_basic_plan() {
|
||||
let mut catalog = module_runtime::AgcModelCatalog::from_upstream_models(
|
||||
vec!["gpt-5.6-sol".to_string(), "gpt-5.6-terra".to_string()],
|
||||
0,
|
||||
@@ -100,6 +106,10 @@ mod model_catalog_tests {
|
||||
payload["models"],
|
||||
json!([{"id": "gpt-5-6-terra", "displayName": "gpt-5.6-terra", "agentMode": "codex", "protocol": "openai_responses"}])
|
||||
);
|
||||
assert_eq!(
|
||||
payload["unavailableModels"],
|
||||
json!([{"id": "gpt-5-6-sol", "displayName": "gpt-5.6-sol", "agentMode": "codex", "protocol": "openai_responses", "reason": "plan_required"}])
|
||||
);
|
||||
assert_eq!(payload["defaultModelId"], "gpt-5-6-terra");
|
||||
}
|
||||
|
||||
@@ -268,20 +278,29 @@ fn public_model_catalog(
|
||||
.to_string();
|
||||
let models = catalog
|
||||
.available_models_for(access)
|
||||
.map(|model| {
|
||||
let protocol = model.resolved_protocol().as_str().to_string();
|
||||
LlmModelSummary {
|
||||
id: model.id.clone(),
|
||||
// 初始目录里别名就是上游原始模型名(不再填“高质量/快速”这类人工别名)。
|
||||
display_name: model.alias.clone(),
|
||||
agent_mode: model.agent_mode.as_str().to_string(),
|
||||
protocol,
|
||||
}
|
||||
.map(|model| LlmModelSummary {
|
||||
id: model.id.clone(),
|
||||
// 初始目录里别名就是上游原始模型名(不再填“高质量/快速”这类人工别名)。
|
||||
display_name: model.alias.clone(),
|
||||
agent_mode: model.agent_mode,
|
||||
protocol: model.resolved_protocol(),
|
||||
})
|
||||
.collect();
|
||||
// 不可用桶只是展示信息:含后台停用与本档档位不够,保持目录顺序。
|
||||
let unavailable_models = catalog
|
||||
.unavailable_models_for(access)
|
||||
.map(|(model, reason)| LlmUnavailableModel {
|
||||
id: model.id.clone(),
|
||||
display_name: model.alias.clone(),
|
||||
agent_mode: model.agent_mode,
|
||||
protocol: model.resolved_protocol(),
|
||||
reason,
|
||||
})
|
||||
.collect();
|
||||
Ok(LlmModelsResponse {
|
||||
default_model_id,
|
||||
models,
|
||||
unavailable_models,
|
||||
revision: catalog.revision,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::{
|
||||
AGC_MODEL_PLATFORM_DEFAULT, AgcModel, AgcModelCatalog, RuntimeProfileMembershipModelAccess,
|
||||
};
|
||||
use shared_contracts::llm_catalog::LlmModelUnavailableReason;
|
||||
|
||||
/// 越权选择模型时对外返回的专用错误码(`api-server` 直接透传到 `AppError.code`)。
|
||||
pub const MODEL_NOT_AVAILABLE_FOR_PLAN: &str = "MODEL_NOT_AVAILABLE_FOR_PLAN";
|
||||
@@ -105,6 +106,26 @@ impl AgcModelCatalog {
|
||||
.filter(move |model| model.is_available_for(access))
|
||||
}
|
||||
|
||||
/// 该档位**不可用**的模型(含后台停用与本档档位不够),保持目录顺序。
|
||||
///
|
||||
/// 与 [`Self::available_models_for`] 互补:两桶并集是目录全集、互不相交。同时停用且
|
||||
/// 档位不够时取 [`LlmModelUnavailableReason::Disabled`]——升级也解锁不了,不能标
|
||||
/// `plan_required` 误导用户。
|
||||
pub fn unavailable_models_for(
|
||||
&self,
|
||||
access: AgcModelAccess,
|
||||
) -> impl Iterator<Item = (&AgcModel, LlmModelUnavailableReason)> {
|
||||
self.models.iter().filter_map(move |model| {
|
||||
if !model.enabled {
|
||||
Some((model, LlmModelUnavailableReason::Disabled))
|
||||
} else if !access.allows(model.access) {
|
||||
Some((model, LlmModelUnavailableReason::PlanRequired))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 该档位的默认模型标识:优先目录登记的 `default_model_id`,不在该档可用集合里时
|
||||
/// 回退到目录顺序里第一个可用项;一个可用模型都没有时返回 `None`。
|
||||
pub fn default_model_id_for(&self, access: AgcModelAccess) -> Option<&str> {
|
||||
@@ -287,4 +308,40 @@ mod tests {
|
||||
"MODEL_UNAVAILABLE"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_models_for_splits_disabled_and_plan_gated_in_catalog_order() {
|
||||
let mut catalog = catalog_from_ids(&["model-a", "model-b", "model-c"], 0);
|
||||
mark_full(&mut catalog, "model-b");
|
||||
mark_full(&mut catalog, "model-c");
|
||||
catalog.models[2].enabled = false;
|
||||
|
||||
let unavailable = catalog
|
||||
.unavailable_models_for(AgcModelAccess::Basic)
|
||||
.map(|(model, reason)| (model.id.as_str(), reason))
|
||||
.collect::<Vec<_>>();
|
||||
// model-c 同时停用且 Full:reason 取 disabled(升级也解锁不了)。
|
||||
assert_eq!(
|
||||
unavailable,
|
||||
vec![
|
||||
("model-b", LlmModelUnavailableReason::PlanRequired),
|
||||
("model-c", LlmModelUnavailableReason::Disabled),
|
||||
]
|
||||
);
|
||||
|
||||
// Full 档只剩停用的 model-c;可用集合与不可用集合互补。
|
||||
let unavailable_full = catalog
|
||||
.unavailable_models_for(AgcModelAccess::Full)
|
||||
.map(|(model, reason)| (model.id.as_str(), reason))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
unavailable_full,
|
||||
vec![("model-c", LlmModelUnavailableReason::Disabled)]
|
||||
);
|
||||
let available = catalog
|
||||
.available_models_for(AgcModelAccess::Basic)
|
||||
.map(|model| model.id.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(available, vec!["model-a"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,99 +14,9 @@ pub const AGC_MODEL_ID_MAX_BYTES: usize = 64;
|
||||
/// 目录项数上限,与后台「AGC 模型」页的新增上限保持一致。
|
||||
pub const AGC_MODEL_CATALOG_MAX_MODELS: usize = 32;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AgcAgentMode {
|
||||
Codex,
|
||||
/// `anthropic` 是早期把协议混进执行器时写下的历史取值,读取时归一到 `cc`;
|
||||
/// 协议本身由 `AgcModel.protocol` 表达。
|
||||
#[serde(alias = "anthropic")]
|
||||
Cc,
|
||||
}
|
||||
|
||||
impl Default for AgcAgentMode {
|
||||
fn default() -> Self {
|
||||
Self::Codex
|
||||
}
|
||||
}
|
||||
|
||||
impl AgcAgentMode {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Codex => "codex",
|
||||
Self::Cc => "cc",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"codex" => Ok(Self::Codex),
|
||||
"cc" => Ok(Self::Cc),
|
||||
_ => Err("Agent 模式只允许 codex 或 cc".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 该执行器允许的线协议。协议与执行器是正交维度,但合法组合是封闭的:
|
||||
/// Codex 只能走 OpenAI Responses / Chat Completions,Claude Code 只能走
|
||||
/// Anthropic Messages。
|
||||
pub const fn supports(self, protocol: AgcModelProtocol) -> bool {
|
||||
match self {
|
||||
Self::Codex => matches!(
|
||||
protocol,
|
||||
AgcModelProtocol::OpenAiResponses | AgcModelProtocol::OpenAiChat
|
||||
),
|
||||
Self::Cc => matches!(protocol, AgcModelProtocol::Anthropic),
|
||||
}
|
||||
}
|
||||
|
||||
/// 存量目录缺少协议字段时按执行器的默认线协议解释。
|
||||
pub const fn default_protocol(self) -> AgcModelProtocol {
|
||||
match self {
|
||||
Self::Codex => AgcModelProtocol::OpenAiResponses,
|
||||
Self::Cc => AgcModelProtocol::Anthropic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 上游线协议,与执行器正交。
|
||||
///
|
||||
/// `openai_responses` / `openai_chat` 走 `/api/llm/v1/responses` 与
|
||||
/// `/api/llm/v1/chat/completions`;`anthropic` 走独立的
|
||||
/// `/api/llm/anthropic/{*path}`。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum AgcModelProtocol {
|
||||
#[serde(rename = "openai_responses")]
|
||||
OpenAiResponses,
|
||||
#[serde(rename = "openai_chat")]
|
||||
OpenAiChat,
|
||||
#[serde(rename = "anthropic")]
|
||||
Anthropic,
|
||||
}
|
||||
|
||||
impl Default for AgcModelProtocol {
|
||||
fn default() -> Self {
|
||||
Self::OpenAiResponses
|
||||
}
|
||||
}
|
||||
|
||||
impl AgcModelProtocol {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::OpenAiResponses => "openai_responses",
|
||||
Self::OpenAiChat => "openai_chat",
|
||||
Self::Anthropic => "anthropic",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"openai_responses" => Ok(Self::OpenAiResponses),
|
||||
"openai_chat" => Ok(Self::OpenAiChat),
|
||||
"anthropic" => Ok(Self::Anthropic),
|
||||
_ => Err("协议只允许 openai_responses、openai_chat 或 anthropic".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
// 执行器与线协议枚举已迁到 `shared-contracts`(前后端 + ts-rs 单一真源);
|
||||
// 这里 re-export,保持 `module_runtime::AgcAgentMode` / `AgcModelProtocol` 路径可用。
|
||||
pub use shared_contracts::llm_catalog::{AgcAgentMode, AgcModelProtocol};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
|
||||
@@ -23,5 +23,6 @@ pub mod game_creation_app;
|
||||
pub mod game_distribution;
|
||||
pub mod hyper3d;
|
||||
pub mod llm;
|
||||
pub mod llm_catalog;
|
||||
pub mod payment;
|
||||
pub mod runtime;
|
||||
|
||||
@@ -60,22 +60,9 @@ mod tests {
|
||||
assert_eq!(payload["messages"][0]["role"], json!("system"));
|
||||
}
|
||||
}
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LlmModelSummary {
|
||||
pub display_name: String,
|
||||
pub id: String,
|
||||
/// 执行器:`codex` 或 `cc`。
|
||||
pub agent_mode: String,
|
||||
/// 上游线协议,与执行器正交:`openai_responses` / `openai_chat` / `anthropic`。
|
||||
pub protocol: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LlmModelsResponse {
|
||||
pub default_model_id: String,
|
||||
pub models: Vec<LlmModelSummary>,
|
||||
/// 模型目录版本,客户端据此做条件刷新。
|
||||
pub revision: u64,
|
||||
}
|
||||
// 目录 DTO 与枚举已迁到 `llm_catalog`(同一 ts-rs 生成目录);这里 re-export 保持既有
|
||||
// `shared_contracts::llm::*` 路径可用。
|
||||
pub use crate::llm_catalog::{
|
||||
AgcAgentMode, AgcModelProtocol, LlmModelSummary, LlmModelUnavailableReason, LlmModelsResponse,
|
||||
LlmUnavailableModel,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
//! AGC LLM 模型目录的对外契约与枚举(前后端唯一真源)。
|
||||
//!
|
||||
//! `GET /api/llm/models` 返回两个平行数组:`models` 只放本档可用模型;
|
||||
//! `unavailableModels` 放目录里其余全部(含后台停用与本档档位不够),元素带 `reason`。
|
||||
//! 整个 DTO 与枚举经 ts-rs 导出到 `packages/shared/src/contracts/generated/`,
|
||||
//! AGC 前端不再手写第二份类型。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 客户端启动的执行器。与线协议正交。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
feature = "ts-bindings",
|
||||
ts(
|
||||
export,
|
||||
export_to = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../../packages/shared/src/contracts/generated/"
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub enum AgcAgentMode {
|
||||
Codex,
|
||||
/// `anthropic` 是早期把协议混进执行器时写下的历史取值,读取时归一到 `cc`;
|
||||
/// 协议本身由 [`AgcModelProtocol`] 表达。
|
||||
#[serde(alias = "anthropic")]
|
||||
Cc,
|
||||
}
|
||||
|
||||
impl Default for AgcAgentMode {
|
||||
fn default() -> Self {
|
||||
Self::Codex
|
||||
}
|
||||
}
|
||||
|
||||
impl AgcAgentMode {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Codex => "codex",
|
||||
Self::Cc => "cc",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"codex" => Ok(Self::Codex),
|
||||
"cc" => Ok(Self::Cc),
|
||||
_ => Err("Agent 模式只允许 codex 或 cc".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 该执行器允许的线协议。协议与执行器是正交维度,但合法组合是封闭的:
|
||||
/// Codex 只能走 OpenAI Responses / Chat Completions,Claude Code 只能走
|
||||
/// Anthropic Messages。
|
||||
pub const fn supports(self, protocol: AgcModelProtocol) -> bool {
|
||||
match self {
|
||||
Self::Codex => matches!(
|
||||
protocol,
|
||||
AgcModelProtocol::OpenAiResponses | AgcModelProtocol::OpenAiChat
|
||||
),
|
||||
Self::Cc => matches!(protocol, AgcModelProtocol::Anthropic),
|
||||
}
|
||||
}
|
||||
|
||||
/// 存量目录缺少协议字段时按执行器的默认线协议解释。
|
||||
pub const fn default_protocol(self) -> AgcModelProtocol {
|
||||
match self {
|
||||
Self::Codex => AgcModelProtocol::OpenAiResponses,
|
||||
Self::Cc => AgcModelProtocol::Anthropic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 上游线协议,与执行器正交。
|
||||
///
|
||||
/// `openai_responses` / `openai_chat` 走 `/api/llm/v1/responses` 与
|
||||
/// `/api/llm/v1/chat/completions`;`anthropic` 走独立的
|
||||
/// `/api/llm/anthropic/{*path}`。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
feature = "ts-bindings",
|
||||
ts(
|
||||
export,
|
||||
export_to = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../../packages/shared/src/contracts/generated/"
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub enum AgcModelProtocol {
|
||||
#[serde(rename = "openai_responses")]
|
||||
OpenAiResponses,
|
||||
#[serde(rename = "openai_chat")]
|
||||
OpenAiChat,
|
||||
#[serde(rename = "anthropic")]
|
||||
Anthropic,
|
||||
}
|
||||
|
||||
impl Default for AgcModelProtocol {
|
||||
fn default() -> Self {
|
||||
Self::OpenAiResponses
|
||||
}
|
||||
}
|
||||
|
||||
impl AgcModelProtocol {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::OpenAiResponses => "openai_responses",
|
||||
Self::OpenAiChat => "openai_chat",
|
||||
Self::Anthropic => "anthropic",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"openai_responses" => Ok(Self::OpenAiResponses),
|
||||
"openai_chat" => Ok(Self::OpenAiChat),
|
||||
"anthropic" => Ok(Self::Anthropic),
|
||||
_ => Err("协议只允许 openai_responses、openai_chat 或 anthropic".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 本档可用的一条模型摘要。不包含上游真实模型名。
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
feature = "ts-bindings",
|
||||
ts(
|
||||
export,
|
||||
export_to = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../../packages/shared/src/contracts/generated/"
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub struct LlmModelSummary {
|
||||
pub display_name: String,
|
||||
pub id: String,
|
||||
/// 执行器;旧服务端缺字段时按 `codex` 兼容。
|
||||
#[serde(default)]
|
||||
pub agent_mode: AgcAgentMode,
|
||||
/// 上游线协议;旧服务端缺字段时按 `openai_responses` 兼容。
|
||||
#[serde(default)]
|
||||
pub protocol: AgcModelProtocol,
|
||||
}
|
||||
|
||||
/// 目录里存在、但当前账号不可用的一条模型。字段与 [`LlmModelSummary`] 对齐,另带原因。
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
feature = "ts-bindings",
|
||||
ts(
|
||||
export,
|
||||
export_to = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../../packages/shared/src/contracts/generated/"
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub struct LlmUnavailableModel {
|
||||
pub display_name: String,
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub agent_mode: AgcAgentMode,
|
||||
#[serde(default)]
|
||||
pub protocol: AgcModelProtocol,
|
||||
pub reason: LlmModelUnavailableReason,
|
||||
}
|
||||
|
||||
/// 模型对当前账号不可用的原因。
|
||||
///
|
||||
/// `Unknown` 是前向兜底:后端将来新增取值时旧客户端不会解析失败。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
feature = "ts-bindings",
|
||||
ts(
|
||||
export,
|
||||
export_to = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../../packages/shared/src/contracts/generated/"
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub enum LlmModelUnavailableReason {
|
||||
/// 会员档位不够(升级可解锁)。
|
||||
PlanRequired,
|
||||
/// 后台人工停用(升级也解锁不了)。
|
||||
Disabled,
|
||||
/// 未知原因的前向兜底;当前服务端不会主动产出。
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// `GET /api/llm/models` 的响应。
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
feature = "ts-bindings",
|
||||
ts(
|
||||
export,
|
||||
export_to = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../../packages/shared/src/contracts/generated/"
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub struct LlmModelsResponse {
|
||||
pub default_model_id: String,
|
||||
/// 本档可用,保持目录顺序。
|
||||
pub models: Vec<LlmModelSummary>,
|
||||
/// 目录里其余全部,保持目录顺序;旧服务端缺字段时为空。
|
||||
#[serde(default)]
|
||||
pub unavailable_models: Vec<LlmUnavailableModel>,
|
||||
/// 模型目录版本;ts-rs 默认把 `u64` 映射成 `bigint`,前端消费 `number`。
|
||||
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
|
||||
pub revision: u64,
|
||||
}
|
||||
Reference in New Issue
Block a user