diff --git a/.agents/skills/unidesk-sub2api/SKILL.md b/.agents/skills/unidesk-sub2api/SKILL.md index 559f6da0..8f9b8e40 100644 --- a/.agents/skills/unidesk-sub2api/SKILL.md +++ b/.agents/skills/unidesk-sub2api/SKILL.md @@ -16,6 +16,7 @@ UniDesk 在 G14 k3s `platform-infra` namespace 运维 Sub2API。日常操作统 - 本 skill 目录下若存在 `agents/*.yaml`,只作为 skill/agent 展示与调用元数据,不是 Sub2API 或 Codex pool 运行配置;不要在 skill 目录维护第二份账号、capacity、priority、endpoint 或 Secret 配置。 - Runtime 在 `G14:k3s` 的 `platform-infra` namespace;master server 只是控制端和消费者,不部署 Sub2API/PostgreSQL/Redis。 - Secret、`~/.codex/config.toml*`、`~/.codex/auth.json*` 是运行时输入或本地状态,不提交。 +- 默认 `~/.codex/config.toml` 和 `~/.codex/auth.json` 只作为统一 Sub2API consumer 使用;`config.toml` 必须指向 `https://sub2api.74-48-78-17.nip.io/`,`auth.json` 必须使用统一 pool API key。新增上游账号不得覆盖这两个默认文件,只能新增 `config.toml.` / `auth.json.` 并在 YAML 里声明。 - 输出只能包含 Secret 路径、长度、preview/fingerprint;禁止打印完整 API key、admin password、JWT secret、TOTP key。 ## 部署与状态 @@ -76,7 +77,7 @@ Codex 启动时反复出现 WebSocket reconnect、HTTPS fallback、`websocket cl ## 添加上游 -1. 在 master `~/.codex/` 准备 profile 文件,例如 `config.toml.` 和 `auth.json.`。 +1. 在 master `~/.codex/` 准备带后缀的上游 profile 文件,例如 `config.toml.` 和 `auth.json.`;禁止覆盖默认 `config.toml` / `auth.json`。 2. 在 `config/platform-infra/sub2api-codex-pool.yaml` 添加 `profiles.entries` 项,指定 `profile`、`accountName`、`configFile`、`authFile`。 3. 如需要,给该项加 `priority`、`capacity`、`loadFactor`、`tempUnschedulable`、`openaiResponsesWebSocketsV2Mode` 或 `upstreamUserAgent`;capacity/loadFactor 的具体数值只写在 YAML。 4. 跑 `codex-pool plan`,确认 profile 可读、`base_url` 和 API key 来源有效,且 stdout 未泄露完整 key。 @@ -118,11 +119,11 @@ 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` 消费端,固定指向 `https://sub2api.74-48-78-17.nip.io/`,provider 名称和 wire API 来自 `localCodex`。 - 按 `localCodex` 写入 Codex transport 标记:`supports_websockets` 与 `[features] responses_websockets_v2` 必须同开同关。只有至少一个上游通过 direct Codex WSv2 probe 时才启用;否则保持 HTTP Responses,避免每次原入口先经历无效 WS reconnect。 - 用统一 key 做一次 gateway 验证。 -防递归规则:`profiles.entries` 中 default 上游应指向 `config.toml.pre-sub2api` / `auth.json.pre-sub2api`,不要把已经改成 Sub2API consumer 的默认文件再导回上游池。 +防递归规则:默认 `config.toml` / `auth.json` 是 Sub2API consumer,不得作为上游账号导回 pool;上游账号必须使用带后缀 profile 文件,并通过 `config/platform-infra/sub2api-codex-pool.yaml` 的 `profiles.entries` 增删。 ## 验收口径 diff --git a/config/platform-infra/sub2api-codex-pool.yaml b/config/platform-infra/sub2api-codex-pool.yaml index e101fbb5..96b3ddc5 100644 --- a/config/platform-infra/sub2api-codex-pool.yaml +++ b/config/platform-infra/sub2api-codex-pool.yaml @@ -5,6 +5,7 @@ pool: apiKeySecretKey: API_KEY minOwnerBalanceUsd: 1000 minOwnerConcurrency: 50 + defaultAccountPriority: 10 defaultAccountCapacity: 5 defaultAccountLoadFactor: 1 defaultTempUnschedulable: @@ -60,6 +61,18 @@ profiles: capacity: 10 loadFactor: 10 priority: 1 + - profile: thinkai + accountName: unidesk-codex-thinkai + configFile: config.toml.thinkai + authFile: auth.json.thinkai + - profile: dawclaudecode + accountName: unidesk-codex-dawclaudecode + configFile: config.toml.dawclaudecode + authFile: auth.json.dawclaudecode + - profile: ranmeng + accountName: unidesk-codex-ranmeng + configFile: config.toml.ranmeng + authFile: auth.json.ranmeng - profile: gptclub accountName: unidesk-codex-gptclub configFile: config.toml.gptclub diff --git a/docs/reference/platform-infra.md b/docs/reference/platform-infra.md index d5ee6531..7ca3d72c 100644 --- a/docs/reference/platform-infra.md +++ b/docs/reference/platform-infra.md @@ -28,6 +28,8 @@ - `pool.minOwnerConcurrency` declares the minimum concurrency for the Sub2API user that owns the unified consumer API key. Keep it high enough to cover the declared account capacity set, so the shared key does not fail WS sessions at the user-concurrency layer. Do not compensate for owner-concurrency 1013 errors by pinning capacity to one provider. - `pool.defaultTempUnschedulable` declares Sub2API account-level temporary unschedulable rules. Keep 429/overload/capacity, service-unavailable, gateway timeout, and stable model-routing 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. +- The unsuffixed master `~/.codex/config.toml` and `~/.codex/auth.json` are reserved for the unified Sub2API consumer. `config.toml` must keep `base_url = "https://sub2api.74-48-78-17.nip.io/"`, and `auth.json` must contain the unified pool API key from `pool.apiKeySecretName` / `pool.apiKeySecretKey`. Do not replace these two files with direct upstream account credentials. +- Additional upstream accounts must use suffixed local profile files such as `config.toml.` and `auth.json.`, then be declared through `profiles.entries` in `config/platform-infra/sub2api-codex-pool.yaml`. - `profiles.entries[].capacity` optionally overrides `pool.defaultAccountCapacity` for one account. Capacity is a YAML-controlled routing input; concrete current values belong only in `config/platform-infra/sub2api-codex-pool.yaml` and runtime validation output, not in long-term reference prose. Code constants, Secrets, ad-hoc runtime patches, or stale tests must not override YAML source of truth. - `profiles.entries[].loadFactor` optionally overrides `pool.defaultAccountLoadFactor` for one account and is rendered to Sub2API `load_factor`. Treat it as routing policy: values belong in YAML and `codex-pool validate` output, not code constants, Secrets, or ad-hoc runtime patches. - Do not change account membership, priority, capacity, load factor, WebSocket mode, or other routing policy from inference alone. Unless the user explicitly asks for a configuration change, first preserve the current YAML, collect provenance and runtime evidence, and write the finding to the relevant issue or runbook before proposing a change. @@ -51,7 +53,7 @@ Sub2API temporary-unschedulable rules require both an HTTP status match and a re The request path is: -1. A client sends an OpenAI-compatible request to the configured consumer base URL, normally master-local `http://127.0.0.1:/v1/...`, with the unified API key. +1. A client sends an OpenAI-compatible request to the configured consumer base URL, normally `https://sub2api.74-48-78-17.nip.io/v1/...`, with the unified API key. 2. master `frps` forwards the TCP connection to `platform-infra/sub2api-frpc` when `publicExposure.enabled` is true. 3. `sub2api-frpc` forwards to `sub2api.platform-infra.svc.cluster.local:8080`. 4. Sub2API validates the unified key and resolves its `group_id`. @@ -59,7 +61,7 @@ The request path is: Adding, removing, exposing, validating, and configuring local Codex consumers are daily operations covered by `$unidesk-sub2api`. The development rule is that ordinary pool membership changes stay YAML-only and do not add code or CI/CD. Code changes are only appropriate when the YAML schema needs a new reusable capability such as account-level WebSocket mode or per-account upstream User-Agent. -After `codex-pool configure-local --confirm`, the default upstream profile must not recursively import the just-created Sub2API consumer endpoint as an upstream account. Keep the default source profile pointed at `config.toml.` and `auth.json.`; fallback to the current default files is only for first bootstrap before backups exist. +After `codex-pool configure-local --confirm`, the default `~/.codex/config.toml` / `auth.json` pair must remain the unified Sub2API consumer and must not be reused as an upstream account profile. Keep every upstream source profile in suffixed files such as `config.toml.` / `auth.json.` and register it through YAML `profiles.entries`. ## Public FRP Boundary diff --git a/scripts/platform-infra-sub2api-codex-local-config-contract-test.ts b/scripts/platform-infra-sub2api-codex-local-config-contract-test.ts index 37bc4089..5bfa0af4 100644 --- a/scripts/platform-infra-sub2api-codex-local-config-contract-test.ts +++ b/scripts/platform-infra-sub2api-codex-local-config-contract-test.ts @@ -6,7 +6,7 @@ function assertCondition(condition: unknown, message: string, detail: unknown = const baseOptions = { providerName: "OpenAI", - baseUrl: "http://127.0.0.1:21880", + baseUrl: "https://sub2api.74-48-78-17.nip.io/", wireApi: "responses", supportsWebSockets: true, responsesWebSocketsV2: true, @@ -33,7 +33,7 @@ const existing = [ const updated = renderCodexLocalConsumerToml(existing, baseOptions); assertCondition(updated.includes('model_provider = "OpenAI"'), "model_provider must point at the configured provider", updated); -assertCondition(updated.includes('base_url = "http://127.0.0.1:21880"'), "provider base_url must use the Sub2API consumer endpoint", updated); +assertCondition(updated.includes('base_url = "https://sub2api.74-48-78-17.nip.io/"'), "provider base_url must use the fixed public Sub2API consumer endpoint", updated); assertCondition(updated.includes("requires_openai_auth = true"), "provider must require OpenAI auth", updated); assertCondition(updated.includes("supports_websockets = true"), "provider must enable WebSocket transport", updated); assertCondition(updated.includes("responses_websockets_v2 = true"), "features must enable Responses WebSocket v2", updated); diff --git a/scripts/platform-infra-sub2api-codex-routing-contract-test.ts b/scripts/platform-infra-sub2api-codex-routing-contract-test.ts index 2ee62bbf..0ec01c62 100644 --- a/scripts/platform-infra-sub2api-codex-routing-contract-test.ts +++ b/scripts/platform-infra-sub2api-codex-routing-contract-test.ts @@ -8,6 +8,7 @@ function assertCondition(condition: unknown, message: string, detail: unknown = const configPath = rootPath("config", "platform-infra", "sub2api-codex-pool.yaml"); const parsed = Bun.YAML.parse(readFileSync(configPath, "utf8")) as { pool?: { + defaultAccountPriority?: number; defaultAccountCapacity?: number; defaultAccountLoadFactor?: number; minOwnerConcurrency?: number; @@ -22,6 +23,7 @@ const parsed = Bun.YAML.parse(readFileSync(configPath, "utf8")) as { const entries = parsed.profiles?.entries ?? []; const rules = parsed.pool?.defaultTempUnschedulable?.rules ?? []; +const defaultPriority = parsed.pool?.defaultAccountPriority ?? 0; const defaultCapacity = parsed.pool?.defaultAccountCapacity ?? 0; const defaultLoadFactor = parsed.pool?.defaultAccountLoadFactor ?? 0; const desiredCapacity = entries.reduce((total, entry) => total + (entry.capacity ?? defaultCapacity), 0); @@ -30,6 +32,7 @@ const wsEnabledEntries = entries.filter((entry) => entry.openaiResponsesWebSocke const localWsEnabled = parsed.localCodex?.supportsWebSockets === true || parsed.localCodex?.responsesWebSocketsV2 === true; assertCondition(entries.length > 0, "Codex pool must declare YAML-managed profile entries", parsed.profiles); +assertCondition(Number.isInteger(defaultPriority) && defaultPriority >= 0, "defaultAccountPriority must be a non-negative integer", parsed.pool); assertCondition(Number.isInteger(defaultCapacity) && defaultCapacity > 0, "defaultAccountCapacity must be a positive integer", parsed.pool); assertCondition(Number.isInteger(defaultLoadFactor) && defaultLoadFactor > 0, "defaultAccountLoadFactor must be a positive integer", parsed.pool); assertCondition(entries.every((entry) => typeof entry.profile === "string" && entry.profile.length > 0), "profile entries must declare profile names", entries); diff --git a/scripts/src/platform-infra-sub2api-codex.ts b/scripts/src/platform-infra-sub2api-codex.ts index 43bbe73c..f3a45993 100644 --- a/scripts/src/platform-infra-sub2api-codex.ts +++ b/scripts/src/platform-infra-sub2api-codex.ts @@ -18,6 +18,7 @@ const defaultPoolApiKeyName = "unidesk-codex-pool-api-key"; const defaultPoolApiKeySecretName = "sub2api-codex-pool-api-key"; const defaultPoolApiKeySecretKey = "API_KEY"; const defaultMinOwnerBalanceUsd = 1000; +const defaultAccountPriority = 10; interface DisclosureOptions { full: boolean; @@ -76,6 +77,7 @@ interface CodexPoolConfig { apiKeySecretKey: string; minOwnerBalanceUsd: number; minOwnerConcurrency: number; + defaultAccountPriority: number; defaultAccountCapacity: number; defaultAccountLoadFactor: number; defaultTempUnschedulable: CodexTempUnschedulablePolicy; @@ -234,7 +236,7 @@ function codexPoolPlan(): Record { grouping: `All discovered Codex profiles are bound to one Sub2API group named ${pool.groupName}.`, unifiedApiKey: `The client-facing API_KEY is controlled by k3s Secret ${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}.`, publicExposure: pool.publicExposure.enabled - ? `Master server consumers use ${pool.publicExposure.masterBaseUrl}; FRP proxy ${pool.publicExposure.proxyName} maps public ${pool.publicExposure.publicBaseUrl} to ${pool.publicExposure.localIP}:${pool.publicExposure.localPort}.` + ? `Default Codex consumers use ${codexConsumerBaseUrl(pool)}; bounded master-local probes may use ${pool.publicExposure.masterBaseUrl}. FRP proxy ${pool.publicExposure.proxyName} maps public ${pool.publicExposure.publicBaseUrl} to ${pool.publicExposure.localIP}:${pool.publicExposure.localPort}.` : "Public FRP exposure is disabled by YAML.", idempotency: "sync reuses the group, account names, and k3s Secret when they already exist; credentials are updated from the current local Codex files; UniDesk-managed accounts removed from YAML are deleted from Sub2API.", configPolicy: "UniDesk-owned durable configuration remains YAML-first; local ~/.codex files and runtime Secrets are not committed.", @@ -268,6 +270,7 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi apiKeySecretKey: pool.apiKeySecretKey, minOwnerBalanceUsd: pool.minOwnerBalanceUsd, minOwnerConcurrency: pool.minOwnerConcurrency, + defaultAccountPriority: pool.defaultAccountPriority, defaultAccountCapacity: pool.defaultAccountCapacity, defaultAccountLoadFactor: pool.defaultAccountLoadFactor, }, @@ -409,7 +412,7 @@ async function codexPoolConfigureLocal(config: UniDeskConfig, options: ConfirmOp authPath, backupConfigPath, backupAuthPath, - baseUrl: pool.publicExposure.masterBaseUrl, + baseUrl: codexConsumerBaseUrl(pool), providerName: pool.localCodex.providerName, wireApi: pool.localCodex.wireApi, supportsWebSockets: pool.localCodex.supportsWebSockets, @@ -454,7 +457,9 @@ function collectCodexProfiles(): CodexProfile[] { const pool = readCodexPoolConfig(); if (!existsSync(codexDir)) return []; const seenAccountNames = new Set(); - const configs = pool.profiles.length > 0 ? pool.profiles : discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable); + const configs = pool.profiles.length > 0 + ? pool.profiles + : discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultAccountPriority); return configs.map((entry) => { const resolved = resolveProfileFiles(codexDir, entry); const profile = entry.profile; @@ -526,7 +531,11 @@ function collectCodexProfiles(): CodexProfile[] { }); } -function discoverCodexProfileConfigs(codexDir: string, defaultTempUnschedulable = defaultCodexTempUnschedulablePolicy()): CodexPoolProfileConfig[] { +function discoverCodexProfileConfigs( + codexDir: string, + defaultTempUnschedulable = defaultCodexTempUnschedulablePolicy(), + defaultPriority = defaultAccountPriority, +): CodexPoolProfileConfig[] { return readdirSync(codexDir) .filter((file) => file === "config.toml" || file.startsWith("config.toml.")) .sort((a, b) => { @@ -546,7 +555,7 @@ function discoverCodexProfileConfigs(codexDir: string, defaultTempUnschedulable fallbackAuthFile: null, openaiResponsesWebSocketsV2Mode: null, upstreamUserAgent: null, - priority: 1, + priority: defaultPriority, capacity: null, loadFactor: null, tempUnschedulable: defaultTempUnschedulable, @@ -579,10 +588,16 @@ function readCodexPoolConfig(): CodexPoolConfig { apiKeySecretKey: stringValue(pool.apiKeySecretKey) ?? defaults.apiKeySecretKey, minOwnerBalanceUsd: numberValue(pool.minOwnerBalanceUsd) ?? defaults.minOwnerBalanceUsd, minOwnerConcurrency: readAccountCapacity(pool.minOwnerConcurrency, "pool.minOwnerConcurrency"), + defaultAccountPriority: readAccountPriority(pool.defaultAccountPriority, "pool.defaultAccountPriority"), defaultAccountCapacity: readAccountCapacity(pool.defaultAccountCapacity, "pool.defaultAccountCapacity"), defaultAccountLoadFactor: readAccountLoadFactor(pool.defaultAccountLoadFactor, "pool.defaultAccountLoadFactor"), defaultTempUnschedulable, - profiles: readProfileConfig(parsed.profiles, defaults.profiles, defaultTempUnschedulable), + profiles: readProfileConfig( + parsed.profiles, + defaults.profiles, + defaultTempUnschedulable, + readAccountPriority(pool.defaultAccountPriority, "pool.defaultAccountPriority"), + ), publicExposure: readPublicExposureConfig(parsed.publicExposure, defaults.publicExposure), localCodex: readLocalCodexConfig(parsed.localCodex, defaults.localCodex), }; @@ -608,6 +623,7 @@ function defaultCodexPoolConfig(): CodexPoolConfig { minOwnerBalanceUsd: defaultMinOwnerBalanceUsd, // Only used for the no-YAML fallback path; real pool configs must declare pool.minOwnerConcurrency. minOwnerConcurrency: 1, + defaultAccountPriority, defaultAccountCapacity: 5, defaultAccountLoadFactor: 1, defaultTempUnschedulable: defaultCodexTempUnschedulablePolicy(), @@ -716,7 +732,12 @@ export function defaultCodexTempUnschedulablePolicy(): CodexTempUnschedulablePol }; } -function readProfileConfig(value: unknown, defaults: CodexPoolProfileConfig[], defaultTempUnschedulable: CodexTempUnschedulablePolicy): CodexPoolProfileConfig[] { +function readProfileConfig( + value: unknown, + defaults: CodexPoolProfileConfig[], + defaultTempUnschedulable: CodexTempUnschedulablePolicy, + defaultPriority: number, +): CodexPoolProfileConfig[] { if (!isRecord(value)) return defaults; const entries = value.entries; if (!Array.isArray(entries)) throw new Error(`${codexPoolConfigPath}.profiles.entries must be a YAML array`); @@ -738,7 +759,7 @@ function readProfileConfig(value: unknown, defaults: CodexPoolProfileConfig[], d if (fallbackAuthFile !== null) validateCodexFileName(fallbackAuthFile, `profiles.entries[${index}].fallbackAuthFile`); const openaiResponsesWebSocketsV2Mode = readOpenAIResponsesWebSocketsV2Mode(entry.openaiResponsesWebSocketsV2Mode, `profiles.entries[${index}].openaiResponsesWebSocketsV2Mode`); const upstreamUserAgent = readUpstreamUserAgent(entry.upstreamUserAgent, `profiles.entries[${index}].upstreamUserAgent`); - const priority = readAccountPriority(entry.priority, `profiles.entries[${index}].priority`); + const priority = readAccountPriority(entry.priority, `profiles.entries[${index}].priority`, defaultPriority); const capacity = entry.capacity === undefined || entry.capacity === null ? null : readAccountCapacity(entry.capacity, `profiles.entries[${index}].capacity`); const loadFactor = entry.loadFactor === undefined || entry.loadFactor === null ? null : readAccountLoadFactor(entry.loadFactor, `profiles.entries[${index}].loadFactor`); const tempUnschedulable = readTempUnschedulablePolicy(entry.tempUnschedulable, `profiles.entries[${index}].tempUnschedulable`, defaultTempUnschedulable); @@ -776,8 +797,8 @@ function readUpstreamUserAgent(value: unknown, key: string): string | null { return text; } -function readAccountPriority(value: unknown, key: string): number { - if (value === undefined || value === null) return 1; +function readAccountPriority(value: unknown, key: string, fallback = defaultAccountPriority): number { + if (value === undefined || value === null) return fallback; const priority = numberValue(value); if (priority === null || !Number.isInteger(priority) || priority < 0 || priority > 1000) { throw new Error(`${codexPoolConfigPath}.${key} must be an integer from 0 to 1000`); @@ -1043,6 +1064,7 @@ function poolTarget(pool = readCodexPoolConfig()): Record { apiKeyName: pool.apiKeyName, apiKeySecret: `${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`, minOwnerConcurrency: pool.minOwnerConcurrency, + defaultAccountPriority: pool.defaultAccountPriority, defaultAccountCapacity: pool.defaultAccountCapacity, defaultAccountLoadFactor: pool.defaultAccountLoadFactor, valuesPrinted: false, @@ -1463,7 +1485,7 @@ function writeLocalCodexConfig(pool: CodexPoolConfig, apiKey: string): Record> { - const probe = await probePublicModels(pool, "with-api-key", apiKey); + const probe = await probePublicModels(pool, "with-api-key", apiKey, "public"); return { ...probe, ok: probe.ok === true, @@ -1697,7 +1723,9 @@ function validateScript(pool: CodexPoolConfig): string { function desiredAccountCapacityMap(pool: CodexPoolConfig): Record { const codexDir = join(homedir(), ".codex"); const seenAccountNames = new Set(); - const configs = pool.profiles.length > 0 ? pool.profiles : discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable); + const configs = pool.profiles.length > 0 + ? pool.profiles + : discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultAccountPriority); const capacities: Record = {}; for (const entry of configs) { const accountName = entry.accountName ?? uniqueAccountName(entry.profile, seenAccountNames); @@ -1710,7 +1738,9 @@ function desiredAccountCapacityMap(pool: CodexPoolConfig): Record { const codexDir = join(homedir(), ".codex"); const seenAccountNames = new Set(); - const configs = pool.profiles.length > 0 ? pool.profiles : discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable); + const configs = pool.profiles.length > 0 + ? pool.profiles + : discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultAccountPriority); const loadFactors: Record = {}; for (const entry of configs) { const accountName = entry.accountName ?? uniqueAccountName(entry.profile, seenAccountNames); @@ -1723,7 +1753,9 @@ function desiredAccountLoadFactorMap(pool: CodexPoolConfig): Record { const codexDir = join(homedir(), ".codex"); const seenAccountNames = new Set(); - const configs = pool.profiles.length > 0 ? pool.profiles : discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable); + const configs = pool.profiles.length > 0 + ? pool.profiles + : discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultAccountPriority); const modes: Record = {}; for (const entry of configs) { const accountName = entry.accountName ?? uniqueAccountName(entry.profile, seenAccountNames); @@ -1736,7 +1768,9 @@ function desiredAccountWebSocketsV2ModeMap(pool: CodexPoolConfig): Record { const codexDir = join(homedir(), ".codex"); const seenAccountNames = new Set(); - const configs = pool.profiles.length > 0 ? pool.profiles : discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable); + const configs = pool.profiles.length > 0 + ? pool.profiles + : discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultAccountPriority); const policies: Record = {}; for (const entry of configs) { const accountName = entry.accountName ?? uniqueAccountName(entry.profile, seenAccountNames); @@ -1770,6 +1804,7 @@ POOL_API_KEY_SECRET_NAME = "${pool.apiKeySecretName}" POOL_API_KEY_SECRET_KEY = "${pool.apiKeySecretKey}" MIN_OWNER_BALANCE_USD = ${JSON.stringify(pool.minOwnerBalanceUsd)} MIN_OWNER_CONCURRENCY = ${JSON.stringify(pool.minOwnerConcurrency)} +POOL_DEFAULT_ACCOUNT_PRIORITY = ${JSON.stringify(pool.defaultAccountPriority)} POOL_DEFAULT_ACCOUNT_CAPACITY = ${JSON.stringify(pool.defaultAccountCapacity)} POOL_DEFAULT_ACCOUNT_LOAD_FACTOR = ${JSON.stringify(pool.defaultAccountLoadFactor)} RESPONSES_SMOKE_MODEL = ${JSON.stringify(pool.localCodex.responsesSmokeModel)} @@ -2006,7 +2041,7 @@ def account_payload(profile, group_id): "credentials": credentials, "extra": extra, "concurrency": int(profile.get("capacity", 5) or 5), - "priority": int(profile.get("priority", 1) or 1), + "priority": int(profile.get("priority", POOL_DEFAULT_ACCOUNT_PRIORITY) or POOL_DEFAULT_ACCOUNT_PRIORITY), "rate_multiplier": 1, "load_factor": int(profile.get("loadFactor", POOL_DEFAULT_ACCOUNT_LOAD_FACTOR) or POOL_DEFAULT_ACCOUNT_LOAD_FACTOR), "group_ids": [group_id], @@ -2040,7 +2075,7 @@ def ensure_accounts(token, profiles, group_id): "apiKeySource": profile["apiKeySource"], "apiKeyFingerprint": profile["apiKeyFingerprint"], "openaiResponsesWebSocketsV2Mode": profile.get("openaiResponsesWebSocketsV2Mode"), - "priority": int(profile.get("priority", 1) or 1), + "priority": int(profile.get("priority", POOL_DEFAULT_ACCOUNT_PRIORITY) or POOL_DEFAULT_ACCOUNT_PRIORITY), "capacity": int(profile.get("capacity", 5) or 5), "loadFactor": int(profile.get("loadFactor", POOL_DEFAULT_ACCOUNT_LOAD_FACTOR) or POOL_DEFAULT_ACCOUNT_LOAD_FACTOR), "runtimeConcurrency": data.get("concurrency") if isinstance(data, dict) else None,