@sema-agent/server 7.122.0 → 7.123.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/MIGRATION.md +77 -0
  2. package/README.md +5 -0
  3. package/README.zh-CN.md +3 -0
  4. package/USAGE.md +23 -5
  5. package/dist/audit.d.ts +1 -1
  6. package/dist/audit.js +52 -22
  7. package/dist/auto-mode-face.d.ts +12 -8
  8. package/dist/auto-mode-face.js +8 -7
  9. package/dist/boot/config-center.js +3 -0
  10. package/dist/config-catalog.js +1 -1
  11. package/dist/config-center/apply-effective.d.ts +1 -1
  12. package/dist/config-center/apply-effective.js +5 -3
  13. package/dist/config-center/http-client.js +3 -3
  14. package/dist/config-center/read-warnings.d.ts +56 -35
  15. package/dist/config-center/read-warnings.js +67 -19
  16. package/dist/config-center/types.d.ts +7 -8
  17. package/dist/config-lkg.d.ts +12 -9
  18. package/dist/config-lkg.js +38 -39
  19. package/dist/config-provider.d.ts +3 -2
  20. package/dist/config-provider.js +2 -2
  21. package/dist/config-types.d.ts +9 -1
  22. package/dist/config.d.ts +8 -8
  23. package/dist/config.js +69 -92
  24. package/dist/fleet/engine-signal.d.ts +54 -23
  25. package/dist/fleet/engine-signal.js +29 -5
  26. package/dist/fleet-client.d.ts +8 -6
  27. package/dist/fleet-client.js +4 -3
  28. package/dist/http/dispatch.js +5 -14
  29. package/dist/http/health-self-report.d.ts +3 -0
  30. package/dist/http/routes/capabilities.js +2 -1
  31. package/dist/http/routes/sessions.js +1 -4
  32. package/dist/http/stored-body-replay.d.ts +4 -3
  33. package/dist/http/stored-body-replay.js +2 -3
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.js +1 -0
  36. package/dist/leader/leader.d.ts +7 -6
  37. package/dist/leader/leader.js +3 -1
  38. package/dist/leader/run-entry.d.ts +2 -2
  39. package/dist/leader/run-entry.js +3 -3
  40. package/dist/leader/wire.d.ts +2 -2
  41. package/dist/model-entry-refusal.d.ts +18 -9
  42. package/dist/model-entry-refusal.js +19 -14
  43. package/dist/model-route-endpoint.d.ts +21 -1
  44. package/dist/model-route-endpoint.js +9 -3
  45. package/dist/model-route-gap.d.ts +103 -0
  46. package/dist/model-route-gap.js +80 -0
  47. package/dist/namespaced-rule-name.d.ts +16 -30
  48. package/dist/namespaced-rule-name.js +2 -20
  49. package/dist/observability/fail-open.d.ts +0 -4
  50. package/dist/observability/fail-open.js +0 -4
  51. package/dist/plugins/one-time-migrations.js +27 -0
  52. package/dist/plugins/session-policy-store-sql.js +1 -2
  53. package/dist/plugins/workflow-run-store-sql.d.ts +4 -2
  54. package/dist/rule-entry-list.d.ts +3 -13
  55. package/dist/rule-entry-list.js +1 -12
  56. package/dist/run-local.js +4 -4
  57. package/dist/runtime-governance.d.ts +31 -8
  58. package/dist/runtime-governance.js +7 -2
  59. package/dist/session-policy-wire.d.ts +47 -49
  60. package/dist/session-policy-wire.js +41 -36
  61. package/dist/session-sync.d.ts +5 -1
  62. package/dist/session-sync.js +2 -1
  63. package/dist/spec-fields.d.ts +4 -4
  64. package/dist/spec-fields.js +1 -4
  65. package/dist/task-settings.d.ts +4 -5
  66. package/dist/task-settings.js +5 -8
  67. package/dist/unattended-permission-mode.d.ts +3 -3
  68. package/dist/unattended-permission-mode.js +2 -2
  69. package/package.json +3 -3
@@ -794,15 +794,12 @@ export async function handleSessions(req, res, match, ctx) {
794
794
  case "not-an-object":
795
795
  sendError(res, 400, "request.body_shape", describeSessionPermissionRulesProblem(read.problem));
796
796
  return;
797
- case "field-not-string-array":
797
+ case "field-not-rule-list":
798
798
  sendError(res, 400, "request.field_invalid", describeSessionPermissionRulesProblem(read.problem));
799
799
  return;
800
800
  case "legacy-tool-name":
801
801
  sendError(res, 400, "rules.legacy_tool_name", describeSessionPermissionRulesProblem(read.problem), { ...read.problem.refusal });
802
802
  return;
803
- case "unreachable-tool-name":
804
- sendError(res, 400, "request.field_invalid", describeSessionPermissionRulesProblem(read.problem));
805
- return;
806
803
  default: {
807
804
  const unhandled = read.problem;
808
805
  throw new Error(`unhandled SessionPermissionRules problem: ${JSON.stringify(unhandled)}`);
@@ -7,13 +7,14 @@
7
7
  * **入口地板**(`runtime-governance.ts` `autonomyModeFloor`)—— 这两只属主带着配置层的包外依赖;`checkpoint-ctx.ts` 被随包的本地排空脚本经编译产物 import,
8
8
  * 它的闭包只许 node 内建 / 引擎包根(`test/drain-check-local.test.ts` 的闭包门)。判据只有这一只,前两问的谓词照旧住在各自属主里。
9
9
  */
10
- import { type PermissionMode } from "@sema-agent/core";
10
+ import type { PermissionMode } from "@sema-agent/core";
11
11
  import { type Autonomy } from "../runtime-governance.js";
12
12
  import { type StoredBodyReplayRefusal } from "./checkpoint-ctx.js";
13
13
  import type { CheckpointCtx } from "./route-ctx.js";
14
14
  /**
15
- * 第三问:本行体里的模式词能否按当前读法重放。不能 ⇔ 本部署此刻有入口地板 ∧ 行上没有当前的模式词读法记号 ∧ 体的生效词被地板钳会变
16
- * (core `clampPermissionMode`,与提交入口的盖章同一只钳)⇒ 返回拒因的两个事实(体的词、地板);能 ⇒ `undefined`。
15
+ * 第三问:本行体里的模式词能否按当前读法重放。不能 ⇔ 本部署此刻有入口地板 ∧ 行上没有当前的模式词读法记号 ∧ 体的生效词进这块地板会变
16
+ * (`runtime-governance.ts` `entryPermissionMode`,与提交入口的盖章同一只函数;判的是「词变不变」而不是 `clamped` —— 体无词的旧行进地板取地板本身,
17
+ * 那也是变)⇒ 返回拒因的两个事实(体的词、地板);能 ⇒ `undefined`。
17
18
  */
18
19
  export declare function permissionModeReadingUnstated(ctx: CheckpointCtx, floor: PermissionMode | undefined): {
19
20
  stored: PermissionMode | undefined;
@@ -1,13 +1,12 @@
1
- import { clampPermissionMode } from "@sema-agent/core";
2
1
  import { storedBodyPermissionMode } from "../task-settings.js";
3
- import { autonomyModeFloor } from "../runtime-governance.js";
2
+ import { autonomyModeFloor, entryPermissionMode } from "../runtime-governance.js";
4
3
  import { skillDenyInertEntryOf } from "../rule-entry-list.js";
5
4
  import { allowListReadingEraOf, allowListReadingUnstated, permissionModeReadingStated } from "./checkpoint-ctx.js";
6
5
  export function permissionModeReadingUnstated(ctx, floor) {
7
6
  if (floor === undefined || permissionModeReadingStated(ctx))
8
7
  return undefined;
9
8
  const stored = storedBodyPermissionMode(ctx.body);
10
- return clampPermissionMode(stored, floor).effective === stored ? undefined : { stored, floor };
9
+ return entryPermissionMode(stored, floor).effective === stored ? undefined : { stored, floor };
11
10
  }
12
11
  export function storedBodyReplayRefusal(ctx, autonomy) {
13
12
  if (allowListReadingUnstated(ctx))
package/dist/index.d.ts CHANGED
@@ -29,5 +29,7 @@ export { createKeyResolver, resolveModelApiKey, type ModelKeyRef } from "./key-r
29
29
  export { isModelKeyPoison, ModelKeyPoisonedError, type ModelKeyPoison, type ModelKeyPoisonReason } from "./key-resolver.js";
30
30
  export { RETIRED_ENVS, type RetiredEnv, type RetiredEnvKind, type RetiredEnvWire } from "./config.js";
31
31
  export { CATALOG_ENTRY_HEADER_LANES, CATALOG_ENTRY_HEADER_NAMES, type CatalogEntryHeaderName } from "./model-entry-headers.js";
32
+ export { judgeModelRouteGap, type ModelRouteGapReading, type ModelRouteGapOptions } from "./model-route-gap.js";
33
+ export type { ModelRouteGap } from "./model-route-endpoint.js";
32
34
  export type { UserInputLedgerStore, UserInputAcceptance, NewUserInputAcceptance, OpenScanCursor, OpenScanPage, SettleHow, WriterGoneEvidence } from "./user-input-ledger.js";
33
35
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -26,4 +26,5 @@ export { createKeyResolver, resolveModelApiKey } from "./key-resolver.js";
26
26
  export { isModelKeyPoison, ModelKeyPoisonedError } from "./key-resolver.js";
27
27
  export { RETIRED_ENVS } from "./config.js";
28
28
  export { CATALOG_ENTRY_HEADER_LANES, CATALOG_ENTRY_HEADER_NAMES } from "./model-entry-headers.js";
29
+ export { judgeModelRouteGap } from "./model-route-gap.js";
29
30
  //# sourceMappingURL=index.js.map
@@ -66,8 +66,8 @@ export interface ProvisionedWorker {
66
66
  verifierTools?: ToolSpec[];
67
67
  }
68
68
  /**
69
- * 一条 leader run 的**入口体**(提交体去掉 objective 的那一半)—— 运维 `AUTONOMY` 的模式词地板在这条 run 出生的那一拍读一次、经受理门同一只盖章
70
- * 盖出来的结果(`run-entry.ts` `leaderRunEntry`);地板缺席 ⇒ `{}`。leader 请求不带调用方的模式词,所以入口词就是地板本身(与 run-local 同)。
69
+ * 一条 leader run 的**入口体**(提交体去掉 objective 的那一半)—— 运维 `AUTONOMY` 的模式词地板在这条 run 出生的那一拍读一次、经受理门同一只函数
70
+ * (`runtime-governance.ts` `entryPermissionMode`)得出的入口词(`run-entry.ts` `leaderRunEntry`);地板缺席 ⇒ `{}`。leader 请求不带调用方的模式词,所以入口词就是地板本身(与 run-local 同)。
71
71
  * 本 run 铸的每一条 spec 经 {@link withLeaderLeg} 从这一份取词;worker 的续跑体由它展开(`{ ...entry, objective }`)⇒ 首跑、续跑、修复腿同一个词。
72
72
  * 入口词是 `plan` 的 run 进不了这条车道(`run-entry.ts` `leaderEntryRefusal`:worker 会话没有计划审阅通道)—— 受理口与出生处同判,到不了 worker。
73
73
  * 热改 `AUTONOMY` 只影响之后起的 leader run:在飞的 run 保持它出生时盖下的这一份。
@@ -76,10 +76,11 @@ export interface LeaderRunEntry {
76
76
  readonly permissionMode?: PermissionMode;
77
77
  }
78
78
  /**
79
- * leader 车道铸 spec 的**唯一**合并口:入口体的词写进 spec,再交给无人值守腿的合并口(`withUnattendedLeg` 的 `default` 钳在入口词之下)。
80
- * 入口体无词 ⇒ spec 原物交给 `withUnattendedLeg`,与不经本口逐字同。本车道的 spec 自己不带模式词(子任务体只有 objective,其余并进的键是预算 / 审批 / 身份),
81
- * 所以这里是「写」而不是「钳」。五个出生点(worker 首跑的工厂 / 静态两条车道、集成修复、模块门修复、单代理修复环)都调它、不直调 `withUnattendedLeg`
82
- * (`test/leader-entry-floor.test.ts` 按语法树数调用点):「这条腿的词从哪来」只有入口体一个答案。
79
+ * leader 车道铸 spec 的**唯一**合并口:spec 的词钳在入口体的词之下(`runtime-governance.ts` `entryPermissionMode(spec 的词, 入口词)` —— 与受理门同一只函数;
80
+ * 写进 spec 的值就是它的 `effective`),再交给无人值守腿的合并口(`withUnattendedLeg` 的 `default` 钳在那个词之下)。入口体无词 ⇒ spec 原物交给
81
+ * `withUnattendedLeg`,与不经本口逐字同。本车道的 spec 今天自己不带模式词(子任务体只有 objective,其余并进的键是预算 / 审批 / 身份)⇒ 入口词在场时写下的就是它;
82
+ * 将来带了词,比入口词松的折回入口词、比它紧的留着 —— 与一条提交体过受理门同一个答案,不另写「写」的规则。五个出生点(worker 首跑的工厂 / 静态两条车道、
83
+ * 集成修复、模块门修复、单代理修复环)都调它、不直调 `withUnattendedLeg`(`test/leader-entry-floor.test.ts` 按语法树数调用点):「这条腿的词从哪来」只有入口体一个答案。
83
84
  */
84
85
  export declare function withLeaderLeg<S extends Partial<TaskSpec>>(spec: S, entry: LeaderRunEntry): S & UnattendedLegSpec;
85
86
  export interface LeaderDeps {
@@ -4,6 +4,7 @@ import { pullDiff } from "./diffout.js";
4
4
  import { mergeBranches } from "./merge.js";
5
5
  import { Runner, runWithVerification, resumeWithVerification } from "@sema-agent/core";
6
6
  import { pausedCauseOf, outwardPlane } from "../terminal.js";
7
+ import { entryPermissionMode } from "../runtime-governance.js";
7
8
  import { withUnattendedLeg } from "../unattended-permission-mode.js";
8
9
  const MAX_RESUME_SLICES = 200;
9
10
  export async function autoResumeOnResource(initial, signal, resume, maxSlices = MAX_RESUME_SLICES) {
@@ -17,7 +18,8 @@ export async function autoResumeOnResource(initial, signal, resume, maxSlices =
17
18
  return vr;
18
19
  }
19
20
  export function withLeaderLeg(spec, entry) {
20
- return withUnattendedLeg(entry.permissionMode === undefined ? spec : { ...spec, permissionMode: entry.permissionMode });
21
+ const word = entryPermissionMode(spec.permissionMode, entry.permissionMode).effective;
22
+ return withUnattendedLeg(word === undefined ? spec : { ...spec, permissionMode: word });
21
23
  }
22
24
  export async function runLeaderTask(task, durableBaseSha, deps) {
23
25
  let subs;
@@ -2,7 +2,7 @@
2
2
  * 一条 leader run 的**入口体**与「它进不进得了 leader 车道」的唯一判据。
3
3
  *
4
4
  * 规则(与受理门、run-local 同一条):运维 `AUTONOMY=plan` 是提交入口的模式词地板,盖在每一个 spec 出生点。leader 请求不带调用方的模式词,
5
- * 所以入口体是一份空体盖上地板(受理门同一只盖章 `task-settings.ts` `stampEntryPermissionMode`;地板的唯一属主 `runtime-governance.ts`
5
+ * 所以入口词 = `runtime-governance.ts` `entryPermissionMode(undefined, 地板).effective`(与受理门、run-local 同一只函数;地板的唯一属主
6
6
  * `autonomyModeFloor`)。
7
7
  *
8
8
  * `plan` 是**生命周期词**:它靠人审通道(`plan_review`)出口 —— 一个人批准计划,run 才离开 plan。leader 的 worker 会话**没有**那条通道
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { type Autonomy } from "../runtime-governance.js";
16
16
  import type { LeaderRunEntry } from "./leader.js";
17
- /** 一条 leader run 的入口体:地板读一次、受理门同一只盖章(地板缺席 ⇒ `{}`)。调用方读**活** config(热改只影响之后起的 run)。 */
17
+ /** 一条 leader run 的入口体:地板读一次、入口词经受理门同一只函数(地板缺席 ⇒ `{}`)。调用方读**活** config(热改只影响之后起的 run)。 */
18
18
  export declare function leaderRunEntry(autonomy: Autonomy | undefined): LeaderRunEntry;
19
19
  /** 拒句:原因(运维的 plan 地板 × 这条车道没有计划审阅通道)+ 两条出路(改走有计划审阅的 run 车道 / 运维换档)。不含任何配置值之外的回显。
20
20
  * 受理口(HTTP 422 的 `error`)与 run 出生处(run 的 `error`)读的是这一份;字节由 `test/api-error-text-freeze.test.ts` 的常量锚格冻。 */
@@ -1,7 +1,7 @@
1
- import { autonomyModeFloor } from "../runtime-governance.js";
2
- import { stampEntryPermissionMode } from "../task-settings.js";
1
+ import { autonomyModeFloor, entryPermissionMode } from "../runtime-governance.js";
3
2
  export function leaderRunEntry(autonomy) {
4
- return stampEntryPermissionMode({}, autonomyModeFloor(autonomy));
3
+ const word = entryPermissionMode(undefined, autonomyModeFloor(autonomy)).effective;
4
+ return word === undefined ? {} : { permissionMode: word };
5
5
  }
6
6
  export const LEADER_PLAN_ENTRY_REFUSAL = "this deployment runs with AUTONOMY=plan, which starts every task in plan mode and lets it out of plan mode only when a person approves its plan through plan review; " +
7
7
  "a leader task's workers have no plan review channel, so they could neither write nor hand their plan to anyone who can approve it. Nothing was started. " +
@@ -65,8 +65,8 @@ export interface LeaderWireConfig {
65
65
  * 保留它准备时的那一份(core 的自然快照语义,两条腿同律)。 */
66
66
  deploymentPosture?: () => DeploymentPostureSeats;
67
67
  /**
68
- * 运维 `AUTONOMY`(活 config 的那一位)—— 本车道每条 leader run **出生那一拍**读一次,经 `run-entry.ts` 的 `leaderRunEntry`(入口地板 + 受理门同一只盖章)
69
- * 盖成这条 run 的入口体(`LeaderDeps.entry`);本 run 铸的每一条 spec 与 worker 的续跑体都从它取词。入口词是 `plan` 的 run 进不了这条车道
68
+ * 运维 `AUTONOMY`(活 config 的那一位)—— 本车道每条 leader run **出生那一拍**读一次,经 `run-entry.ts` 的 `leaderRunEntry`(入口地板 + 受理门同一只入口函数 `entryPermissionMode`)
69
+ * 得出这条 run 的入口体(`LeaderDeps.entry`);本 run 铸的每一条 spec 与 worker 的续跑体都从它取词。入口词是 `plan` 的 run 进不了这条车道
70
70
  * (`leaderEntryRefusal`:worker 会话没有计划审阅通道)⇒ 出生处即以拒句收场;HTTP 受理口用同一只判据先答 422。
71
71
  *
72
72
  * 🔴 **函数席**(与 `deploymentPosture` / `durable` 同一条先例):`config.autonomy` 被 config-center 原地热改,boot 期取一次值会把这条腿钉在起服那一刻。
@@ -1,4 +1,5 @@
1
1
  import { type ModelEntryRefusalCode, type ModelPlaneRefusalCode, type ModelRefusalsSignal } from "@sema-agent/settings-schema";
2
+ import { type DroppedCatalogEntry } from "./config-center/read-warnings.js";
2
3
  /**
3
4
  * 条目拒因码 → 本仓铸它的成因 —— **以码为键**(S-768):码闭集的属主是 settings-schema(`MODEL_ENTRY_REFUSAL_CODES`,6.2.0 起;此前本仓
4
5
  * 自持一份同名码表,撞名已消灭、零别名)。那边加一个条目码 ⇒ 本表缺键 ⇒ 本仓 tsc 红(只拿 `satisfies` 约束码值证明的是「子集」,
@@ -58,23 +59,31 @@ export interface CatalogEntryLike {
58
59
  * 坐标,不取它的输出(输出会补缺省、剥扩展键,那是读腿的事)。
59
60
  */
60
61
  export declare function judgeCatalogEntry(m: CatalogEntryLike, ctx: CatalogEntryVerdictCtx): ModelEntryRefusal | undefined;
61
- /** 一批已启用条目的判决:可用的(保序)、不可用的(按 catalog name)、以及形不对的被丢身份载体行(坐标)。 */
62
+ /** 一批已启用条目的判决:可用的(保序)、不可用的(按 catalog name)、被丢目录条目的载体行(形对的,保序),以及形不对的载体行(坐标)。 */
62
63
  export interface CatalogEntriesVerdict<M extends CatalogEntryLike> {
63
64
  readonly usable: readonly M[];
64
65
  /** 键 = catalog name(**每一个**名,含秘密形 / 空串 / `__proto__`:引擎自己的判决保持完整 —— S-712 不变量与选择面的「让出」都读它;
65
66
  * 名筛只在出口,见 {@link buildModelRefusalsSignal})。键以自有数据属性写入(`__proto__` 不走原型 setter)。 */
66
67
  readonly refused: Readonly<Record<string, ModelEntryRefusal>>;
67
- /** S-712:被丢身份**载体形不对**的行坐标(`models.3`;载体不是列表时是一个固定标记)。读腿只为有名的被丢条目铸载体行
68
- * (`read-warnings.ts` `DroppedModelIdentity`,`name` 必在),所以这一形只来自绕过读腿的输入被改过(盘上 LKG 被手改 / 手写
69
- * effective)。非空 ⇒ 读不出的名字无从证明它不被增广铸出 ⇒ 不变量 `refused-names-not-minted` 不成立 ⇒ 模型面组拒(fail-closed)。 */
68
+ /** S-712:被丢目录条目**载体形不对**的行坐标(`models.3`;读不出下标时是 `(unknown coordinate)`;载体不是列表时是一个固定标记)。读腿铸的每一行
69
+ * 都合 `read-warnings.ts` {@link DroppedCatalogEntryRow} 的形,所以这一形只来自绕过读腿的输入被改过(盘上 LKG 被手改 / 手写 effective)。
70
+ * 非空 ⇒ 改过的行原本代表谁说不清,无从证明它的名字不被增广铸出 ⇒ 不变量 `refused-names-not-minted` 不成立 ⇒ 模型面组拒(fail-closed)。 */
70
71
  readonly malformed: readonly string[];
72
+ /** S-1295:载体里形对的行(保序)—— 模型面 stage 把它与 `models` 同 commit 进 `config.droppedCatalogEntries`(`GET /v1/models.droppedEntries`
73
+ * 的源):载体只在这里被判形、被读一次,墓碑与 wire 读的是同一批行。 */
74
+ readonly dropped: readonly DroppedCatalogEntry[];
71
75
  }
72
76
  /**
73
77
  * 批量形(模型面 stage 与 dry-run 共用这一只 —— 报数与真 apply 永不撕裂)。
74
- * S-712:`dropped` = 读腿交出的被丢条目身份(`EffectiveConfig.models.droppedIdentities`)—— 它们与逐条判据判出的 `entry_invalid`
75
- * 是**同一件事**(条目不合 `ModelEntry` 形),只是在第一道边界就被丢了;每一行铸一块墓碑(拒因 provider 词空:告警身份不带它)。
76
- * 入参按 `unknown` 判形:载体可能来自盘上 LKG(本仓写的,但文件可被改)。**规则一条**:载体要么缺席,要么是一列带字符串 `name`
77
- * 的行;其余一律「载体形不对」进 `malformed`(fail-closed)—— 读腿自己从不铸这种形。
78
+ * S-712:`dropped` = 读腿交出的被丢目录条目载体(`EffectiveConfig.models.droppedCatalogEntries`,每一条被丢的目录条目一行)。入参按 `unknown`
79
+ * 判形:载体可能来自盘上 LKG(本仓写的,但文件可被改)。载体要么缺席,要么是一列合 {@link DroppedCatalogEntryRow} 形的行;其余一律「载体形不对」
80
+ * 进 `malformed`(fail-closed)—— 读腿自己从不铸这种形。
81
+ * **墓碑判据(一条,S-712 原样)**:行的 `identity.name` 在 ∧ `identity.enabled !== false` ⇒ 铸一块墓碑(它与逐条判据判出的 `entry_invalid`
82
+ * 是同一件事 —— 条目不合 `ModelEntry` 形,只是在第一道边界就被丢了;拒因 provider 词空:告警身份不带它)。其余行不是墓碑:
83
+ * · 停用 ⇒ 不在目录(与 7.100.0 同义);
84
+ * · 没有名 —— 原文 `name` 不是字符串,或是字符串但不能原样出门(settings-schema `isDisclosableIdentity` 说不,身份里不带)⇒ **无名可留**,
85
+ * 与停用同处置。后者对本判据同样安全:档位增广只铸闭集词(`ultra` / `max` / `pro` / `flash` / `lite`、CC 别名、`best`),没有一个是
86
+ * 秘密形或非标识符,被扣下的名字无从被铸出;代价只是显式点它的请求拿到「未知模型」而不是 `request.model_unavailable`。
78
87
  * 与一条可用 / 已判不可用条目同名时不覆盖(可用条目仍可用;同名撞车由不变量 `refused-names-not-minted` 响亮拒)。
79
88
  */
80
89
  export declare function judgeCatalogEntries<M extends CatalogEntryLike>(enabled: readonly M[], ctx: CatalogEntryVerdictCtx, dropped?: unknown): CatalogEntriesVerdict<M>;
@@ -163,6 +172,6 @@ export interface RefusedNameView {
163
172
  export declare function mintedRefusedNames(view: RefusedNameView): MintedRefusedName[];
164
173
  /** 不变量 `refused-names-not-minted` 的拒因句(`undefined` = 成立)。warn 与世代账 / 拒因同一句;逐对明细另走 {@link mintedRefusedNames}。 */
165
174
  export declare function refusedNameMintedRefusal(view: RefusedNameView): string | undefined;
166
- /** 被丢身份载体形不对的拒因句(同一不变量同一个码:读不出的名字无从证明它不被增广铸出)。 */
175
+ /** 被丢目录条目载体形不对的拒因句(同一不变量同一个码:改过的行原本代表谁说不清,无从证明它的名字不被增广铸出)。 */
167
176
  export declare function malformedDroppedCarrierRefusal(entries: readonly string[]): string;
168
177
  //# sourceMappingURL=model-entry-refusal.d.ts.map
@@ -1,8 +1,8 @@
1
1
  import { CC_MODEL_TIER_ALIASES, DEFAULT_TIER_ORDER, expandTiers } from "@sema-agent/core";
2
- import { ModelEntry, isReportableRefusalName } from "@sema-agent/settings-schema";
2
+ import { ModelEntry, formatConfigPath, isReportableRefusalName } from "@sema-agent/settings-schema";
3
3
  import { catalogBrainLane } from "./model-provider.js";
4
4
  import { readCatalogEntryHeaders } from "./model-entry-headers.js";
5
- import { describeSchemaIssueFields, describeSchemaIssues } from "./config-center/read-warnings.js";
5
+ import { DroppedCatalogEntryRow, describeSchemaIssueFields, describeSchemaIssues } from "./config-center/read-warnings.js";
6
6
  import { DEFAULT_MODEL_REF } from "./model-select.js";
7
7
  export const ENTRY_REFUSAL_REASON_OF = {
8
8
  "config.model_no_brain": "no_brain",
@@ -55,7 +55,7 @@ function entryInvalidRefusal(id, provider, fields, issues) {
55
55
  "deployment's catalog on its own (the other entries keep serving) instead of being served with a value nothing validated; fix the entry to bring it back",
56
56
  };
57
57
  }
58
- const MALFORMED_CARRIER_NOT_A_LIST = "(droppedIdentities is not a list)";
58
+ const MALFORMED_CARRIER_NOT_A_LIST = "(droppedCatalogEntries is not a list)";
59
59
  export function judgeCatalogEntries(enabled, ctx, dropped) {
60
60
  const usable = [];
61
61
  const refused = {};
@@ -71,20 +71,25 @@ export function judgeCatalogEntries(enabled, ctx, dropped) {
71
71
  }
72
72
  const rows = dropped === undefined ? [] : Array.isArray(dropped) ? dropped : undefined;
73
73
  if (rows === undefined)
74
- return { usable, refused, malformed: [MALFORMED_CARRIER_NOT_A_LIST] };
74
+ return { usable, refused, malformed: [MALFORMED_CARRIER_NOT_A_LIST], dropped: [] };
75
75
  const malformed = [];
76
+ const carried = [];
76
77
  for (const d of rows) {
77
- const e = (typeof d === "object" && d !== null ? d : {});
78
- if (typeof e.name !== "string") {
79
- malformed.push(typeof e.entry === "string" ? e.entry : "(unknown coordinate)");
78
+ const row = DroppedCatalogEntryRow.safeParse(d);
79
+ if (!row.success) {
80
+ const index = typeof d === "object" && d !== null ? d.index : undefined;
81
+ malformed.push(typeof index === "number" && Number.isSafeInteger(index) && index >= 0 ? formatConfigPath(["models", index]) : "(unknown coordinate)");
80
82
  continue;
81
83
  }
82
- if (Object.hasOwn(refused, e.name))
84
+ carried.push(row.data);
85
+ const { identity } = row.data;
86
+ if (identity?.name === undefined || identity.enabled === false)
83
87
  continue;
84
- const fields = Array.isArray(e.fields) ? e.fields.filter((f) => typeof f === "string") : [];
85
- mint(e.name, entryInvalidRefusal(typeof e.id === "string" ? e.id : "", "", fields, typeof e.issues === "string" ? e.issues : fields.join(", ")));
88
+ if (Object.hasOwn(refused, identity.name))
89
+ continue;
90
+ mint(identity.name, entryInvalidRefusal(identity.id ?? "", "", row.data.fields, row.data.issues));
86
91
  }
87
- return { usable, refused, malformed };
92
+ return { usable, refused, malformed, dropped: carried };
88
93
  }
89
94
  export function buildModelRefusalsSignal(v) {
90
95
  if (v === undefined)
@@ -185,8 +190,8 @@ export function refusedNameMintedRefusal(view) {
185
190
  "rename the unavailable entry, fix it (see its own warn), or rebind / drop the tier binding that mints the name.");
186
191
  }
187
192
  export function malformedDroppedCarrierRefusal(entries) {
188
- return (`the dropped-entry identity carrier of this candidate is malformed [${REFUSED_NAME_MINTED_CODE}]: ${entries.join(", ")} — ` +
189
- "every row the settings-schema read writes there carries a string name, so this carrier was edited outside the read legs (e.g. a hand-edited last-known-good file); " +
190
- "the candidate cannot prove the tier/alias expansion does not re-mint the unnamed entries, so its model plane is refused — restore or delete that carrier (a fresh read of the source re-mints it).");
193
+ return (`the dropped catalog entry carrier of this candidate is malformed [${REFUSED_NAME_MINTED_CODE}]: ${entries.join(", ")} — ` +
194
+ "every row the settings-schema read writes there has the carrier row shape (index, reasonCode, optional identity, fields, issues), so this carrier was edited outside the read legs (e.g. a hand-edited last-known-good file); " +
195
+ "the candidate cannot prove the tier/alias expansion does not re-mint the entries those rows stood for, so its model plane is refused — restore or delete that carrier (a fresh read of the source re-mints it).");
191
196
  }
192
197
  //# sourceMappingURL=model-entry-refusal.js.map
@@ -15,7 +15,8 @@
15
15
  * (`MODEL_GATEWAY_FALLBACK_URLS`)是主网关的镜像,没有主就没有镜像 ⇒ 拒。
16
16
  *
17
17
  * ## 执法点(判据只在这里,执法点在调用处 —— 与 S-475 `gateway-url-userinfo` 同形)
18
- * · boot:`config.ts` 九域解析之后的跨域不变量区(锚 `CROSS-INVARIANT:model-route-endpoint`)⇒ 拒启;严格名册
18
+ * · boot:`config.ts` 九域解析之后的跨域不变量区(锚 `CROSS-INVARIANT:model-route-endpoint`)⇒ 拒启;经 `model-route-gap.ts`
19
+ * 的 {@link judgeModelRouteGap}(env 读成本文件判据要的切面,与包根导出的纯判官是**同一只**);严格名册
19
20
  * 布防(`isRosterGateArmed`)时 env 腿是占位、从不被派发 ⇒ 不判它(备用网关那一条照判);
20
21
  * · 热路径:`config-center/apply-effective.ts` 的模型面 stage ⇒ 模型面组拒(opus 腿 O3:同候选其余组照落)、上一代模型面继续服务;
21
22
  * · run-local:配置源解析完之后按真要派发的表再判一次(`run-local.ts`,7.98.0 合并复审 F2)—— 本入口没有名册门,
@@ -37,6 +38,23 @@ import type { Model } from "@sema-agent/core";
37
38
  export declare const MODEL_ROUTE_ENDPOINT_MISSING_CODE = "config.model_route_endpoint_missing";
38
39
  /** 同族第二码:备用网关设了、主网关没设。与上一码分开是因为出路不同(补主网关 / 撤备用 vs 补主网关 / 给条目 baseUrl)。 */
39
40
  export declare const GATEWAY_FALLBACK_WITHOUT_PRIMARY_CODE = "config.gateway_fallback_without_primary";
41
+ /**
42
+ * **路由缺口的出路轴**(闭集;{@link judgeModelRouteGap} 的 `gap` 词,包根导出 —— 壳 / 包在引擎起来之前问同一句)。
43
+ *
44
+ * 与上面的**码**是两根轴:码是组拒的机读词(`ModelPlaneRefusalCode`,settings-schema 属主),这里是**出路**:
45
+ * · `endpoint_missing` —— 缺的是网关端点:openai 兼容腿上的条目解析不到 base URL(或备用网关设了、主网关没设);
46
+ * 出路 = 设 `MODEL_GATEWAY_BASEURL`,或给条目自己的 `baseUrl`;
47
+ * · `lane_credential_missing` —— 缺的是**车道凭据**:条目的 provider 词是 `anthropic`(用户选的是 anthropic 车道),
48
+ * 却因 `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` 都没有而没装上 anthropic 路由、落进了 openai 腿,那条腿又没有端点;
49
+ * 出路 = 补那两把钥匙之一(设网关是另一条出路,拒因句一并给);
50
+ * · `none` —— 这份配置能派发到的每一条 openai 兼容路由都解析得到端点。
51
+ *
52
+ * **声明序 = 语义序**(缺口两词在前,`none` 最后);加一词 ⇒ 所有穷举 switch 当场编译红。
53
+ */
54
+ export declare const MODEL_ROUTE_GAP_WORDS: readonly ["endpoint_missing", "lane_credential_missing", "none"];
55
+ export type ModelRouteGap = (typeof MODEL_ROUTE_GAP_WORDS)[number];
56
+ /** 有缺口的那两词(判决的结构化形只在有缺口时存在,`none` = 判决为 `undefined`)。 */
57
+ export type ModelRouteGapBreach = Exclude<ModelRouteGap, "none">;
40
58
  /**
41
59
  * 拒因给出的**本机网关显式写法**。`src/` 里本机 8000 那个字面的**唯一**住处 —— 源码门钉在
42
60
  * `test/config-gateway-endpoint-required.test.ts` ⑧(出现在别处 = 有人又在铸缺省)。
@@ -82,6 +100,8 @@ export declare function modelRouteEndpointRefusal(view: ModelRouteEndpointView):
82
100
  /** S-768:部署级判决的结构化形 —— 码与拒因句由**同一次**判决给出(一个不变量 id 两个码,组拒码在判决那刻当数据带出,不从句子里抠)。 */
83
101
  export interface ModelRouteEndpointVerdict {
84
102
  readonly code: typeof GATEWAY_FALLBACK_WITHOUT_PRIMARY_CODE | typeof MODEL_ROUTE_ENDPOINT_MISSING_CODE;
103
+ /** 出路轴({@link MODEL_ROUTE_GAP_WORDS} 的缺口两词):同一次判决给出,不从句子或码里反推(`lane_credential_missing` 与 `endpoint_missing` 共用一个码)。 */
104
+ readonly gap: ModelRouteGapBreach;
85
105
  readonly reason: string;
86
106
  }
87
107
  /**
@@ -2,6 +2,7 @@ import { dispatchBrainLane } from "./model-provider.js";
2
2
  import { DEFAULT_MODEL_REF } from "./model-select.js";
3
3
  export const MODEL_ROUTE_ENDPOINT_MISSING_CODE = "config.model_route_endpoint_missing";
4
4
  export const GATEWAY_FALLBACK_WITHOUT_PRIMARY_CODE = "config.gateway_fallback_without_primary";
5
+ export const MODEL_ROUTE_GAP_WORDS = ["endpoint_missing", "lane_credential_missing", "none"];
5
6
  export const LOCAL_GATEWAY_HINT = "MODEL_GATEWAY_BASEURL=http://127.0.0.1:8000/v1";
6
7
  function declared(url) {
7
8
  return url !== undefined && url.trim() !== "" ? url : undefined;
@@ -24,11 +25,12 @@ export function judgeModelRouteEndpoint(view) {
24
25
  "after the primary fails; with no primary there is nothing to fail over from, and MODEL_GATEWAY_BASEURL has no " +
25
26
  `factory default. Set MODEL_GATEWAY_BASEURL to the primary gateway (for one on this machine: ${LOCAL_GATEWAY_HINT}), ` +
26
27
  "or unset MODEL_GATEWAY_FALLBACK_URLS.");
27
- return { code: GATEWAY_FALLBACK_WITHOUT_PRIMARY_CODE, reason };
28
+ return { code: GATEWAY_FALLBACK_WITHOUT_PRIMARY_CODE, gap: "endpoint_missing", reason };
28
29
  }
29
30
  const anthropicMounted = view.anthropic !== undefined;
30
31
  const entries = Object.entries(view.models ?? {});
31
32
  const unresolved = [];
33
+ let laneCredentialMissing = false;
32
34
  for (const [name, m] of entries) {
33
35
  if (name === DEFAULT_MODEL_REF && entries.some(([k, other]) => k !== name && other === m))
34
36
  continue;
@@ -36,11 +38,15 @@ export function judgeModelRouteEndpoint(view) {
36
38
  continue;
37
39
  if (openaiRouteBaseUrl(m.baseUrl, gateway) !== undefined)
38
40
  continue;
39
- unresolved.push(m.provider === "anthropic"
41
+ const anthropicWordWithoutRoute = m.provider === "anthropic";
42
+ laneCredentialMissing ||= anthropicWordWithoutRoute;
43
+ unresolved.push(anthropicWordWithoutRoute
40
44
  ? `"${name}" (provider "anthropic", but no Anthropic route is mounted — ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN are unset — so it falls to the openai-compatible lane)`
41
45
  : `"${name}"`);
42
46
  }
43
- return unresolved.length > 0 ? { code: MODEL_ROUTE_ENDPOINT_MISSING_CODE, reason: routeEndpointMissingRefusal(unresolved) } : undefined;
47
+ if (unresolved.length === 0)
48
+ return undefined;
49
+ return { code: MODEL_ROUTE_ENDPOINT_MISSING_CODE, gap: laneCredentialMissing ? "lane_credential_missing" : "endpoint_missing", reason: routeEndpointMissingRefusal(unresolved) };
44
50
  }
45
51
  function routeEndpointMissingRefusal(quotedNames) {
46
52
  const one = quotedNames.length === 1;
@@ -0,0 +1,103 @@
1
+ import { type ModelRouteEndpointVerdict, type ModelRouteGapBreach } from "./model-route-endpoint.js";
2
+ /**
3
+ * 路由读数读的八把 env 键(**闭表**;`judgeModelRouteGap` 只碰这几把,测试用代理钉「读集 ⊆ 本表」)。
4
+ * 配置目录的 D1 对账门把 {@link readModelRouteEnv} 的调用当作这八把键的读点(`test/config-catalog.test.ts` 的 `DELEGATED_ENV_READERS`,键集取本表,不手抄)。
5
+ */
6
+ export declare const MODEL_ROUTE_ENV_KEYS: {
7
+ readonly gatewayBaseUrl: "MODEL_GATEWAY_BASEURL";
8
+ readonly gatewayFallbackUrls: "MODEL_GATEWAY_FALLBACK_URLS";
9
+ readonly provider: "MODEL_PROVIDER";
10
+ readonly mainId: "MODEL_ID";
11
+ readonly cheapId: "MODEL_CHEAP_ID";
12
+ readonly anthropicApiKey: "ANTHROPIC_API_KEY";
13
+ readonly anthropicAuthToken: "ANTHROPIC_AUTH_TOKEN";
14
+ readonly anthropicBaseUrl: "ANTHROPIC_BASE_URL";
15
+ };
16
+ /** 一份 env 快照(`process.env` 也满足这个形)。 */
17
+ export type ModelRouteEnv = Readonly<Record<string, string | undefined>>;
18
+ /**
19
+ * env 腿的 **cheap 槽**(`MODEL_CHEAP_ID`)铸不铸 —— 闭集四态,消费方用穷举 switch({@link mintedCheapId};`config.ts` 的启动通知):
20
+ * · `absent` —— 没设;
21
+ * · `without_main` —— 设了而主槽(`MODEL_ID`)没设:cheap 槽是主模型「同网关换个 id」的形,没有主槽就没有它要派生的东西 ⇒ 不铸(启动时说一句);
22
+ * · `same_as_main` —— 与主 id 相同:同一个 id 说的就是主模型本身 ⇒ 不铸(启动时说一句);
23
+ * · `minted` —— 铸(`id` = `MODEL_CHEAP_ID`)。
24
+ */
25
+ export type EnvCheapSlot = {
26
+ readonly kind: "absent";
27
+ } | {
28
+ readonly kind: "without_main" | "same_as_main" | "minted";
29
+ readonly id: string;
30
+ };
31
+ /** cheap 槽铸出来的那个 id(不铸 ⇒ `undefined`)。**穷举 switch**:{@link EnvCheapSlot} 加一态不补臂 ⇒ 编译红。 */
32
+ export declare function mintedCheapId(slot: EnvCheapSlot): string | undefined;
33
+ /** {@link readModelRouteEnv} 的读数。 */
34
+ export interface ModelRouteEnvReading {
35
+ /** `MODEL_GATEWAY_BASEURL`;**空串 = 未设**(compose `${X:-}` 常态),缺席不铸。 */
36
+ readonly gatewayBaseUrl: string | undefined;
37
+ /** `MODEL_GATEWAY_FALLBACK_URLS` 逐项 trim、丢空项。 */
38
+ readonly gatewayFallbackUrls: string[];
39
+ /** `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_BASE_URL`:空串 = 未设,别名各自独立(一个空别名不遮蔽另一个)。 */
40
+ readonly anthropicApiKey: string | undefined;
41
+ readonly anthropicAuthToken: string | undefined;
42
+ readonly anthropicBaseUrl: string | undefined;
43
+ /** anthropic 路由装不装 = 两把凭据任一在场(`createBrain` 同判);只读在场性。 */
44
+ readonly anthropicRouteMounted: boolean;
45
+ /**
46
+ * env 腿模型的 provider 词:显式 `MODEL_PROVIDER` 胜(原样,**不在此过闭集** —— 域外拒启是 `model-provider-word` 那条不变量的事);
47
+ * 缺席时「`ANTHROPIC_BASE_URL` + anthropic 凭据两样齐」⇒ `anthropic`(provider 推断),否则 `gateway`。
48
+ */
49
+ readonly provider: string;
50
+ /** `provider` 的来路(推断那一臂启动时要说一句)。 */
51
+ readonly providerSource: "explicit" | "inferred" | "default";
52
+ /** `MODEL_ID`;空串 = 没有 env 主槽(env 腿不铸任何可派发条目)。 */
53
+ readonly mainId: string;
54
+ /** cheap 槽铸不铸(见 {@link EnvCheapSlot})。 */
55
+ readonly cheapSlot: EnvCheapSlot;
56
+ }
57
+ /** 逗号串读法的归一点:逐项 trim、丢空项(缺席 = 空串 = `[]`)。本文件的 CSV 路由键与 `config.ts` 的逗号串旋钮(按名薄壳 `csv(name)`)同读这一只;
58
+ * 目录模块的回显分词与 `SERVICE_AUTH_TOKENS` 的 `k=v` 对分词是同形的另两处,尚未改调(清账版收,本函数届时挪到中性模块)。 */
59
+ export declare const csvOf: (raw: string | undefined) => string[];
60
+ /**
61
+ * **env 模型路由读数的唯一读口**(纯:按 {@link MODEL_ROUTE_ENV_KEYS} 点名取值,不读 `process.env`,不枚举入参)。
62
+ * `parseModelDomain`(起服铸 `ServiceConfig`)与 {@link judgeModelRouteGap}(判官)吃同一份读数。
63
+ */
64
+ export declare function readModelRouteEnv(env: ModelRouteEnv): ModelRouteEnvReading;
65
+ /**
66
+ * **env 腿的条目表** —— 主槽别名 `default` + 主槽 id(+ 铸出的 cheap 槽);没有 `MODEL_ID` ⇒ 空表(不铸占位条目:一个不存在的模型名
67
+ * 进可选目录 / 当缺省,正是 3.0.0 删掉的那种病)。`parseModelDomain` 铸真 `Model` 表与判官铸路由切面都经这一只,条目名与键序不会各写一份。
68
+ */
69
+ export declare function envLaneTable<M extends {
70
+ readonly id: string;
71
+ }>(mainId: string, main: M, cheap: M | undefined): Record<string, M>;
72
+ /** {@link judgeModelRouteGap} 的旁路入参。 */
73
+ export interface ModelRouteGapOptions {
74
+ /**
75
+ * env 腿的模型会不会被派发(缺省 `true`)。起服期严格名册布防(`CONFIG_REQUIRE_ROSTER=true` 且接了远程中心、非 dryRun)= 运维声明 env
76
+ * `MODEL_ID` 是**占位**:名册落地前计费提交 503、落地后整表换装 ⇒ env 腿从不被派发,不在「配置能派发到的」射程里(备用网关那一条与派发
77
+ * 无关,照判)。布防与否是中心接入的事实(不是路由键),快照读不出 ⇒ 由调用方给;壳问「本机引擎、配置在本机」那一形不用传。
78
+ */
79
+ readonly envLaneDispatchable?: boolean;
80
+ }
81
+ /**
82
+ * 判官的读数(闭集三词,穷举 switch 消费):
83
+ * · `none` —— 无缺口,仅此一键;
84
+ * · 缺口两词 —— `code` = 起服拒启的机读码(与 `reason` 里的方括号码同一个词;`lane_credential_missing` 也带 `config.model_route_endpoint_missing`),
85
+ * `reason` = 起服拒启句原文(点名条目与出路)。
86
+ */
87
+ export type ModelRouteGapReading = {
88
+ readonly gap: "none";
89
+ } | {
90
+ [G in ModelRouteGapBreach]: {
91
+ readonly gap: G;
92
+ readonly code: ModelRouteEndpointVerdict["code"];
93
+ readonly reason: string;
94
+ };
95
+ }[ModelRouteGapBreach];
96
+ /**
97
+ * **起服前可调的模型路由缺口纯判官**(与起服判官同一实现;包根导出)。
98
+ *
99
+ * @param env 一份 env 快照(只读 {@link MODEL_ROUTE_ENV_KEYS} 那八把键;`process.env` 也行 —— 起服段就是这样调的)。
100
+ * @returns 三词之一;有缺口时带 `code` 与 `reason`(见 {@link ModelRouteGapReading})。
101
+ */
102
+ export declare function judgeModelRouteGap(env: ModelRouteEnv, opts?: ModelRouteGapOptions): ModelRouteGapReading;
103
+ //# sourceMappingURL=model-route-gap.d.ts.map
@@ -0,0 +1,80 @@
1
+ import { DEFAULT_MODEL_REF } from "./model-select.js";
2
+ import { judgeModelRouteEndpoint } from "./model-route-endpoint.js";
3
+ export const MODEL_ROUTE_ENV_KEYS = {
4
+ gatewayBaseUrl: "MODEL_GATEWAY_BASEURL",
5
+ gatewayFallbackUrls: "MODEL_GATEWAY_FALLBACK_URLS",
6
+ provider: "MODEL_PROVIDER",
7
+ mainId: "MODEL_ID",
8
+ cheapId: "MODEL_CHEAP_ID",
9
+ anthropicApiKey: "ANTHROPIC_API_KEY",
10
+ anthropicAuthToken: "ANTHROPIC_AUTH_TOKEN",
11
+ anthropicBaseUrl: "ANTHROPIC_BASE_URL",
12
+ };
13
+ export function mintedCheapId(slot) {
14
+ switch (slot.kind) {
15
+ case "minted":
16
+ return slot.id;
17
+ case "absent":
18
+ case "without_main":
19
+ case "same_as_main":
20
+ return undefined;
21
+ }
22
+ }
23
+ const declaredValue = (v) => (v !== undefined && v !== "" ? v : undefined);
24
+ export const csvOf = (raw) => (raw ?? "")
25
+ .split(",")
26
+ .map((s) => s.trim())
27
+ .filter(Boolean);
28
+ export function readModelRouteEnv(env) {
29
+ const K = MODEL_ROUTE_ENV_KEYS;
30
+ const anthropicApiKey = declaredValue(env[K.anthropicApiKey]);
31
+ const anthropicAuthToken = declaredValue(env[K.anthropicAuthToken]);
32
+ const anthropicBaseUrl = declaredValue(env[K.anthropicBaseUrl]);
33
+ const anthropicRouteMounted = anthropicApiKey !== undefined || anthropicAuthToken !== undefined;
34
+ const explicitProvider = declaredValue(env[K.provider]);
35
+ const inferAnthropic = anthropicBaseUrl !== undefined && anthropicRouteMounted;
36
+ const mainId = env[K.mainId] ?? "";
37
+ const rawCheapId = env[K.cheapId];
38
+ return {
39
+ gatewayBaseUrl: declaredValue(env[K.gatewayBaseUrl]),
40
+ gatewayFallbackUrls: csvOf(env[K.gatewayFallbackUrls]),
41
+ anthropicApiKey,
42
+ anthropicAuthToken,
43
+ anthropicBaseUrl,
44
+ anthropicRouteMounted,
45
+ provider: explicitProvider ?? (inferAnthropic ? "anthropic" : "gateway"),
46
+ providerSource: explicitProvider !== undefined ? "explicit" : inferAnthropic ? "inferred" : "default",
47
+ mainId,
48
+ cheapSlot: !rawCheapId
49
+ ? { kind: "absent" }
50
+ : mainId === ""
51
+ ? { kind: "without_main", id: rawCheapId }
52
+ : rawCheapId === mainId
53
+ ? { kind: "same_as_main", id: rawCheapId }
54
+ : { kind: "minted", id: rawCheapId },
55
+ };
56
+ }
57
+ export function envLaneTable(mainId, main, cheap) {
58
+ if (mainId === "")
59
+ return {};
60
+ const table = { [DEFAULT_MODEL_REF]: main, [main.id]: main };
61
+ if (cheap !== undefined)
62
+ table[cheap.id] = cheap;
63
+ return table;
64
+ }
65
+ export function judgeModelRouteGap(env, opts = {}) {
66
+ const r = readModelRouteEnv(env);
67
+ const slot = (id) => ({ id, baseUrl: "", provider: r.provider });
68
+ const cheapId = mintedCheapId(r.cheapSlot);
69
+ const models = opts.envLaneDispatchable === false ? {} : envLaneTable(r.mainId, slot(r.mainId), cheapId !== undefined ? slot(cheapId) : undefined);
70
+ const verdict = judgeModelRouteEndpoint({
71
+ gatewayBaseUrl: r.gatewayBaseUrl,
72
+ gatewayFallbackUrls: r.gatewayFallbackUrls,
73
+ anthropic: r.anthropicRouteMounted ? {} : undefined,
74
+ models,
75
+ });
76
+ if (verdict === undefined)
77
+ return { gap: "none" };
78
+ return { gap: verdict.gap, code: verdict.code, reason: verdict.reason };
79
+ }
80
+ //# sourceMappingURL=model-route-gap.js.map