diff --git a/.agents/skills/unidesk-sub2api/SKILL.md b/.agents/skills/unidesk-sub2api/SKILL.md index 3972d0dc..60f3d717 100644 --- a/.agents/skills/unidesk-sub2api/SKILL.md +++ b/.agents/skills/unidesk-sub2api/SKILL.md @@ -56,17 +56,24 @@ bun scripts/cli.ts platform-infra sub2api codex-pool validate - `pool.groupName`: Sub2API group 名称。 - `pool.apiKeySecretName` / `pool.apiKeySecretKey`: 统一消费 API key 的 k3s Secret 位置,默认 `platform-infra/sub2api-codex-pool-api-key.API_KEY`。 - `pool.minOwnerBalanceUsd`: pool key owner 最低余额,sync/validate 会补齐。 +- `pool.defaultTempUnschedulable`: 默认账号级临时下线规则;用于在上游返回容量、限流、overload 或认证状态异常时,让 Sub2API 冷却该账号并切换到同组其他账号。 - `profiles.entries`: 从 master `~/.codex/` 选择上游 profile 并映射到 Sub2API account。 +- `profiles.entries[].capacity`: 可选 per-account concurrency override;不写则使用 `pool.defaultAccountCapacity`。不要因为某 provider 当前看起来更可用就给 HY 或任一账号写容量钉死;容量 override 必须来自明确 YAML 决策和新鲜 runtime 证据。 +- `profiles.entries[].tempUnschedulable`: 可选 per-account 临时下线规则覆盖;字段语义以 `docs/reference/platform-infra.md` 为权威。 - `profiles.entries[].openaiResponsesWebSocketsV2Mode`: 需要 Responses WebSocket v2 的上游才设置,值为 `off`、`ctx_pool` 或 `passthrough`。 - `profiles.entries[].upstreamUserAgent`: 少数要求 Codex CLI User-Agent 的上游才设置,不能含换行。 `sync --confirm` 会登录 Sub2API admin、创建/更新 group、创建/更新 YAML 中的 `unidesk-codex-*` accounts、创建/复用统一 API key Secret,并删除 YAML 中已移除且 `extra.unidesk_managed=true` 的 `unidesk-codex-*` account。 +不要给 UniDesk-managed Codex accounts 开 Sub2API `pool_mode`。UniDesk 期望的 failover 是把失败账号临时标记为 unschedulable,让同组其他账号接手;`pool_mode` 会重试同一个 account path。 + +WebSocket v2 是账号能力集合,不是调度 pin。`openaiResponsesWebSocketsV2Mode` 只声明该账号可承担 Codex Responses WSv2 链路;`codex-pool validate` 至少要看到一个 `webSocketsV2.schedulableEnabled` 账号,真实可用性仍以 Codex smoke 和运行日志为准。 + ## 添加上游 1. 在 master `~/.codex/` 准备 profile 文件,例如 `config.toml.` 和 `auth.json.`。 2. 在 `config/platform-infra/sub2api-codex-pool.yaml` 添加 `profiles.entries` 项,指定 `profile`、`accountName`、`configFile`、`authFile`。 -3. 如需要,给该项加 `priority`、`openaiResponsesWebSocketsV2Mode` 或 `upstreamUserAgent`。 +3. 如需要,给该项加 `priority`、`capacity`、`tempUnschedulable`、`openaiResponsesWebSocketsV2Mode` 或 `upstreamUserAgent`;不要用 `capacity` 把 HY 或任一账号变成硬编码 fallback。 4. 跑 `codex-pool plan`,确认 profile 可读、`base_url` 和 API key 来源有效,且 stdout 未泄露完整 key。 5. 跑 `codex-pool sync --confirm`。 6. 跑 `codex-pool validate`。 @@ -107,6 +114,7 @@ bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm - 从 `platform-infra/.` 读取统一 API key。 - 把当前 `~/.codex/config.toml` 和 `~/.codex/auth.json` 备份为 `.`,默认 `.pre-sub2api`。 - 重写默认 `~/.codex` 消费端,指向 `publicExposure.masterBaseUrl`,provider 名称和 wire API 来自 `localCodex`。 +- 写入 Codex Responses WebSocket v2 能力标记:provider section 必须有 `supports_websockets = true`,`[features]` 必须有 `responses_websockets_v2 = true`,否则 auto compact 后续链路会退回 HTTP-only summary context。 - 用统一 key 做一次 gateway 验证。 防递归规则:`profiles.entries` 中 default 上游应指向 `config.toml.pre-sub2api` / `auth.json.pre-sub2api`,不要把已经改成 Sub2API consumer 的默认文件再导回上游池。 @@ -117,7 +125,7 @@ bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm - `sub2api status`:Deployment/StatefulSet/Service/Secret 可见,运行镜像与 YAML 一致。 - `sub2api validate`:app、PostgreSQL、Redis 和 service proxy 基础检查通过。 -- `codex-pool validate`:统一 key 的 `GET /v1/models` 成功,owner balance 已满足 YAML 最小值。 +- `codex-pool validate`:统一 key 的 `GET /v1/models` 成功,owner balance 已满足 YAML 最小值,capacity、WebSocket v2 和 temporary-unschedulable 运行时状态与 YAML 对齐。 - 若 `publicExposure.enabled=true`,确认 FRP path 可用;`expose --confirm` 会用未带 key 的 public `/v1/models` 401 作为网关可达性探针。 如果要证明真实模型请求可用,使用最小 `/v1/responses` 或等价 Codex smoke。不要把 group-level `/v1/models` 成功解释成每个上游 account 都健康。 @@ -128,8 +136,11 @@ bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm - pool key 401:跑 `codex-pool sync --confirm` 重建 Sub2API key 与 k3s Secret 绑定,再跑 `codex-pool validate`。 - FRP 不通:先看 `codex-pool expose --confirm` 输出的 `masterFrps`、`sub2api-frpc` 和 public 401 probe;需要低层证据时只用 `trans G14:k3s` 做 bounded 查询。 - default profile 递归:检查 YAML default entry 是否使用 `*.pre-sub2api` 备份文件;必要时恢复备份后重新 `configure-local --confirm`。 -- 上游需要 WebSocket v2:只给该 profile 配 `openaiResponsesWebSocketsV2Mode: ctx_pool|passthrough`,跑 `sync --confirm`。 +- 上游需要 WebSocket v2:只给该 profile 配 `openaiResponsesWebSocketsV2Mode: ctx_pool|passthrough`,跑 `sync --confirm`;把它当 capability candidate,不要同时把它写成唯一/最高容量调度目标。 - 上游要求 Codex User-Agent:只给该 profile 配 `upstreamUserAgent`,跑 `sync --confirm`。 +- 上游报 capacity/rate-limit/overload 后没有切号:先确认 `codex-pool validate` 里 `tempUnschedulable.ok=true` 且目标 account `runtimeEnabled=true`、规则数符合 YAML;若 mismatch,跑 `codex-pool sync --confirm`,不要手工 patch Sub2API credentials。 +- Codex auto compact 后丢上下文:先确认本机 `~/.codex/config.toml` 是否有 `supports_websockets = true` 和 `responses_websockets_v2 = true`,再看 `codex-pool validate` 的 WSv2 candidate 和 Sub2API 日志里的 `transport=responses_websockets_v2`。 +- Codex smoke 有 reconnect/1013:这是上游并发/可用性问题,和 HTTP-only compact context-loss 分开处理;记录 session/log 证据并关联专项 issue,不要用 HY 容量钉死掩盖。 ## 禁止事项 diff --git a/AGENTS.md b/AGENTS.md index 9bcf0c52..3a83180f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 ## P0 最高优先级:G14 platform-infra 规则 - P0: `platform-infra` 是 G14 k3s 上 UniDesk 运维的平台基础设施 namespace;Sub2API、Codex pool、FRP 暴露、统一消费 API key 和后续平台基础设施迁移的长期边界、路由与探针口径统一见 `docs/reference/platform-infra.md`,Sub2API 日常操作统一见 `$unidesk-sub2api`(`.agents/skills/unidesk-sub2api/SKILL.md`)。 -- P0: Codex pool 账号容量必须从 `config/platform-infra/sub2api-codex-pool.yaml` 进入受控 CLI;HY 当前显式 `capacity: 20`,不得用代码常量、Secret、运行时手补或旧测试断言覆盖。 +- P0: Codex pool 账号容量与调度候选必须从 `config/platform-infra/sub2api-codex-pool.yaml` 进入受控 CLI;禁止把 HY 或任一 provider 硬编码为“最可用”对象、容量钉死对象或旧测试断言。 - P0: `devops-infra` 仅作为既有控制面基础设施逐步迁移来源,不再作为新增平台服务的默认 namespace;新增/迁移必须优先落到 `platform-infra`,并通过 `config/platform-infra/*.yaml` 与 `bun scripts/cli.ts platform-infra ...` 受控。 ## P0 最高优先级:CaseRun 无服务与单步调试规则 diff --git a/config/platform-infra/sub2api-codex-pool.yaml b/config/platform-infra/sub2api-codex-pool.yaml index c360a490..bdbc1dac 100644 --- a/config/platform-infra/sub2api-codex-pool.yaml +++ b/config/platform-infra/sub2api-codex-pool.yaml @@ -51,7 +51,6 @@ profiles: configFile: config.toml.HY authFile: auth.json.HY openaiResponsesWebSocketsV2Mode: passthrough - capacity: 20 priority: 1 - profile: gptclub accountName: unidesk-codex-gptclub diff --git a/docs/reference/hwlab.md b/docs/reference/hwlab.md index 1dd85341..d924cf72 100644 --- a/docs/reference/hwlab.md +++ b/docs/reference/hwlab.md @@ -107,6 +107,8 @@ HWLAB 与 AgentRun 协同修复必须按 `docs/reference/agentrun.md` 的职责 HWLAB v0.2 provider profile 必须通过已部署的 HWLAB Cloud API/CLI 管理,正式入口是 `hwlab-cli client provider-profiles`。不要把手工 patch `agentrun-v01` Secret、直接调用 AgentRun manager 或临时 runner job 当作正式配置路径或关闭证据;这些只可作为基础设施定位证据。标准执行面是 `G14:/root/hwlab-v02`,调用前锁定 `HWLAB_RUNTIME_NAMESPACE=hwlab-v02`、`HWLAB_RUNTIME_WEB_URL=http://74.48.78.17:19666` 和 `HWLAB_RUNTIME_ENDPOINT_LOCKED=1`,`HWLAB_API_KEY` 从 `/root/.config/hwlab-v02/master-server-admin-api-key.env` 加载。 +当明确需要把当前 operator 的 Codex 凭据作为 AgentRun 动态 profile 提供给 HWLAB Code Agent 或 CaseRun 使用时,固定操作手册在 `$hwlab-code-agent` 的 `AgentRun 动态 sub2api profile` 小节。该场景是 AgentRun runtime profile 投影,不替代上面的 HWLAB provider-profiles 正式配置路径;默认 profile 名为 `sub2api`、模型为 `gpt-5.5`,优先使用 G14 k3s 内 Sub2API ClusterIP 作为 `base_url`,并只记录 SecretRef、keyPresence、hash suffix 和原入口验收结果。 + `config.toml` 和 Codex `auth.json` 是两个独立输入。`set-config --config-stdin` 写入 profile 的 Codex 配置文本;`set-auth-json --auth-json-stdin` 写入完整 Codex auth JSON object,并在 AgentRun runtime Secret 中作为 `auth.json` key 保留。`set-key --key-stdin` 只用于单值 API key profile;当输入本来就是 Codex `auth.json` 时,不得再把它压扁、拆成单个 key、改走 legacy API-key-only 路径或保留旧断言阻塞提交。遇到 CLI 不支持完整 auth JSON、输出不可见或缺少 hash/keyPresence 等证据时,先补 HWLAB CLI/API 和 AgentRun provider-profile 能力,再继续试机。 标准命令形态如下,文件名可按实际 profile 替换: diff --git a/docs/reference/platform-infra.md b/docs/reference/platform-infra.md index 2655df0d..1e96a3a0 100644 --- a/docs/reference/platform-infra.md +++ b/docs/reference/platform-infra.md @@ -27,7 +27,7 @@ - `pool.apiKeySecretName` and `pool.apiKeySecretKey` name the k3s Secret that stores the single consumer API key. - `pool.defaultTempUnschedulable` declares Sub2API account-level temporary unschedulable rules. Keep 429/overload/capacity failures in this YAML policy so the scheduler can cool down a failing account and choose another candidate instead of hard-pinning one provider. - `profiles.entries` selects local Codex profile files from `~/.codex/` and maps them to Sub2API account names. -- `profiles.entries[].capacity` optionally overrides `pool.defaultAccountCapacity` for one account. Capacity is a YAML-controlled routing input; HY is intentionally declared as `capacity: 20` in this file, and code constants, Secrets, ad-hoc runtime patches, or stale tests must not override that source of truth. +- `profiles.entries[].capacity` optionally overrides `pool.defaultAccountCapacity` for one account. Capacity is a YAML-controlled routing input; omit it unless that account needs a deliberate per-account concurrency override. Code constants, Secrets, ad-hoc runtime patches, or stale tests must not override YAML source of truth. - `profiles.entries[].tempUnschedulable` may override the pool default for one account. The CLI renders it into Sub2API credentials as `temp_unschedulable_enabled` and `temp_unschedulable_rules`; rules match HTTP status plus response-body keywords and place only that account into a temporary unschedulable cooldown. - `profiles.entries[].openaiResponsesWebSocketsV2Mode` is the account-level Responses WebSocket v2 switch for OpenAI-compatible upstreams that require WebSocket transport. Allowed values are `off`, `ctx_pool`, and `passthrough`; omit the field unless that upstream needs it. - `profiles.entries[].upstreamUserAgent` is an optional account-level upstream request User-Agent override. Use it only for upstreams that require a Codex CLI compatible User-Agent; keep the value YAML-controlled and newline-free. @@ -36,6 +36,8 @@ Enable account-level WebSocket v2 only for upstream profiles that have passed a direct Codex WSv2 probe. Treat this as a YAML-declared capability set, not a hard scheduling pin to one profile; `codex-pool validate` must show at least one current `webSocketsV2.schedulableEnabled` account, and runtime smoke remains the availability proof. The same validation reports each managed account's runtime WebSocket v2 mode and whether it matches YAML, so stale `ctx_pool` settings cannot silently keep routing Codex WS sessions to an upstream that closes with `no available account`. +Do not encode current availability assumptions as account capacity or tests. HY is a WSv2-capable candidate in the current pool, not a privileged fallback target; if an account needs higher concurrency, that must be a deliberate YAML override backed by fresh runtime evidence and a test that still preserves multi-account scheduling. + Do not enable Sub2API `pool_mode` for UniDesk-managed Codex accounts. `pool_mode` retries the same selected account path, while UniDesk's desired failover behavior is to mark the failing account temporarily unschedulable and let Sub2API choose another account from the group. `codex-pool validate` reports each managed account's temporary-unschedulable runtime alignment and should be used after `codex-pool sync --confirm`. The request path is: diff --git a/scripts/platform-infra-sub2api-codex-routing-contract-test.ts b/scripts/platform-infra-sub2api-codex-routing-contract-test.ts index 5a1672ba..4947b1de 100644 --- a/scripts/platform-infra-sub2api-codex-routing-contract-test.ts +++ b/scripts/platform-infra-sub2api-codex-routing-contract-test.ts @@ -24,8 +24,8 @@ const rules = parsed.pool?.defaultTempUnschedulable?.rules ?? []; const overload429 = rules.find((rule) => rule.statusCode === 429); assertCondition(parsed.pool?.defaultAccountCapacity === 5, "Codex pool default capacity should remain explicit YAML", parsed.pool); -assertCondition(hy !== undefined, "HY profile should remain declared as a YAML-managed pool candidate", entries); -assertCondition(hy?.capacity === 20, "HY capacity must remain explicitly YAML-controlled at 20", hy); +assertCondition(hy !== undefined, "HY profile should remain declared as a candidate, not a special scheduler target", entries); +assertCondition(hy?.capacity === undefined, "HY must not carry a hard-coded capacity override", hy); assertCondition(wsEnabled.length >= 2, "Responses WSv2 should be a capability set with multiple candidates", wsEnabled); assertCondition(parsed.pool?.defaultTempUnschedulable?.enabled === true, "temporary unschedulable policy must be enabled from YAML", parsed.pool?.defaultTempUnschedulable); assertCondition(overload429 !== undefined, "temporary unschedulable policy must include 429 handling", rules); @@ -35,7 +35,7 @@ assertCondition((overload429?.durationMinutes ?? 0) > 0, "429 handling must cool console.log(JSON.stringify({ ok: true, checks: [ - "HY capacity is explicitly YAML-controlled at 20", + "HY is not capacity-pinned", "WSv2 routing is represented as a multi-account capability set", "temporary unschedulable rules are YAML-controlled", ],