/** * Resolve the model ref for a task spec from the request (CLI `/model` picker + `run --model`). * * Priority (a deliberate selection beats incidental free text): * 1) an explicit, allow-listed `body.model` (the picker / `--model ` flag), * 2) an inline `@name` mention in the objective (core's `parseModelMention`), * 3) the `default` role. * * Security: BOTH the explicit field and the @mention are gated by the SAME catalog (`config.models` keys), so * untrusted body/text can pick WHICH configured model but can never inject a baseUrl/apiKey (parseModelMention * only ever returns an allow-listed catalog name). * * [ref]②(生产实锤:面板上选了 A 模型,实际跑的是目录里的另一个): * - explicit ref 走 name+id 双键索引({@link matchCatalogModel})——壳 boot 回读的 mainLoopModel 是 **id 形**, * 目录键是 **name 形**,单键索引把 id 形请求当「未知」静默落 default = session 中途静默换模型(最恶性的 * 上下文污染路径)。 * - 真未知不再无声:FRESH submit 在 HTTP 门 400 fail-loud(带 available 列表,http/server.ts);本函数保持 * 不 throw(RESUME 重放持久化 body 不过 400 门——模型可能事后被移出目录,砖死 resume 比降级更糟),但把 * 未知 ref 透出给调用方(`unknownExplicit`),resolveSpec 记 warn——降级永远可见,绝不静默。 * * Returns the chosen model ref AND the objective with any `@mention` stripped (parseModelMention's cleanedText). * * 🔴 Was a real defect: resolveSpec previously read ONLY the inline @mention, so `body.model` (the picker + the * `--model` flag) was SILENTLY DROPPED — the picker UX was a no-op server-side. This restores it. */ /** 调用方**什么都没选**时这条 task 落到的目录 ref —— 即 `config.models.default` 那一格(引擎侧 * `resolveModel("default", catalog)` 解它)。**单点**:读面要说「这台部署的主模型是谁」时读同一个词 * (`auto-mode-face.ts` 的席解析器入参),两处各写一个 `"default"` 字面串正是漂移的成因。 */ export declare const DEFAULT_MODEL_REF = "default"; export declare function resolveTaskModel(bodyModel: unknown, objective: string, catalog: Record, allowlist?: readonly string[]): { model: string; cleanedObjective: string; unknownExplicit?: string; notAllowed?: ModelNotAllowed; }; /** [ref] 判别式拒因:名单在场且这次**用户选择**落在名单外。`resolved` 已是目录 name 形(归一后)。 */ export interface ModelNotAllowed { /** 调用方发来的原始 ref(name / 档位词 / 别名 / id / `@name`),已做过日志安全的 repr 归一。 */ requested: string; /** 归一到的目录 name(即这次真会跑的模型)。 */ resolved: string; /** 走的是哪条用户通道:`explicit`=body.model/settings.model,`mention`=objective 内联 `@name`。 */ source: "explicit" | "mention"; } /** * [ref] / [ref] —— **归一条款**:membership 判定前把 ref 归一到**终端 catalog 条目**再比。 * * 为什么不能拿字符串直接跟名单比:`expandTiers` 的增广目录里,档位词(`pro`)与 CC 别名(`sonnet`) * 的值与 `config.models[]` 是**同一个对象引用**——裸串比较会让 `@pro` 跳过一张按 name 写的名单 * (同一只模型,两个键)。所以两边(ref 与每个名单条目)都先经 {@link matchCatalogModel} 解到目录条目, * 再按 **引用相等** 判同。id 形的 ref 不需要特别处理:`matchCatalogModel` 本身就是 name+id 双键索引, * 它会把 id 解成目录键,引用相等接着就对上了。 * * 🔴 **刻意没有「id 相等」退路**(codex 复审 R1 [high],已采纳):目录**允许两条不同 name 的条目共享 * 同一个上游 `Model.id`,却各自带不同的 `baseUrl` / `apiKeyEnv` / `cost` / provider** —— 那正是多网关、 * 多账号部署形。按 id 判同会把 `safe` 与 `premium` 合并,于是一张只含 `safe` 的名单会放行 `premium`, * 而真正执行的是 `premium` 那份坐标与计费。判据只认**目录条目本身**:两条条目就是两只模型,哪怕上游 * id 撞车。(引用相等因此不是"优化",是唯一正确的判据;它天然覆盖 tier 词/别名/id 三形。) * * 缺席/空名单 ⇒ 恒 `true`(ship-dark:缺省 = 旧行为,全放行)。 * 目录里根本没有的 ref ⇒ 恒 `true`:那是「未知模型」那道门的辖区(fresh 400 / resume 降级留声), * 本门只答「在目录里,但准不准点」——两道门同码同拒会让消费端分不清"没这模型"和"不让你点"。 * * ⚠️ 射程:本门是**提交时**门。core 运行时路由(degrade 降级链 / cascade 梯 / roles 解析)按 operator * 配置自己走,**不经本门**——那不是一次用户选择,如实声明,不假装是运行时执法。 */ export declare function isModelAllowlisted(ref: string, catalog: Record, allowlist: readonly string[] | undefined): boolean; /** * [ref] —— ref → **终端 catalog name**(拒因/日志的可读形)。 * * 为什么不是 `matchCatalogModel` 的返回值:增广目录里 `pro`/`opus`/`default` 都是**真实存在的键**, * 于是 `matchCatalogModel("pro")` 回的就是 `"pro"` —— 那是用户点的那个别名,不是他真会跑的模型。 * 取条目自己的 `Model.name`(applyEffective 建目录时 `models[m.name].name === m.name`,env lane 同) * 才是终端名;拿不到时如实退回匹配键(诚实缺席,不猜)。 */ export declare function terminalCatalogName(ref: string, catalog: Record): string | undefined; /** * [ref]② name+id 双键目录索引:返回 ref 命中的目录 **name 键**(spec.model 的规范形),未知 → undefined。 * - name 命中用 Object.hasOwn(🔴 不用真值索引:Object.prototype 继承键 "constructor"/"__proto__" 等在裸 * 索引下是真值,会跳过 allow-list——沿袭原实现的纪律)。 * - id 命中扫目录值(`Model.id`),跳过 "default" 别名键(它与其指向的真身共享 id,解析到别名会让 /model * 面板与实际路由再度撕裂);同 id 多 name 时取目录序第一个(与 /v1/models 列表序一致,确定性)。 */ export declare function matchCatalogModel(ref: string, catalog: Record): string | undefined; /** * vision precheck: a catalog model can read images iff its `input` modality list * includes `"image"` (the SAME test as the vision capability on GET /v1/models — `server.ts`). FALSE-SAFE on * missing/unknown modality info: returns `true` (do NOT block) — only an EXPLICIT text-only `input` triggers the * precheck, so a model that never declared its modalities is never wrongly 422'd. Lets a request with images 422 * up-front instead of hitting the downstream gateway as an opaque deserialize 400 on a text-only model. */ export declare function modelSupportsImages(model: { input?: unknown; } | undefined): boolean; //# sourceMappingURL=model-select.d.ts.map