@sema-agent/server 7.97.0 → 7.98.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.
- package/MIGRATION.md +5 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +6 -0
- package/deploy/sema-up/cluster-up.sh +8 -4
- package/deploy/sema-up/kube-up.sh +40 -15
- package/deploy/sema-up/lib/envf.sh +173 -0
- package/deploy/sema-up/sema-up.sh +113 -94
- package/deploy/sema-up/smoke.sh +8 -1
- package/dist/boot/config-center.js +2 -2
- package/dist/boot/resolve-spec.js +9 -4
- package/dist/brain.js +28 -18
- package/dist/config-catalog.js +4 -4
- package/dist/config-center/apply-effective.js +7 -1
- package/dist/config-center/facade.d.ts +2 -1
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +9 -1
- package/dist/config-types.d.ts +12 -2
- package/dist/config.d.ts +9 -0
- package/dist/config.js +42 -5
- package/dist/hooks/hook-llm.d.ts +1 -1
- package/dist/hooks/hook-llm.js +3 -1
- package/dist/memory-scope.d.ts +62 -11
- package/dist/memory-scope.js +32 -3
- package/dist/model-provider.d.ts +13 -0
- package/dist/model-provider.js +3 -0
- package/dist/model-route-endpoint.d.ts +82 -0
- package/dist/model-route-endpoint.js +55 -0
- package/dist/project-identity.d.ts +29 -0
- package/dist/project-identity.js +120 -0
- package/dist/run-local.d.ts +14 -4
- package/dist/run-local.js +8 -5
- package/dist/trace/redact.d.ts +1 -1
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import { projectCollabToWorkflows, registerCollabWorkflows } from "../capabiliti
|
|
|
7
7
|
import { stageLimits } from "./stage-limits.js";
|
|
8
8
|
import { BATCH1_LIMIT_KEYS } from "./hot-keys-registry.js";
|
|
9
9
|
import { buildDisplaySafeUrl } from "../trace/redact.js";
|
|
10
|
+
import { crossInvariant } from "../config-invariants.js";
|
|
10
11
|
const charsPerTokenOf = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined);
|
|
11
12
|
function envDerivedModel(config) {
|
|
12
13
|
const m = config.envModel;
|
|
@@ -203,7 +204,7 @@ function stageEffective(config, eff, logger, opts) {
|
|
|
203
204
|
if (ok)
|
|
204
205
|
logger?.warn("sema_registry_model_base_url", { model: m.name, baseUrl: buildDisplaySafeUrl(rawBaseUrl), hint: "registry-directed gateway override — this model's key authenticates AGAINST THIS URL" });
|
|
205
206
|
else {
|
|
206
|
-
logger?.warn("sema_registry_model_base_url_rejected", { model: m.name, baseUrl: buildDisplaySafeUrl(String(rawBaseUrl)), hint: "only http(s) URLs are honored — falling back to the
|
|
207
|
+
logger?.warn("sema_registry_model_base_url_rejected", { model: m.name, baseUrl: buildDisplaySafeUrl(String(rawBaseUrl)), hint: "only http(s) URLs are honored — falling back to the deployment gateway (MODEL_GATEWAY_BASEURL); with none declared the whole candidate is refused (model-route-endpoint)" });
|
|
207
208
|
safeUrl = undefined;
|
|
208
209
|
}
|
|
209
210
|
}
|
|
@@ -264,6 +265,11 @@ function stageEffective(config, eff, logger, opts) {
|
|
|
264
265
|
const activeTiers = resolveActiveTiers(eff.models) ?? {};
|
|
265
266
|
const picked = resolveDefaultModelName(eff, (n) => models[n] !== undefined, enabled[0].name, (source, name) => logger?.warn("sema_registry_default_dangling", { source, name, hint: "explicit default names a model that is not in the enabled catalog — falling to the next source" }));
|
|
266
267
|
models.default = models[picked.name];
|
|
268
|
+
const endpointRefusal = crossInvariant("model-route-endpoint").check({ live: config, next: { ...config, models } });
|
|
269
|
+
if (endpointRefusal !== undefined) {
|
|
270
|
+
throw new Error(`candidate model plane refused whole (cross-invariant model-route-endpoint): ${endpointRefusal} ` +
|
|
271
|
+
"The current generation keeps serving; the candidate is re-judged on the next pull.");
|
|
272
|
+
}
|
|
267
273
|
const atModelAllowlist = stageAtModelAllowlist(eff.models?.atModelAllowlist, models, logger);
|
|
268
274
|
modelPlane = {
|
|
269
275
|
models,
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* (core 1.45), so each model/cascade-rung authenticates with its own upstream key.
|
|
16
16
|
*
|
|
17
17
|
* Per-model `baseUrl` IS transported (a catalog model may live on a different gateway;
|
|
18
|
-
* core brain honors model.baseUrl, absent = "" =
|
|
18
|
+
* core brain honors model.baseUrl, absent = "" = the deployment gateway `MODEL_GATEWAY_BASEURL`; S-623: with no gateway
|
|
19
|
+
* declared, a candidate carrying such an openai-lane entry is refused whole — cross-invariant `model-route-endpoint`).
|
|
19
20
|
*
|
|
20
21
|
* Hot-reload status (复审 2026-07-29 P1-11 — 亲读判定,取代此处旧的 "models/roles are restart-to-apply"
|
|
21
22
|
* TODO, which went stale when `mutateInPlace` landed):
|
|
@@ -28,9 +28,9 @@ import type { ServiceConfigFlat } from "./config-types.js";
|
|
|
28
28
|
*/
|
|
29
29
|
export declare const LIVENESS_HEARTBEAT_MS = 30000;
|
|
30
30
|
/** 闭集:新增一条不变量必须在这里加词(拼错 = 编译红,而不是运行期悄悄不判)。 */
|
|
31
|
-
export type CrossInvariantId = "run-stale-heartbeat" | "bind-posture" | "bake-runner-credential" | "direct-door-anchors" | "operator-principals-required" | "bake-operator-principals" | "sensitive-write-patterns" | "leader-host-lane" | "leader-durable-store" | "device-durable-store" | "device-durable-approval" | "model-provider-word" | "gateway-url-userinfo" | "require-execution-lane" | "hosted-requires-principal";
|
|
31
|
+
export type CrossInvariantId = "run-stale-heartbeat" | "bind-posture" | "bake-runner-credential" | "direct-door-anchors" | "operator-principals-required" | "bake-operator-principals" | "sensitive-write-patterns" | "leader-host-lane" | "leader-durable-store" | "device-durable-store" | "device-durable-approval" | "model-provider-word" | "gateway-url-userinfo" | "model-route-endpoint" | "require-execution-lane" | "hosted-requires-principal";
|
|
32
32
|
/** 表读的键集(取自平铺配置型 ⇒ 键名/类型永不与真配置漂)。 */
|
|
33
|
-
export type CrossInvariantView = Readonly<Pick<ServiceConfigFlat, "runStaleSec" | "authToken" | "authTokens" | "allowUnauthedWrites" | "imageBakes" | "directApprovalDoor" | "durableApproval" | "approvalHmacKeys" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "requirePrincipal" | "operatorPrincipals" | "sensitiveWritePatterns" | "leaderEnabled" | "remoteExec" | "requireExecutionLane" | "dbBackend" | "tidb" | "pg" | "envModel" | "gatewayBaseUrl" | "gatewayFallbackUrls" | "anthropic" | "authBridgeIssuer">>;
|
|
33
|
+
export type CrossInvariantView = Readonly<Pick<ServiceConfigFlat, "runStaleSec" | "authToken" | "authTokens" | "allowUnauthedWrites" | "imageBakes" | "directApprovalDoor" | "durableApproval" | "approvalHmacKeys" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "requirePrincipal" | "operatorPrincipals" | "sensitiveWritePatterns" | "leaderEnabled" | "remoteExec" | "requireExecutionLane" | "dbBackend" | "tidb" | "pg" | "envModel" | "gatewayBaseUrl" | "gatewayFallbackUrls" | "anthropic" | "models" | "authBridgeIssuer">>;
|
|
34
34
|
export interface CrossInvariantInput {
|
|
35
35
|
/** 现役配置(boot 期 = 候选自身)。 */
|
|
36
36
|
readonly live: CrossInvariantView;
|
|
@@ -2,6 +2,7 @@ import { assertGuardPatternsUsable } from "./deployment-governance.js";
|
|
|
2
2
|
import { hostedPrincipalRefusal } from "./hosted-posture.js";
|
|
3
3
|
import { modelProviderRefusal } from "./model-provider.js";
|
|
4
4
|
import { modelRouteUrlRefusal } from "./model-route-url.js";
|
|
5
|
+
import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
|
|
5
6
|
export const LIVENESS_HEARTBEAT_MS = 30_000;
|
|
6
7
|
const anyServiceAuth = (v) => Boolean(v.authToken) || Object.keys(v.authTokens ?? {}).length > 0;
|
|
7
8
|
const mustNarrowBind = (v) => Boolean(v.allowUnauthedWrites) && !anyServiceAuth(v);
|
|
@@ -160,12 +161,19 @@ export const CONFIG_CROSS_INVARIANTS = [
|
|
|
160
161
|
keys: ["gatewayBaseUrl", "gatewayFallbackUrls", "anthropic"],
|
|
161
162
|
restartReason: "model-route-url",
|
|
162
163
|
bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:gateway-url-userinfo" },
|
|
163
|
-
check: ({ next }) => modelRouteUrlRefusal("MODEL_GATEWAY_BASEURL", next.gatewayBaseUrl, "MODEL_API_KEY") ??
|
|
164
|
+
check: ({ next }) => modelRouteUrlRefusal("MODEL_GATEWAY_BASEURL", next.gatewayBaseUrl ?? "", "MODEL_API_KEY") ??
|
|
164
165
|
next.gatewayFallbackUrls
|
|
165
166
|
.map((u, i) => modelRouteUrlRefusal(`MODEL_GATEWAY_FALLBACK_URLS[${i}]`, u, "MODEL_API_KEY"))
|
|
166
167
|
.find((r) => r !== undefined) ??
|
|
167
168
|
modelRouteUrlRefusal("ANTHROPIC_BASE_URL", next.anthropic?.baseUrl ?? "", "ANTHROPIC_API_KEY"),
|
|
168
169
|
},
|
|
170
|
+
{
|
|
171
|
+
id: "model-route-endpoint",
|
|
172
|
+
keys: ["gatewayBaseUrl", "gatewayFallbackUrls", "anthropic", "models"],
|
|
173
|
+
restartReason: "model-route-endpoint",
|
|
174
|
+
bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:model-route-endpoint" },
|
|
175
|
+
check: ({ live, next }) => (next.models === live.models ? undefined : modelRouteEndpointRefusal(next)),
|
|
176
|
+
},
|
|
169
177
|
];
|
|
170
178
|
export function crossInvariant(id) {
|
|
171
179
|
const inv = CONFIG_CROSS_INVARIANTS.find((i) => i.id === id);
|
package/dist/config-types.d.ts
CHANGED
|
@@ -366,8 +366,18 @@ export interface ImageBakeConfig {
|
|
|
366
366
|
* fixtures that CONSTRUCT a config literal build this type and hand it to `attachConfigGroups`. */
|
|
367
367
|
export interface ServiceConfigFlat {
|
|
368
368
|
port: number;
|
|
369
|
-
/** OpenAI-compatible model gateway base URL (without /chat/completions)
|
|
370
|
-
|
|
369
|
+
/** OpenAI-compatible model gateway base URL (without /chat/completions) — `MODEL_GATEWAY_BASEURL`.
|
|
370
|
+
*
|
|
371
|
+
* 🔴 S-623:**缺席 = 这台部署没有网关**(没有出厂缺省;旧的本机占位值在读入那一步抹掉「来源」,让没配网关的
|
|
372
|
+
* 部署起得来、第一次调用才炸)。形取「可选键」而不是判别联合:消费方问的只有「有没有声明网关根、是哪个」,
|
|
373
|
+
* 一个只带一位载荷的联合不比 `string | undefined` 多说任何事,且与紧邻的 `gatewayApiKey?` / `gatewayMaxRetries?`
|
|
374
|
+
* 的「缺席不铸键」同一条 presence 律。一条 openai 路由请求打到哪儿,只经 `model-route-endpoint.ts` 的
|
|
375
|
+
* `openaiRouteBaseUrl` 求(路由自己的 `baseUrl` 胜、网关次之);「配置能派发到的每条 openai 路由都解析得到」
|
|
376
|
+
* 由 boot(config.ts)与热路径(apply-effective)同一只判据保证。 */
|
|
377
|
+
gatewayBaseUrl?: string;
|
|
378
|
+
/** `MODEL_API_KEY` —— **网关的**凭据,只与网关根配对([ref])。S-623:与 `gatewayBaseUrl` 同进同出 ——
|
|
379
|
+
* 没有网关就不装载(否则 brain 的声明根为空 ⇒ core 判 `unpinned` ⇒ 这把 key 会被送到每条自带 baseUrl 的
|
|
380
|
+
* openai 条目),由 `model_gateway_key_unpaired` 启动通知说出来。 */
|
|
371
381
|
gatewayApiKey?: string;
|
|
372
382
|
/** Secondary openai-compatible gateways for cross-gateway FAILOVER (same protocol + same model id).
|
|
373
383
|
* Tried in order after the primary when a gateway fails before streaming any content. */
|
package/dist/config.d.ts
CHANGED
|
@@ -243,6 +243,15 @@ export declare const REQUIRED_ENVS_BY_LANE: Record<RemoteExecProvider, readonly
|
|
|
243
243
|
* a loud CONFIG_NOTICES line ("removed-1m"); the center lane's roster culture is explicit declaration.
|
|
244
244
|
* Shared by the env lane (main + cheap models) and the center lane (sema-registry toModel). */
|
|
245
245
|
export declare function applyAutoCompactWindow(m: Model, explicit?: number): "explicit" | "sonnet5" | "removed-1m" | "removed";
|
|
246
|
+
/**
|
|
247
|
+
* **严格名册布防**的唯一判据(boot 就绪门 `boot/config-center.ts` 与 S-623 端点判据同调这一只)。
|
|
248
|
+
*
|
|
249
|
+
* 布防 = 远程 registry ∧ 非 dryRun ∧ `CONFIG_REQUIRE_ROSTER=true`(严格 `"true"`,直读 env、不进 ServiceConfig —— 纯
|
|
250
|
+
* boot 期姿势旋钮,原判据逐字搬来,零语义变化)。布防时运维声明 env MODEL_ID 是**占位**:名册落地前计费提交 503、
|
|
251
|
+
* `/health` ready:false,落地后整张模型表被中心目录换装。dryRun 是 compare-only(名册从不被应用),local / 纯 env
|
|
252
|
+
* 部署没有「中心」可等 ⇒ 都不布防。
|
|
253
|
+
*/
|
|
254
|
+
export declare function isRosterGateArmed(configCenter: ServiceConfig["configCenter"]): boolean;
|
|
246
255
|
/**
|
|
247
256
|
* 一条工具名**永不匹配任何现役工具**的三种拼法,及其修正指引。审批/权限名单是 server 侧 ToolPolicy 的
|
|
248
257
|
* RAW 逐字比对(canonicalToolName 折叠面已随 core 5.0.0 [ref] 退役),所以写错的名字不会报错、只会静默
|
package/dist/config.js
CHANGED
|
@@ -7,7 +7,7 @@ import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOU
|
|
|
7
7
|
import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/settings-schema";
|
|
8
8
|
import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
|
|
9
9
|
import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
|
|
10
|
-
import { isV2ScopeKey } from "./memory-scope.js";
|
|
10
|
+
import { isV2ScopeKey, memoryReplicationCoverage } from "./memory-scope.js";
|
|
11
11
|
import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
|
|
12
12
|
import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
|
|
13
13
|
import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
|
|
@@ -22,6 +22,7 @@ import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf } from "./execution-lane-ca
|
|
|
22
22
|
import { AUTONOMY_MODES } from "./runtime-governance.js";
|
|
23
23
|
import { modelProviderRefusal } from "./model-provider.js";
|
|
24
24
|
import { modelRouteUrlRefusal } from "./model-route-url.js";
|
|
25
|
+
import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
|
|
25
26
|
import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
|
|
26
27
|
export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
|
|
27
28
|
function csv(name) {
|
|
@@ -795,8 +796,11 @@ function parseStoreDomain(ctx) {
|
|
|
795
796
|
: undefined,
|
|
796
797
|
};
|
|
797
798
|
}
|
|
799
|
+
export function isRosterGateArmed(configCenter) {
|
|
800
|
+
return configCenter !== undefined && !configCenter.dryRun && process.env.CONFIG_REQUIRE_ROSTER === "true";
|
|
801
|
+
}
|
|
798
802
|
function parseModelDomain() {
|
|
799
|
-
const gatewayBaseUrl =
|
|
803
|
+
const gatewayBaseUrl = firstSetEnv("MODEL_GATEWAY_BASEURL");
|
|
800
804
|
const modelId = env("MODEL_ID", "");
|
|
801
805
|
if (modelId === "") {
|
|
802
806
|
throw new Error("MODEL_ID is required (no factory default since server 3.0.0) — set MODEL_ID=<model name your gateway serves>, " +
|
|
@@ -811,7 +815,7 @@ function parseModelDomain() {
|
|
|
811
815
|
}
|
|
812
816
|
}
|
|
813
817
|
for (const [envKey, raw, knob] of [
|
|
814
|
-
["MODEL_GATEWAY_BASEURL", gatewayBaseUrl, "MODEL_API_KEY"],
|
|
818
|
+
["MODEL_GATEWAY_BASEURL", gatewayBaseUrl ?? "", "MODEL_API_KEY"],
|
|
815
819
|
...csv("MODEL_GATEWAY_FALLBACK_URLS").map((u, i) => [`MODEL_GATEWAY_FALLBACK_URLS[${i}]`, u, "MODEL_API_KEY"]),
|
|
816
820
|
["ANTHROPIC_BASE_URL", firstSetEnv("ANTHROPIC_BASE_URL") ?? "", "ANTHROPIC_API_KEY"],
|
|
817
821
|
]) {
|
|
@@ -918,6 +922,15 @@ function parseModelDomain() {
|
|
|
918
922
|
const models = { default: model, [model.id]: model };
|
|
919
923
|
if (cheapModel)
|
|
920
924
|
models[cheapModel.id] = cheapModel;
|
|
925
|
+
const gatewayApiKey = process.env.MODEL_API_KEY;
|
|
926
|
+
if (gatewayBaseUrl === undefined && gatewayApiKey !== undefined && gatewayApiKey !== "") {
|
|
927
|
+
CONFIG_NOTICES.push({
|
|
928
|
+
event: "model_gateway_key_unpaired",
|
|
929
|
+
fields: {
|
|
930
|
+
hint: "MODEL_API_KEY is set but MODEL_GATEWAY_BASEURL is not — MODEL_API_KEY is the gateway's credential and pairs only with the gateway's own root, so with no gateway it is NOT loaded and is sent to no route (a model with its own baseUrl authenticates with its own per-model key). Set MODEL_GATEWAY_BASEURL to use it, or unset MODEL_API_KEY to silence this line.",
|
|
931
|
+
},
|
|
932
|
+
});
|
|
933
|
+
}
|
|
921
934
|
const cheapRef = cheapModel ? cheapModel.id : DEFAULT_MODEL_REF;
|
|
922
935
|
const verifierEnv = process.env.MODEL_VERIFIER;
|
|
923
936
|
const verifierRef = verifierEnv && models[verifierEnv] ? verifierEnv : DEFAULT_MODEL_REF;
|
|
@@ -940,8 +953,7 @@ function parseModelDomain() {
|
|
|
940
953
|
}
|
|
941
954
|
const degradeOn = parseDegradeOn(csv("MODEL_DEGRADE_ON"));
|
|
942
955
|
return {
|
|
943
|
-
gatewayBaseUrl,
|
|
944
|
-
gatewayApiKey: process.env.MODEL_API_KEY,
|
|
956
|
+
...(gatewayBaseUrl !== undefined ? { gatewayBaseUrl, gatewayApiKey } : {}),
|
|
945
957
|
gatewayFallbackUrls: csv("MODEL_GATEWAY_FALLBACK_URLS"),
|
|
946
958
|
...(process.env.GATEWAY_MAX_RETRIES ? { gatewayMaxRetries: numEnvBounded("GATEWAY_MAX_RETRIES", "2", 0, 20) } : {}),
|
|
947
959
|
anthropic: firstSetEnv("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN")
|
|
@@ -1469,6 +1481,19 @@ function parseMemoryDomain(ctx) {
|
|
|
1469
1481
|
...syncEntryCap("MEMORY_SYNC_MAX_PULL_ENTRIES", "maxPullEntries"),
|
|
1470
1482
|
};
|
|
1471
1483
|
}
|
|
1484
|
+
const replication = memoryReplicationCoverage(memorySync !== undefined ? { memorySync } : {});
|
|
1485
|
+
if (replication.kind === "scopes" && memoryEngineEnabled && !requirePrincipal && memoryScope !== undefined) {
|
|
1486
|
+
CONFIG_NOTICES.push({
|
|
1487
|
+
event: "memory_project_scope_yields_to_sync",
|
|
1488
|
+
fields: {
|
|
1489
|
+
replicatedScopes: replication.scopes.join(","),
|
|
1490
|
+
writeScope: memoryScope,
|
|
1491
|
+
hint: "memory sync (MEMORY_SYNC_*) replicates only the scope(s) listed — the per-project memory layer (S-512, repo:<key>) is not replicated, " +
|
|
1492
|
+
"so sessions do NOT write project-keyed memory here: new memory lands in the global layer (MEMORY_SCOPE), the same as 7.97.0, and every " +
|
|
1493
|
+
"project reads it. Project-layer replication is a separate change; unset MEMORY_SYNC_URL to get per-project memory without sync.",
|
|
1494
|
+
},
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1472
1497
|
const projectMemoryEnabled = boolEnvWithLegacyNegated("PROJECT_MEMORY_ENABLED", "PROJECT_MEMORY_DISABLED", true);
|
|
1473
1498
|
const memoryOrgAdmissionMode = enumEnv("MEMORY_ORG_ADMISSION_MODE", "enforce", ["audit", "enforce"]);
|
|
1474
1499
|
const memoryOrgDirectoryJson = process.env.MEMORY_ORG_DIRECTORY_JSON || undefined;
|
|
@@ -2225,6 +2250,18 @@ export function loadConfig() {
|
|
|
2225
2250
|
`or set CONFIG_PROVIDER=local to take the single-machine \`host\` default, ` +
|
|
2226
2251
|
`or unset REQUIRE_EXECUTION_LANE to keep running without hands on purpose.`);
|
|
2227
2252
|
}
|
|
2253
|
+
{
|
|
2254
|
+
const envLaneDispatchable = !isRosterGateArmed(integrations.configCenter);
|
|
2255
|
+
const refusal = modelRouteEndpointRefusal({
|
|
2256
|
+
gatewayBaseUrl: modelPlane.gatewayBaseUrl,
|
|
2257
|
+
gatewayFallbackUrls: modelPlane.gatewayFallbackUrls,
|
|
2258
|
+
anthropic: modelPlane.anthropic,
|
|
2259
|
+
models: envLaneDispatchable ? modelPlane.models : {},
|
|
2260
|
+
});
|
|
2261
|
+
if (refusal !== undefined) {
|
|
2262
|
+
throw new Error(refusal);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2228
2265
|
EXEC_LANE_NOTICE = buildExecLaneNotice(orchestration.remoteExec);
|
|
2229
2266
|
return attachConfigGroups({
|
|
2230
2267
|
...store,
|
package/dist/hooks/hook-llm.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { HookLlmCall } from "./hook-runner.js";
|
|
|
9
9
|
* - `openai-completions` ⇒ POST {base}/chat/completions(现状,字节不变);
|
|
10
10
|
* - `anthropic-messages` ⇒ POST {base}/v1/messages(x-api-key / Authorization: Bearer + anthropic-version
|
|
11
11
|
* 头,messages 数组,回包取 content[].text)。此前 hookLlm 只会说 openai 形且缺省 baseUrl 恒落
|
|
12
|
-
* `config.gatewayBaseUrl`(
|
|
12
|
+
* `config.gatewayBaseUrl`(当年它有一个本机 8000 端口的出厂缺省,S-623 已删)——anthropic 单路由部署(主模型
|
|
13
13
|
* provider=anthropic 走 ANTHROPIC_BASE_URL,没配 MODEL_GATEWAY_BASEURL)下 titler 每会话打死
|
|
14
14
|
* localhost:8000 ⇒ `session_title_failed: TypeError: fetch failed`;即使端点活着,anthropic-messages
|
|
15
15
|
* 模型也不能用 /chat/completions 形调。
|
package/dist/hooks/hook-llm.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { adjudicateModelRoute, joinRequestUrl, resolveRouteCredential, routeKeyOf, routeRefusalText } from "@sema-agent/core";
|
|
2
2
|
import { matchCatalogModel } from "../model-select.js";
|
|
3
|
+
import { resolveOpenaiRouteEndpoint } from "../model-route-endpoint.js";
|
|
3
4
|
import { buildDisplaySafeRefusal, redactSecrets } from "../trace/redact.js";
|
|
4
5
|
export function buildHookFailureText(raw) {
|
|
5
6
|
if (raw === "")
|
|
@@ -26,7 +27,8 @@ export function createHookLlm(deps) {
|
|
|
26
27
|
}
|
|
27
28
|
return { ok: true, catalogRef, providerModelId, baseUrl, api };
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
+
const endpoint = resolveOpenaiRouteEndpoint(catalogRef, m?.baseUrl, config.gatewayBaseUrl);
|
|
31
|
+
return endpoint.ok ? { ok: true, catalogRef, providerModelId, baseUrl: endpoint.baseUrl, api } : { ok: false, error: endpoint.refusal };
|
|
30
32
|
};
|
|
31
33
|
const hookModelFor = (requested) => {
|
|
32
34
|
const shape = (ref) => shapeFrom(ref, config.models[ref], config.models[ref]?.id ?? ref);
|
package/dist/memory-scope.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FileMemoryEngineBackend, type TaskSpec } from "@sema-agent/core";
|
|
2
2
|
import type { ServiceConfig } from "./config-types.js";
|
|
3
3
|
import { type ExecutionLane } from "./execution-lane-caps.js";
|
|
4
|
+
import { type MemoryProjectKey } from "./project-identity.js";
|
|
4
5
|
/** 前缀词表本体(供门/诊断读;判定一律走 {@link isV2ScopeKey},别在别处重写 startsWith 链)。 */
|
|
5
6
|
export declare const V2_SCOPE_PREFIXES: readonly string[];
|
|
6
7
|
/**
|
|
@@ -12,17 +13,22 @@ export declare const V2_SCOPE_PREFIXES: readonly string[];
|
|
|
12
13
|
* 校验点 fail-loud。
|
|
13
14
|
*/
|
|
14
15
|
export declare function isV2ScopeKey(key: string): boolean;
|
|
15
|
-
/** Resolve the memory scope for a request
|
|
16
|
+
/** Resolve the memory scope for a request —— {@link memoryScopeFor} 的三形(按实现逐臂写;S-512 合并前主会话点名:
|
|
17
|
+
* 旧版这里写「多租户恒 `undefined` ⇒ 记忆暗」,与下面 DB 后端那一臂正好相反,test [ref] 就是被这句带偏的)。
|
|
16
18
|
*
|
|
17
|
-
* SINGLE-USER
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
19
|
+
* ① SINGLE-USER(`requirePrincipal !== true`):返回**全局层** `config.memoryScope`(缺省 "local";显式
|
|
20
|
+
* `MEMORY_SCOPE` 就是它),与调用方自报的 principal 标签无关(单用户 worker 的 `anon:*` / 临时 principal
|
|
21
|
+
* 是面上的标签,不是隔离身份 —— 按它们键控会把一个用户的记忆拆散到各个面)。🔴 S-512([ref] ①)起它只是
|
|
22
|
+
* **读列表的第二层**,不是写家:写家是该任务的**项目层**,由 {@link memoryProjectKeyFor} 按任务根派生、经
|
|
23
|
+
* {@link memorySpecForRequest} 的 `projectKey` 位铸进 spec(读 = [项目, 本 scope],`writeScope` 显式 = 项目)。
|
|
24
|
+
* 本函数在授权期调(还不知道任务根),所以项目层不在这里。
|
|
25
|
+
* ② MULTI-TENANT + DB 后端(`requirePrincipal === true` ∧ `memoryEngineBackend` ∈ {`pg`, `tidb`}):按**验过的**
|
|
26
|
+
* principal 铸 v2 typed 键 —— 无 projectId ⇒ `user:<tenant>`(core `formatUserScope`),有形状合法的
|
|
27
|
+
* projectId ⇒ `proj:<tenant>/<projectId>`(core `formatProjScope`);principal 缺席(匿名 run)⇒ `undefined`
|
|
28
|
+
* (无记忆)。scope 列是租户分区,授权就是「验过的 principal 进、一个租户的 scope 出」这一绑定;身份永远不来自
|
|
29
|
+
* body(projectId 只挑盘)。本形**不**按项目根键控(同一租户跨项目共用 `user:` 层是另一件事,S-626,待设计与裁定)。
|
|
30
|
+
* ③ MULTI-TENANT + file 后端:`undefined` ⇒ 没有 `spec.memory` ⇒ 记忆**暗**,fail-closed —— file 引擎底座是一棵共享
|
|
31
|
+
* 目录树、没有租户隔离,在它上面给租户按 principal 发 scope 就是跨租户串盘面。 */
|
|
26
32
|
/** R5(批γ):memory scope 列宽(两方言 VARCHAR(190) 同宽)。principal 上限 190 **字符**在
|
|
27
33
|
* security.ts 把身份轴守住了,但 `formatUserScope`/`formatProjScope` 的段编码(百分号转义)会
|
|
28
34
|
* **膨胀**——非 ASCII principal 编码后可超列宽,此前落裸 SQL 错。产出处响亮拒,错误可分类。
|
|
@@ -31,6 +37,43 @@ export declare function isV2ScopeKey(key: string): boolean;
|
|
|
31
37
|
* 只再导出——旧 import 路径(本文件)逐字不变,`memoryScopeFor` 的产出处消费点也不变。 */
|
|
32
38
|
export { MEMORY_SCOPE_COLUMN_CHARS, assertMemoryScopeWidth } from "./plugins/memory-key-guards.js";
|
|
33
39
|
export declare function memoryScopeFor(config: ServiceConfig, principal?: string, projectId?: string): string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* S-512 / [ref] ① —— 这一请求的**项目身份声明**(`TaskSpec.memory.projectKey`)的唯一属主。
|
|
42
|
+
*
|
|
43
|
+
* **一条规则,与车道无关**(主会话 09-23 裁 ②):项目根 = spec 解析出的**真任务根**(`boot/resolve-spec.ts`
|
|
44
|
+
* 的 `realTaskRootOf`,S-177 属主;run-local 腿 = 它的 `workspaceDir`)。本进程读得到这个根 ⇒ 求 canonical
|
|
45
|
+
* 项目根并铸 `repo:` 键(in-process 车道的真任务根是 `process.cwd()`,读得到,所以照常铸 —— 它的语义就是
|
|
46
|
+
* 「项目 = 任务根指向的地方」,不需要另一个词);读不到(根在沙箱/设备盘上,或 host 车道会话没登记 cwd、
|
|
47
|
+
* spec 期还不知道根)⇒ 调用方传 `undefined` ⇒ `{ absent: "unreadable" }`,core 铸 operator 通知
|
|
48
|
+
* `memory.project_scope_absent`,写照常落全局层 —— **响亮**,不是静默落 `local`。
|
|
49
|
+
*
|
|
50
|
+
* 🔴 **写家只落复制路径覆盖得到的层**(7.98.0 合并复审 F1,主会话裁「统一规则,不做特判」):写家 = 读列表里、本部署
|
|
51
|
+
* **每一条已配置的复制路径都覆盖到**的最具体的一层。项目层不在覆盖面里 ⇒ 本任务**不按项目键控**,写家落全局层,
|
|
52
|
+
* 读列表与 7.97.0 同形 —— 否则开了 `MEMORY_SYNC_*` 的部署会把新记忆写进一个同步腿从不碰的层(静默不同步)。
|
|
53
|
+
* 覆盖面只经 {@link memoryReplicationCoverage} 求(调用点零 `if (memorySync)`);以后项目层同步落地,只改覆盖面的
|
|
54
|
+
* 来源,本规则不动。让位由启动通知 `memory_project_scope_yields_to_sync` 说出来(`config.ts` 记忆域,读同一只覆盖面)。
|
|
55
|
+
* 缺席声明(`{ absent }`)不受覆盖面影响:它本来就写全局层,通知说的是「为什么没有项目层」。
|
|
56
|
+
*
|
|
57
|
+
* 返回 `undefined` 只有三形,都不是「忘了」:
|
|
58
|
+
* · 多租户(`requirePrincipal`)—— 项目层键控在多租户上另立(S-406 §3;principal 形 `user:`/`proj:` 不变);
|
|
59
|
+
* · 记忆面暗(`scope` 缺席)—— 没有 spec 可铸,也就不读盘;
|
|
60
|
+
* · 本任务的项目层不在复制覆盖面里(见上)—— 让位给全局层。
|
|
61
|
+
*/
|
|
62
|
+
export declare function memoryProjectKeyFor(config: Pick<ServiceConfig, "requirePrincipal" | "memorySync">, scope: string | undefined, taskRoot: string | undefined): MemoryProjectKey | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* 本部署记忆的**复制覆盖面** —— 唯一属主(7.98.0 合并复审 F1)。「复制路径」= 把某一层记忆搬出本机的腿;今天只有一条:
|
|
65
|
+
* TOC 同步客户端腿(`MEMORY_SYNC_*`,`memory-sync-client.ts`),它只同步**一层**(`config.memorySync.scope`,core
|
|
66
|
+
* `syncMemoryScope` 按 `scope ===` 取条目)。没有复制路径 ⇒ `unreplicated`(没有哪一层会被复制落下)。
|
|
67
|
+
*/
|
|
68
|
+
export type MemoryReplicationCoverage = {
|
|
69
|
+
readonly kind: "unreplicated";
|
|
70
|
+
} | {
|
|
71
|
+
readonly kind: "scopes";
|
|
72
|
+
readonly scopes: readonly string[];
|
|
73
|
+
};
|
|
74
|
+
export declare function memoryReplicationCoverage(config: Pick<ServiceConfig, "memorySync">): MemoryReplicationCoverage;
|
|
75
|
+
/** 覆盖面是否含这一层(`unreplicated` 含一切)。 */
|
|
76
|
+
export declare function replicationCovers(coverage: MemoryReplicationCoverage, scope: string): boolean;
|
|
34
77
|
/**
|
|
35
78
|
* N0 (通宵测试 2026-07-09,三重坐实 e2b+kata+代码): the file memory engine (core [ref]) materializes and
|
|
36
79
|
* harvests via `node:fs` on the WORKER's local fs, while a remote exec lane routes the model's file tools to
|
|
@@ -184,7 +227,14 @@ onIncident?: (err: Error & {
|
|
|
184
227
|
* harvest 不提交)· `memoryCapture:"off"` = 采集面关(挂、读、本会话内容永不进长期记忆)· `memory:"off"` =
|
|
185
228
|
* **整面关**(`enabled:false`,这一次连记忆面都不挂)。后两者从 `declarations` 进(键名与 wire 逐字同名)。
|
|
186
229
|
*/
|
|
187
|
-
export declare function memorySpecForRequest(scope: string | undefined,
|
|
230
|
+
export declare function memorySpecForRequest(scope: string | undefined,
|
|
231
|
+
/** S-512 / [ref] ①:本请求的项目身份声明({@link memoryProjectKeyFor} 的产物)。**必填位**(不是可选参数):
|
|
232
|
+
* 每个铸点都得写出一个值 —— `undefined` 只合法地表示「本任务不按项目键控」(多租户 / 记忆暗 / 项目层不在复制覆盖面里)。
|
|
233
|
+
* · `{ scope: repoKey }` ⇒ 读 = `[repoKey, scope, ...extras]`、`writeScope` **显式** = `repoKey`(core 缺省取末层
|
|
234
|
+
* = 全局层,不钉就是 M-03 本身;core 另有 `config.memory_write_scope_mismatch` 兜这一向);
|
|
235
|
+
* · `{ absent }` ⇒ 读 = `[scope, ...extras]`、`writeScope` 显式 = `scope`(全局层),core 铸 `memory.project_scope_absent`;
|
|
236
|
+
* · 两形都把声明原样落 `TaskSpec.memory.projectKey`(core 的生效面回显它)。 */
|
|
237
|
+
projectKey: MemoryProjectKey | undefined, memoryWrite: boolean | undefined, defaultScopes?: string[],
|
|
188
238
|
/** [ref] 件A([ref] 件3④):origin 盖章的部署形态维(N2)。`multiTenant=true`(requirePrincipal)
|
|
189
239
|
* ⇒ 登记簿 defaultScopes 的 org 键按 **request** 盖章(条目由调用方 projectId 选定=caller 可影响的
|
|
190
240
|
* 选择器,core 准入门据此过目录判决);单用户 ⇒ 一律 deployment(operator 登记簿条目归 deployment,
|
|
@@ -219,6 +269,7 @@ declarations?: {
|
|
|
219
269
|
scopeOrigins?: Record<string, "deployment" | "request">;
|
|
220
270
|
capture?: "off";
|
|
221
271
|
enabled?: false;
|
|
272
|
+
projectKey?: MemoryProjectKey;
|
|
222
273
|
} | undefined;
|
|
223
274
|
/**
|
|
224
275
|
* 「这一腿挂不挂记忆面」的**唯一属主**(合并复审 车HF F1,2026-09-18):与 core `prepareMemory` 的判据同源 ——
|
package/dist/memory-scope.js
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from "node:path";
|
|
|
2
2
|
import { FileMemoryEngineBackend, resolveMemoryEngineRoot, deriveControlPlaneDir, formatUserScope, formatProjScope, normalizeMemorySpec } from "@sema-agent/core";
|
|
3
3
|
import { executionLaneCaps, executionLaneOf } from "./execution-lane-caps.js";
|
|
4
4
|
import { assertMemoryScopeWidth } from "./plugins/memory-key-guards.js";
|
|
5
|
+
import { projectKeyForTaskRoot } from "./project-identity.js";
|
|
5
6
|
const V2_SCOPE_PREFIX_BY_KIND = {
|
|
6
7
|
user: "user:",
|
|
7
8
|
org: "org:",
|
|
@@ -26,6 +27,25 @@ export function memoryScopeFor(config, principal, projectId) {
|
|
|
26
27
|
}
|
|
27
28
|
return config.memoryScope;
|
|
28
29
|
}
|
|
30
|
+
export function memoryProjectKeyFor(config, scope, taskRoot) {
|
|
31
|
+
if (config.requirePrincipal === true || scope === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
const declared = projectKeyForTaskRoot(taskRoot);
|
|
34
|
+
if (declared.scope !== undefined && !replicationCovers(memoryReplicationCoverage(config), declared.scope))
|
|
35
|
+
return undefined;
|
|
36
|
+
return declared;
|
|
37
|
+
}
|
|
38
|
+
export function memoryReplicationCoverage(config) {
|
|
39
|
+
return config.memorySync === undefined ? { kind: "unreplicated" } : { kind: "scopes", scopes: [config.memorySync.scope] };
|
|
40
|
+
}
|
|
41
|
+
export function replicationCovers(coverage, scope) {
|
|
42
|
+
switch (coverage.kind) {
|
|
43
|
+
case "unreplicated":
|
|
44
|
+
return true;
|
|
45
|
+
case "scopes":
|
|
46
|
+
return coverage.scopes.includes(scope);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
29
49
|
export function memoryEngineRemoteLanePosture(config) {
|
|
30
50
|
const lane = executionLaneOf(config.remoteExec?.provider);
|
|
31
51
|
if (executionLaneCaps(lane).hostFilePlane === "supported")
|
|
@@ -97,12 +117,21 @@ export function memoryEngineBackendFor(config, fallbackRoot, onIncident) {
|
|
|
97
117
|
root: plane.root,
|
|
98
118
|
};
|
|
99
119
|
}
|
|
100
|
-
export function memorySpecForRequest(scope, memoryWrite, defaultScopes, originPolicy, declarations) {
|
|
120
|
+
export function memorySpecForRequest(scope, projectKey, memoryWrite, defaultScopes, originPolicy, declarations) {
|
|
101
121
|
if (!scope)
|
|
102
122
|
return undefined;
|
|
123
|
+
const projectScope = projectKey?.scope;
|
|
124
|
+
if (projectScope !== undefined && projectScope === scope) {
|
|
125
|
+
throw new Error(`memory project scope ${JSON.stringify(projectScope)} equals the global memory scope (MEMORY_SCOPE) — the project layer and the global layer would be one directory. ` +
|
|
126
|
+
"Set MEMORY_SCOPE to a name outside the server-minted project-key family (see src/project-identity.ts).");
|
|
127
|
+
}
|
|
103
128
|
const isV2 = isV2ScopeKey;
|
|
104
|
-
const extras = (defaultScopes ?? []).filter((s, i, arr) => typeof s === "string" && s.length > 0 && s !== scope && arr.indexOf(s) === i);
|
|
105
|
-
const layered =
|
|
129
|
+
const extras = (defaultScopes ?? []).filter((s, i, arr) => typeof s === "string" && s.length > 0 && s !== scope && s !== projectScope && arr.indexOf(s) === i);
|
|
130
|
+
const layered = projectKey !== undefined
|
|
131
|
+
? { scopes: projectScope !== undefined ? [projectScope, scope, ...extras] : [scope, ...extras], writeScope: projectScope ?? scope, projectKey }
|
|
132
|
+
: extras.length > 0
|
|
133
|
+
? { scopes: [scope, ...extras], writeScope: scope }
|
|
134
|
+
: { scopes: [scope] };
|
|
106
135
|
const v2 = isV2(scope) || extras.some(isV2) ? { scopeContract: "v2" } : {};
|
|
107
136
|
const stamped = {};
|
|
108
137
|
if (originPolicy !== undefined) {
|
package/dist/model-provider.d.ts
CHANGED
|
@@ -74,4 +74,17 @@ export declare function modelBrainLane(provider: ModelProvider): ModelBrainLane;
|
|
|
74
74
|
* 在这里加一个 throw 会让一次热发布打死在跑的副本,方向反了。
|
|
75
75
|
*/
|
|
76
76
|
export declare function catalogBrainLane(provider: string | undefined): ModelBrainLane;
|
|
77
|
+
/**
|
|
78
|
+
* 🔴 S-623 —— 一条目录条目在**本部署**上真正落哪条 brain 腿(= `createBrain` 的组装事实,不是词面)。
|
|
79
|
+
*
|
|
80
|
+
* `createBrain` 只在 anthropic 路由**装上**(`ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` 在场 ⇒
|
|
81
|
+
* `config.anthropic` 在场)时才铸 `createRoutingBrain({anthropic}, {fallback: gatewayStack})`;没装时整台机器
|
|
82
|
+
* 只有网关栈这一只 brain,**一切**条目(包括 `provider:"anthropic"` 的)都落 openai 兼容腿 ——
|
|
83
|
+
* `apply-effective.ts` 那条 `sema_registry_model_no_brain` warn 说的「mis-routes to the gateway brain」即此。
|
|
84
|
+
*
|
|
85
|
+
* 所以「这条路由要不要一个 openai 端点」的判据必须同时看词与装配:词判 {@link catalogBrainLane},
|
|
86
|
+
* 装配判路由在不在。本函数是这一合取的**唯一**写法 —— 端点判官(`model-route-endpoint.ts`)、降级腿与
|
|
87
|
+
* 启动摘要(`brain.ts`)都调它,不再各写一个 `=== "anthropic" && config.anthropic`。
|
|
88
|
+
*/
|
|
89
|
+
export declare function dispatchBrainLane(provider: string | undefined, anthropicRouteMounted: boolean): ModelBrainLane;
|
|
77
90
|
//# sourceMappingURL=model-provider.d.ts.map
|
package/dist/model-provider.js
CHANGED
|
@@ -23,4 +23,7 @@ export function modelBrainLane(provider) {
|
|
|
23
23
|
export function catalogBrainLane(provider) {
|
|
24
24
|
return isModelProvider(provider) ? modelBrainLane(provider) : "gateway";
|
|
25
25
|
}
|
|
26
|
+
export function dispatchBrainLane(provider, anthropicRouteMounted) {
|
|
27
|
+
return anthropicRouteMounted && catalogBrainLane(provider) === "anthropic" ? "anthropic" : "gateway";
|
|
28
|
+
}
|
|
26
29
|
//# sourceMappingURL=model-provider.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S-623 —— **openai 兼容路由的有效端点**:解析规则与「解析不到」的拒因,唯一属主。
|
|
3
|
+
*
|
|
4
|
+
* ## 病(事实,主会话 09-23 12:40 亲核 + cli L-589 真案)
|
|
5
|
+
* `MODEL_GATEWAY_BASEURL` 曾有一个出厂缺省(本机 8000 端口的 `/v1`)。缺省值在读入那一步就把「来源」抹掉了:
|
|
6
|
+
* 只设 `MODEL_ID` + key 的隔离配置照常起,第一次模型调用才在 core 的 `describeNetworkError` 里炸成
|
|
7
|
+
* `fetch failed (connect ECONNREFUSED …:8000)` —— core 不知道、也不该知道那个端点是缺省值;compose 的
|
|
8
|
+
* `${MODEL_GATEWAY_BASEURL:-}` 透传空串 ⇒ 落到**容器自身回环**,恒错;本仓 `run-local` 的预检注释写着「本机
|
|
9
|
+
* 用户必须指向自己的网关」,判据却只查 `/v1` 后缀,缺省值照样过。紧邻的 `MODEL_ID` 自 3.0.0 起同病同治
|
|
10
|
+
* (「旧缺省让每个外部部署都炸在离根因最远的地方」)。
|
|
11
|
+
*
|
|
12
|
+
* ## 规则(一句)
|
|
13
|
+
* **`MODEL_GATEWAY_BASEURL` 没有出厂缺省;配置能派发到的每一条 openai 兼容路由,都必须解析到一个显式配置的
|
|
14
|
+
* base URL(路由自己的 `baseUrl`,或显式设了的网关),否则拒。** 附带一条同类:备用网关
|
|
15
|
+
* (`MODEL_GATEWAY_FALLBACK_URLS`)是主网关的镜像,没有主就没有镜像 ⇒ 拒。
|
|
16
|
+
*
|
|
17
|
+
* ## 执法点(判据只在这里,执法点在调用处 —— 与 S-475 `gateway-url-userinfo` 同形)
|
|
18
|
+
* · boot:`config.ts` 九域解析之后的跨域不变量区(锚 `CROSS-INVARIANT:model-route-endpoint`)⇒ 拒启;严格名册
|
|
19
|
+
* 布防(`isRosterGateArmed`)时 env 腿是占位、从不被派发 ⇒ 不判它(备用网关那一条照判);
|
|
20
|
+
* · 热路径:`config-center/apply-effective.ts` 的模型面 stage ⇒ 候选整批拒、上一代继续服务;
|
|
21
|
+
* · run-local:配置源解析完之后按真要派发的表再判一次(`run-local.ts`,7.98.0 合并复审 F2)—— 本入口没有名册门,
|
|
22
|
+
* loadConfig 对严格名册布防的占位豁免在这里兑现不了;没布防时恒惰性;
|
|
23
|
+
* · 登记:`config-invariants.ts` 的 `model-route-endpoint` 条(表的「不许有表外不变量」职责);
|
|
24
|
+
* · 派发面:titler / hook 选模型(`hooks/hook-llm.ts`)与降级腿 / 启动摘要(`brain.ts`)求有效端点时调
|
|
25
|
+
* {@link openaiRouteBaseUrl},不再各写一个 `model.baseUrl || gatewayBaseUrl`。
|
|
26
|
+
*
|
|
27
|
+
* ## 三问
|
|
28
|
+
* · **谁需要**:忘设端点的本机 / 隔离配置用户与 compose 直用者(cli L-589)—— 他们今天起得来、第一次调用才炸;
|
|
29
|
+
* · **谁受伤**:隐式依赖本机 8000 网关(如 vLLM 缺省端口)的部署,从「照常起」变成「拒启」;
|
|
30
|
+
* · **补偿**:显式写一行 —— 拒因原样给出这一行({@link LOCAL_GATEWAY_HINT})。
|
|
31
|
+
*
|
|
32
|
+
* 依赖纪律:本文件是**叶**(只引 provider 词表与目录缺省别名),`config.ts` / `config-invariants.ts` /
|
|
33
|
+
* `brain.ts` / `hooks/hook-llm.ts` 都可以安全引它。
|
|
34
|
+
*/
|
|
35
|
+
import type { Model } from "@sema-agent/core";
|
|
36
|
+
/** 机读码(拼法随 `config.*` 家族,按**规则**命名不按键名):一条 openai 路由解析不到端点。 */
|
|
37
|
+
export declare const MODEL_ROUTE_ENDPOINT_MISSING_CODE = "config.model_route_endpoint_missing";
|
|
38
|
+
/** 同族第二码:备用网关设了、主网关没设。与上一码分开是因为出路不同(补主网关 / 撤备用 vs 补主网关 / 给条目 baseUrl)。 */
|
|
39
|
+
export declare const GATEWAY_FALLBACK_WITHOUT_PRIMARY_CODE = "config.gateway_fallback_without_primary";
|
|
40
|
+
/**
|
|
41
|
+
* 拒因给出的**本机网关显式写法**。`src/` 里本机 8000 那个字面的**唯一**住处 —— 源码门钉在
|
|
42
|
+
* `test/config-gateway-endpoint-required.test.ts` ⑧(出现在别处 = 有人又在铸缺省)。
|
|
43
|
+
*/
|
|
44
|
+
export declare const LOCAL_GATEWAY_HINT = "MODEL_GATEWAY_BASEURL=http://127.0.0.1:8000/v1";
|
|
45
|
+
/**
|
|
46
|
+
* **一条 openai 兼容路由的有效 base URL** —— 路由自己的 `baseUrl` 胜,否则显式设了的网关;都没有 ⇒ `undefined`。
|
|
47
|
+
*
|
|
48
|
+
* 与 core 请求期的解析(`resolveRequestRoot(model.baseUrl, config.baseUrl)`,entry 胜、declared 次之)
|
|
49
|
+
* 同序同尺;区别只在这里**没有**第三档 —— 本仓不给 core 递任何「车道缺省」,缺席就是缺席。
|
|
50
|
+
*/
|
|
51
|
+
export declare function openaiRouteBaseUrl(routeBaseUrl: string | undefined, gatewayBaseUrl: string | undefined): string | undefined;
|
|
52
|
+
/** 派发面用的单条形(titler / hook 选模型):有效端点,或一句可以原样交给运维的拒因。 */
|
|
53
|
+
export type OpenaiRouteEndpoint = {
|
|
54
|
+
readonly ok: true;
|
|
55
|
+
readonly baseUrl: string;
|
|
56
|
+
} | {
|
|
57
|
+
readonly ok: false;
|
|
58
|
+
readonly refusal: string;
|
|
59
|
+
};
|
|
60
|
+
/** 单条 openai 路由的有效端点 / 拒因(拒因与部署级判据是**同一只**铸点 {@link routeEndpointMissingRefusal})。 */
|
|
61
|
+
export declare function resolveOpenaiRouteEndpoint(routeName: string, routeBaseUrl: string | undefined, gatewayBaseUrl: string | undefined): OpenaiRouteEndpoint;
|
|
62
|
+
/** 部署级判据读的切面(`ServiceConfig` 与 `CrossInvariantView` 都结构满足)。 */
|
|
63
|
+
export interface ModelRouteEndpointView {
|
|
64
|
+
readonly gatewayBaseUrl?: string;
|
|
65
|
+
readonly gatewayFallbackUrls: readonly string[];
|
|
66
|
+
/** 在场 = anthropic 路由装上(`createBrain` 同判)。只读在场性。 */
|
|
67
|
+
readonly anthropic?: object;
|
|
68
|
+
readonly models: Readonly<Record<string, Pick<Model, "baseUrl" | "provider">>>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* **部署级判据**:这份配置能派发到的每一条 openai 兼容路由都解析得到端点吗?`undefined` = 是。
|
|
72
|
+
*
|
|
73
|
+
* 判序即语义:
|
|
74
|
+
* ① 备用网关设了、主网关没设 ⇒ `config.gateway_fallback_without_primary`(备用是主的镜像,这一条与条目无关,先说);
|
|
75
|
+
* ② 逐条目:按 {@link dispatchBrainLane} 落 openai 腿、且 {@link openaiRouteBaseUrl} 解析不到 ⇒ 收集名字,
|
|
76
|
+
* 一句话**全部点名**(运维一次改完,而不是改一个撞下一个)。
|
|
77
|
+
*
|
|
78
|
+
* `default` 键是目录里另一条的别名(env 腿 `{default: m, [m.id]: m}`、center 腿 `models.default = models[picked]`),
|
|
79
|
+
* 与别的键指同一只对象时不重复点名;只有它一个键时(手搓夹具)照样点名 `default`。
|
|
80
|
+
*/
|
|
81
|
+
export declare function modelRouteEndpointRefusal(view: ModelRouteEndpointView): string | undefined;
|
|
82
|
+
//# sourceMappingURL=model-route-endpoint.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { dispatchBrainLane } from "./model-provider.js";
|
|
2
|
+
import { DEFAULT_MODEL_REF } from "./model-select.js";
|
|
3
|
+
export const MODEL_ROUTE_ENDPOINT_MISSING_CODE = "config.model_route_endpoint_missing";
|
|
4
|
+
export const GATEWAY_FALLBACK_WITHOUT_PRIMARY_CODE = "config.gateway_fallback_without_primary";
|
|
5
|
+
export const LOCAL_GATEWAY_HINT = "MODEL_GATEWAY_BASEURL=http://127.0.0.1:8000/v1";
|
|
6
|
+
function declared(url) {
|
|
7
|
+
return url !== undefined && url.trim() !== "" ? url : undefined;
|
|
8
|
+
}
|
|
9
|
+
export function openaiRouteBaseUrl(routeBaseUrl, gatewayBaseUrl) {
|
|
10
|
+
return declared(routeBaseUrl) ?? declared(gatewayBaseUrl);
|
|
11
|
+
}
|
|
12
|
+
export function resolveOpenaiRouteEndpoint(routeName, routeBaseUrl, gatewayBaseUrl) {
|
|
13
|
+
const baseUrl = openaiRouteBaseUrl(routeBaseUrl, gatewayBaseUrl);
|
|
14
|
+
return baseUrl !== undefined ? { ok: true, baseUrl } : { ok: false, refusal: routeEndpointMissingRefusal([`"${routeName}"`]) };
|
|
15
|
+
}
|
|
16
|
+
export function modelRouteEndpointRefusal(view) {
|
|
17
|
+
const gateway = declared(view.gatewayBaseUrl);
|
|
18
|
+
if (gateway === undefined && view.gatewayFallbackUrls.length > 0) {
|
|
19
|
+
return (`env MODEL_GATEWAY_FALLBACK_URLS is set but MODEL_GATEWAY_BASEURL is not [${GATEWAY_FALLBACK_WITHOUT_PRIMARY_CODE}] — ` +
|
|
20
|
+
"the fallback URLs are mirrors of the primary gateway (same provider, same account, same key scope), tried only " +
|
|
21
|
+
"after the primary fails; with no primary there is nothing to fail over from, and MODEL_GATEWAY_BASEURL has no " +
|
|
22
|
+
`factory default. Set MODEL_GATEWAY_BASEURL to the primary gateway (for one on this machine: ${LOCAL_GATEWAY_HINT}), ` +
|
|
23
|
+
"or unset MODEL_GATEWAY_FALLBACK_URLS.");
|
|
24
|
+
}
|
|
25
|
+
const anthropicMounted = view.anthropic !== undefined;
|
|
26
|
+
const entries = Object.entries(view.models ?? {});
|
|
27
|
+
const unresolved = [];
|
|
28
|
+
for (const [name, m] of entries) {
|
|
29
|
+
if (name === DEFAULT_MODEL_REF && entries.some(([k, other]) => k !== name && other === m))
|
|
30
|
+
continue;
|
|
31
|
+
if (dispatchBrainLane(m.provider, anthropicMounted) !== "gateway")
|
|
32
|
+
continue;
|
|
33
|
+
if (openaiRouteBaseUrl(m.baseUrl, gateway) !== undefined)
|
|
34
|
+
continue;
|
|
35
|
+
unresolved.push(m.provider === "anthropic"
|
|
36
|
+
? `"${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)`
|
|
37
|
+
: `"${name}"`);
|
|
38
|
+
}
|
|
39
|
+
return unresolved.length > 0 ? routeEndpointMissingRefusal(unresolved) : undefined;
|
|
40
|
+
}
|
|
41
|
+
function routeEndpointMissingRefusal(quotedNames) {
|
|
42
|
+
const one = quotedNames.length === 1;
|
|
43
|
+
const subject = one ? `model ${quotedNames[0]} rides` : `models ${quotedNames.join(", ")} ride`;
|
|
44
|
+
return (`${subject} the openai-compatible lane but ${one ? "resolves" : "resolve"} to no endpoint ` +
|
|
45
|
+
`[${MODEL_ROUTE_ENDPOINT_MISSING_CODE}] — ${one ? "it has no" : "none has a"} baseUrl of its own, and MODEL_GATEWAY_BASEURL is unset. ` +
|
|
46
|
+
"MODEL_GATEWAY_BASEURL has no factory default: the former silent placeholder (this machine's port 8000) let such a " +
|
|
47
|
+
"deployment start fine and then fail on the first model call, far from the cause (\"fetch failed … ECONNREFUSED\", or " +
|
|
48
|
+
"a container dialling its own loopback when compose passes an empty value). " +
|
|
49
|
+
`Set it explicitly — for a gateway on this machine: ${LOCAL_GATEWAY_HINT} — or give ${one ? "that model" : "each named model"} its own baseUrl in the catalog. ` +
|
|
50
|
+
"(A model defined by MODEL_ID / MODEL_CHEAP_ID has no baseUrl of its own: it needs the gateway, or MODEL_PROVIDER=anthropic " +
|
|
51
|
+
"with ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN. It is judged at boot even when a catalog will replace it later — it is what this " +
|
|
52
|
+
"process serves whenever no catalog lands: the config source unreachable, a refused candidate, or zero enabled models — unless " +
|
|
53
|
+
"CONFIG_REQUIRE_ROSTER=true on a remote registry declares it a placeholder that is never served.)");
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=model-route-endpoint.js.map
|