diff --git a/docs/adr/【ADR】模型权限与并发上限的强制边界-2026-10-03.md b/docs/adr/【ADR】模型权限与并发上限的强制边界-2026-10-03.md index c817b6756..658f92635 100644 --- a/docs/adr/【ADR】模型权限与并发上限的强制边界-2026-10-03.md +++ b/docs/adr/【ADR】模型权限与并发上限的强制边界-2026-10-03.md @@ -24,3 +24,14 @@ accepted(2026-10-03) - 模型限制在人工标注 `Full` 之前不会拦截任何请求。 - `raw` gpt-image-2 与其它直连同步生成不受 `concurrent_job_limit` 约束(TODO)。 - 无灰度开关;出问题只能回滚发版。 + +## 修订:列表改为可用 / 不可用分桶(2026-10-03) + +原实现让 `GET /api/llm/models` 只返回本档可用模型(`Basic` 看不到 `Full`),降级用户在客户端只会看到「所选模型已停用」的笼统提示,且产品无法在列表里露出可升级的高档模型。修订为: + +- 返回两个平行数组:`models` 只放本档可用(`enabled` 且档位允许);`unavailableModels` 放目录里其余全部,元素额外带 `reason` 枚举。 +- `reason` 取值 `plan_required`(档位不够)/ `disabled`(后台停用)/ `unknown`(前向兜底);同时停用且档位不够时取 `disabled`(升级也解锁不了,不能标 `plan_required`)。 +- 上游真实模型名(`modelId`)两桶都不下发;`defaultModelId` 必须落在 `models` 内。 +- 分桶只是展示层的信息补充,**不放松服务端强制**:`/api/llm/responses`、`/api/llm/chat/completions`、`/api/llm/anthropic/*` 仍按档 4xx。 +- `unavailableModels` 是加法字段:旧客户端忽略后行为不变;`reason` 在 wire 侧带 `unknown` 兜底,后端新增取值不会让旧客户端解析失败。 +- 目录 DTO 与枚举改为 ts-rs 单一真源(whole DTO 导出),`agentMode` / `protocol` 由 `String` 收敛为 Rust 枚举。 diff --git a/docs/project-memory/plans/【里程碑】AGC模型目录上游同步-2026-09-24.md b/docs/project-memory/plans/【里程碑】AGC模型目录上游同步-2026-09-24.md index 3c14f4a5e..327ac50b1 100644 --- a/docs/project-memory/plans/【里程碑】AGC模型目录上游同步-2026-09-24.md +++ b/docs/project-memory/plans/【里程碑】AGC模型目录上游同步-2026-09-24.md @@ -17,6 +17,8 @@ 2. 拉不到就报错、不写替代目录,并在下一次启动继续重试,直到目录里有数据。 3. **保持既有格式与契约不变**:目录字段(`id`/`alias`/`modelId`/`defaultModelId`)、后台页面与 DTO、`GET /api/llm/models` 形状、客户端模型标识校验都不变,不引入不兼容变更。 +> 2026-10-03 修订:本条「`GET /api/llm/models` 形状不变」只约束本里程碑自身;后续「模型权限」变更已把该接口改为可用 / 不可用分桶(新增 `unavailableModels` 与 `reason` 枚举),见 `docs/adr/【ADR】模型权限与并发上限的强制边界-2026-10-03.md` 的修订节。 + ## 不在本里程碑内 - 不改目录字段语义与后台维护方式,不删别名/稳定标识概念。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index cafcb0a0e..ab10fcd52 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -9642,6 +9642,16 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 验证:`cargo check -p api-server`、`cargo check -p api-server --tests`;`cargo test -p api-server llm` 49 passed、`model_catalog_tests` 3 passed(含 Basic 档过滤 Full 模型、403 错误码映射);`npm run check:spacetime-schema` 的失败项全部是分支既有 `profile_membership` / `profile_recharge_product_config` 字段变更(对照 `origin/master` merge-base `25beb3ad5`),与本次仅新增 btree 索引无关,guard 不比较索引;`npm run check:encoding`、`git diff --check`。 - 边界 / TODO:账号档位仍复用带周期刷新写入的 `get_profile_recharge_center`,后续替换为轻量专用读;AGC 客户端仍靠「目录按档过滤 + 本地选择对账回退默认项」,未加专门的 403 重选提示;统一「账号在飞生成」口径(含无 job 行的同步生成)仍为后续 TODO。 +## 2026-10-03 模型列表改为可用 / 不可用分桶(修订) + +- 背景:`GET /api/llm/models` 原按档过滤,降级用户在客户端只看到「所选模型已停用」的笼统提示,且无法在列表里露出可升级的高档模型。 +- 决策(契约):返回 `models`(本档可用)与 `unavailableModels`(目录里其余全部,带 `reason` 枚举)两个平行数组,**不新增** `available` 布尔;`reason` 取 `plan_required` / `disabled` / `unknown`,同时停用且档位不够取 `disabled`;`unavailableModels` 保持目录顺序、恒定下发(无内容为空数组);上游真实模型名两桶都不下发;`defaultModelId` 必须落在 `models` 内;某档一个可用模型都没有时保留 503 `MODEL_UNAVAILABLE`。 +- 决策(兼容):`unavailableModels` 是加法字段,旧客户端忽略后行为不变,无需能力协商;`reason` 在 wire 侧带 `unknown` 兜底,后端将来新增取值不会让旧客户端解析失败;服务端按档强制不变。 +- 决策(客户端):AGC 桌面选择器把 `unavailableModels` 渲染为不可点项,仅 hover / 键盘 focus 出 tooltip(`plan_required`→「订阅计划不支持」、`disabled`→「该模型已下线」、`unknown`→「暂不可用」);降级仍自动回退默认,提示按 `reason` 取;后台直接删除(两桶都没有)用泛化文案「所选模型已不可用,已切回默认模型」;不再新增「当前已选不可用」的 API 字段,前端自行从 `unavailableModels` 推断。 +- 决策(ts-rs 单一真源):整个目录 DTO 与枚举导出到 `packages/shared/src/contracts/generated/`;新增 barrel `packages/shared/src/llm/modelCatalog.ts` 并由 `packages/shared/src/index.ts` 转发;`AgcAgentMode` / `AgcModelProtocol` 从 `module-runtime` 迁入 `shared-contracts`(`module-runtime` re-export);`revision: u64` 标 `#[ts(as = "f64")]`;AGC 前端删除手写 `ClientLlmModel`,改从 `@genarrative/shared` 根导入。 +- 影响范围:`docs/adr/【ADR】模型权限与并发上限的强制边界-2026-10-03.md`(修订节)、技术设计 §11.2/§11.4、`docs/technical/【技术方案】AGC后台模型别名与对话选择-2026-09-05.md`、`docs/project-memory/plans/【里程碑】AGC模型目录上游同步-2026-09-24.md`、本文件。 +- 验证:`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。 + ## 2026-10-03 会员链路错误分类改为机器可读错误码(typed error) - 背景:`api-server/src/runtime_profile.rs` 的 `is_runtime_profile_membership_domain_error` 用中文 `error_message` 前缀 / 精确匹配决定返回 400 还是 502;文案一改(或新增拒绝点)就静默退化,且分类逻辑与 module 的错误文案跨层重复。 diff --git a/docs/technical/【技术方案】AGC后台模型别名与对话选择-2026-09-05.md b/docs/technical/【技术方案】AGC后台模型别名与对话选择-2026-09-05.md index d14ae6087..eb2e3cc57 100644 --- a/docs/technical/【技术方案】AGC后台模型别名与对话选择-2026-09-05.md +++ b/docs/technical/【技术方案】AGC后台模型别名与对话选择-2026-09-05.md @@ -42,6 +42,7 @@ - 上游变化不自动跟随:目录只在未初始化时重建;上游新增或移除模型由 owner 在后台增删条目或调整启用、默认项。 - `GET/PUT /admin/api/agc-models` 仅 owner 可用,返回完整配置;PUT 携带上次读取的 revision,冲突拒绝覆盖。 - `GET /api/llm/models` 返回启用项的 `id/displayName`、`defaultModelId` 和目录 `revision`,不返回实际模型名、Router 目录、凭据或能力原始数据。 +- 2026-10-03 修订(模型权限):`GET /api/llm/models` 改为两个平行数组——`models` 只放本档可用;`unavailableModels` 放目录里其余(含后台停用与本档档位不够),元素带 `reason` 枚举(`plan_required` / `disabled` / `unknown`,同时停用且档位不够取 `disabled`)。`unavailableModels` 是加法字段,旧客户端忽略后行为不变,且不放松服务端按档强制;`defaultModelId` 仍必须落在 `models` 内。客户端把 `unavailableModels` 渲染为不可点项并 hover / focus 出原因(`plan_required`→「订阅计划不支持」、`disabled`→「该模型已下线」、`unknown`→「暂不可用」);已选模型落进不可用桶时自动回退默认并给出对应原因文案,后台直接删除(两桶都没有)用泛化文案。 - 客户端缓存最近 `revision`,在项目切换 / 对话表面挂载 / 下拉展开 / 窗口聚焦时条件刷新:`revision` 未变化不更新界面,同一时刻只保留一个在途请求,刷新失败保留上一次有效目录与本地选择。发起对话前用同一份快照校验所选模型仍启用,已停用或删除则回退默认模型并提示。 - 手动刷新立即显示进行中状态;真实刷新成功后显示完成反馈,即使 `revision` 未变化也有反馈。失败沿用有效缓存时仍显示失败,不能报告刷新成功;HTTP 状态和超时使用可辨认的提示。 - 模型目录与其它客户端 JSON API 的成功、失败响应体读取均复用 `readClientHttpResponseText` 的 15 秒上限;响应头已返回但响应体卡住时必须结束本次等待、释放目录在途请求并允许重试,迟到的响应不得覆盖新目录。 diff --git a/docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md b/docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md index 182072df7..d6826571b 100644 --- a/docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md +++ b/docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md @@ -588,7 +588,7 @@ flowchart TD - **标注**:由**人工在后台「AGC 模型」页手动**把高性能模型标为 `Full`;代码里**不**写死清单、不播种。 - **档位解析**:`load_llm_catalog(owner)` 已带 owner;由 `profile_membership.plan` → 目录 `model_access` 解析;缺档位失败关闭到 `Normal`(`Basic`)。 - **生效点**: - - `GET /api/llm/models`:按账号档位过滤(`Basic` 档只看到非 `Full` 模型),`defaultModelId` 取该档可用集合的默认项(见 §11.4)。 + - `GET /api/llm/models`:返回 `models`(本档可用)与 `unavailableModels`(目录里其余,元素带 `reason` 枚举 `plan_required` / `disabled` / `unknown`;同时停用且档位不够取 `disabled`),`defaultModelId` 取该档可用集合的默认项(见 §11.4)。分桶只补充展示信息,不放松下述强校验。 - `POST /api/llm/responses`:`catalog.resolve_requested()` 映射后校验档位,越权返回 4xx 专用错误码 `MODEL_NOT_AVAILABLE_FOR_PLAN`。 - `POST /api/llm/chat/completions`(`model` 恒为默认)与 anthropic bridge:只需保证解析出的默认模型落在该档可用集合内。 - 客户端本地残留旧选择(`select_game_creator_model` 持久化)→ **明确 4xx**,由客户端提示重选;**不静默回退**。 @@ -609,7 +609,10 @@ flowchart TD - **`api-server`**:新增 `llm/model_access.rs`,用现有 `get_profile_recharge_center` 读出账号 `membership.plan` 与目录行 `model_access` 并映射到 `AgcModelAccess`;`GET /api/llm/models` 按档过滤,`/api/llm/responses`、`/api/llm/chat/completions`、`/api/llm/anthropic/*` 全部按档解析,越权返回 403 + 错误码 `MODEL_NOT_AVAILABLE_FOR_PLAN`。 - **后台**:`AdminAgcModel` 与后台「AGC 模型」页新增 `access` 字段(`basic` / `full`,缺省 `basic`),GET 回读、PUT 写入;**不做**自动播种或代码内清单。 - **性能说明**:本期账号档位复用现有 `get_profile_recharge_center` 读模型,它带幂等的周期刷新写入;后续可替换为更轻的专用读(TODO)。 -- **落地状态(2026-10-03)**:`module-runtime` 权限档与目录解析 `b78003143`;`spacetime-module` 并发索引与认领过滤 `db8d467b8`;后台 `access` 标注与页面 `919b4b01d`;`api-server` 按档过滤 / 校验 `9b1a538d2`。AGC 客户端侧仍以「目录按档过滤 + 本地选择对账回退默认项」为主,未额外新增针对 403 `MODEL_NOT_AVAILABLE_FOR_PLAN` 的专门提示,作为后续可选项。 +- **列表契约(2026-10-03 修订)**:`shared-contracts` 新增 `llm_catalog.rs` 承载 `LlmModelsResponse` / `LlmModelSummary` / `LlmUnavailableModel` / `LlmModelUnavailableReason`(`plan_required` / `disabled` / `unknown`);`AgcAgentMode` / `AgcModelProtocol` 从 `module-runtime` 迁入 `shared-contracts` 并由 `module-runtime` re-export;整个目录 DTO 与枚举用 `#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))]` 导出到 `packages/shared/src/contracts/generated/`,`revision` 标 `#[ts(as = "f64")]`,AGC 前端从 `@genarrative/shared` 根导入,删掉手写的 `ClientLlmModel`。 +- **分桶来源**:`module-runtime` 新增 `AgcModelCatalog::unavailable_models_for(access)`(保持目录顺序,返回 `(&AgcModel, LlmModelUnavailableReason)`),`api-server` 只做 DTO 映射;`models` 仍走 `available_models_for`;`unavailableModels` 恒定下发(无内容时为空数组)。 +- **客户端**:AGC 桌面选择器把 `unavailableModels` 渲染为不可点项,hover / 键盘 focus 出 tooltip(`plan_required` → 「订阅计划不支持」、`disabled` → 「该模型已下线」、`unknown` → 「暂不可用」);降级仍自动回退默认,提示按 `reason` 取;后台直接删除(两桶都没有)用泛化文案「所选模型已不可用,已切回默认模型」。 +- **落地状态(2026-10-03)**:`module-runtime` 权限档与目录解析 `da9602839`;`spacetime-module` 并发索引与认领过滤 `df7682816`;后台 `access` 标注与页面 `54b02e228`;`api-server` 按档过滤 / 校验 `3aae77bc5`;认领按账号折叠并发查询 `c3c3fc5a2`。AGC 客户端侧原以「目录按档过滤 + 本地选择对账回退默认项」为主,本次修订为可用 / 不可用分桶。 ---