@lqc123qwe/car-runtime 1.13.0 → 1.14.1

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.
@@ -0,0 +1,261 @@
1
+ /**
2
+ * 1.14-S4 · K-POOL-1 模型池网关(D-42:登记 + health 探测 + fallback 链 + retry 策略 + routing/decide 落链)
3
+ *
4
+ * 口径:
5
+ * - 候选序 = priority 升序(缺省 0)→ weight 降序(缺省 0;D-42:weight 仅作排序 tie-break,
6
+ * 不做负载均衡调度)→ 登记序(稳定排序);
7
+ * - health 预检(doctor 面同款口径——值不打印):GET {baseUrl}/models(openai)/
8
+ * {baseUrl}/v1/models(anthropic);**任意 HTTP 状态码 = 传输可达(健康)**——404/401 ≠ 不健康
9
+ * (真实连通性以发消息为准,doctor 先例);网络层失败/超时 = 不健康(按序跳过,fail-visible
10
+ * 落 routing/decide attempts);结果 TTL 缓存(防死主端点每请求烧重试预算);probeImpl 测试注入缝;
11
+ * - fallback 链 = **首块前失败才可降级**(已消费 chunk 不可重放——llm.ts §3.5.4 同红线):
12
+ * 成员请求抛错(网络/429/5xx 重试耗尽/凭据缺失)或首 chunk 即 error chunk(4xx 业务错,零内容
13
+ * 消费)→ 按序降级下一候选;流中途失败 = error chunk 原样收口不降级;
14
+ * - retry 策略 = 成员级重试(§3.5.4 基线,装配层透传)+ 池级 maxAttempts 候选请求尝试上限
15
+ * (缺省 = 端点数;耗尽 = providerUnreachable B080001——runTurn 以 error 收口);
16
+ * - **routing/decide 审计事件**:每次决策一条(payload.type='decide':selected/reason/retries/
17
+ * attempts)——onDecide 回调由宿主接线 SessionLog.append(事件链只追加红线;禁静默换模型);
18
+ * retries = 发起请求且失败的候选数(健康跳过不计,attempts 全量在册);
19
+ * - 成员请求 req.model/maxTokens 按成员改写(成员适配器消费自身 model——usage 按成员模型落账);
20
+ * - 无池配置 = 行为与 1.13 逐字节一致(兼容验收线——池路由零介入);
21
+ * - 明文凭据红线:成员凭据仅收 keyEnv env 变量名引用(config schema 明文键显式拒绝,CR-05)。
22
+ */
23
+ import { request as httpRequest } from 'node:http';
24
+ import { request as httpsRequest } from 'node:https';
25
+ import { createAnthropicAdapter, createOpenAICompatAdapter } from '../runtime-core/llm.js';
26
+ import { providerUnreachable } from '../runtime-core/errors.js';
27
+ /** 候选序:priority 升序(缺省 0)→ weight 降序(缺省 0,D-42 tie-break)→ 登记序(稳定) */
28
+ export function orderPoolMembers(members) {
29
+ return members
30
+ .map((m, i) => ({ m, i }))
31
+ .sort((a, b) => (a.m.priority ?? 0) - (b.m.priority ?? 0) || (b.m.weight ?? 0) - (a.m.weight ?? 0) || a.i - b.i)
32
+ .map(x => x.m);
33
+ }
34
+ /** config modelPool → 池成员(id 缺省 ep-1..N 派生;重复 id 显式拒绝;候选序就位) */
35
+ export function resolveModelPoolMembers(pool) {
36
+ const seen = new Set();
37
+ const members = [];
38
+ for (const [i, ep] of pool.endpoints.entries()) {
39
+ const id = ep.id ?? `ep-${i + 1}`;
40
+ if (seen.has(id)) {
41
+ return { members: [], error: `CAR-E-CONFIG: 模型池端点 id "${id}" 重复(routing/decide 落链定位歧义禁入)` };
42
+ }
43
+ seen.add(id);
44
+ members.push({
45
+ id,
46
+ baseUrl: ep.baseUrl,
47
+ model: ep.model,
48
+ adapterId: ep.adapterId ?? 'openai-compat',
49
+ ...(ep.keyEnv !== undefined ? { keyEnv: ep.keyEnv } : {}),
50
+ ...(ep.maxTokens !== undefined ? { maxTokens: ep.maxTokens } : {}),
51
+ ...(ep.priority !== undefined ? { priority: ep.priority } : {}),
52
+ ...(ep.weight !== undefined ? { weight: ep.weight } : {}),
53
+ });
54
+ }
55
+ return { members: orderPoolMembers(members) };
56
+ }
57
+ /** node:http(s) GET 状态码(1.8-BUG-1 出站通道口径——全局 fetch/undici win32 退出断言预防) */
58
+ function httpGetStatus(url, signal) {
59
+ return new Promise((resolve, reject) => {
60
+ try {
61
+ const u = new URL(url);
62
+ const mod = u.protocol === 'https:' ? httpsRequest : httpRequest;
63
+ const req = mod(u, { method: 'GET', headers: { accept: 'application/json' } }, res => {
64
+ res.resume(); // 排干 body——连接收口
65
+ resolve(res.statusCode ?? 0);
66
+ });
67
+ const onAbort = () => { try {
68
+ req.destroy(new Error('aborted'));
69
+ }
70
+ catch { /* 已完成 */ } };
71
+ if (signal.aborted)
72
+ onAbort();
73
+ else
74
+ signal.addEventListener('abort', onAbort, { once: true });
75
+ req.on('error', reject);
76
+ req.end();
77
+ }
78
+ catch (e) {
79
+ reject(e);
80
+ }
81
+ });
82
+ }
83
+ /** 单端点 health 探测(doctor/池预检复用;值不打印——只出状态/延迟/类别) */
84
+ export async function probePoolMember(member, opts = {}) {
85
+ const base = member.baseUrl.replace(/\/+$/, '');
86
+ const url = member.adapterId === 'anthropic' ? `${base}/v1/models` : `${base}/models`;
87
+ const t0 = Date.now();
88
+ const controller = new AbortController();
89
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 4000);
90
+ try {
91
+ const status = await httpGetStatus(url, controller.signal);
92
+ return { ok: true, status, latencyMs: Date.now() - t0 };
93
+ }
94
+ catch (e) {
95
+ const aborted = e.name === 'AbortError' || String(e.message).includes('aborted');
96
+ return { ok: false, latencyMs: Date.now() - t0, errorCategory: aborted ? 'timeout' : 'network-error' };
97
+ }
98
+ finally {
99
+ clearTimeout(timer);
100
+ }
101
+ }
102
+ const errorCategory = (e) => {
103
+ const err = e;
104
+ return err.slug ?? err.name ?? 'unknown';
105
+ };
106
+ export function createModelPoolAdapter(opts) {
107
+ const ordered = opts.members;
108
+ const maxAttempts = opts.maxAttempts ?? ordered.length;
109
+ const probeEnabled = opts.probe?.enabled !== false;
110
+ const probeFn = opts.probe?.probeImpl ?? probePoolMember;
111
+ const probeTimeoutMs = opts.probe?.timeoutMs ?? 4000;
112
+ const ttlMs = opts.probe?.ttlMs ?? 30_000;
113
+ const healthCache = new Map();
114
+ const id = opts.id ?? 'model-pool';
115
+ const healthOf = async (m) => {
116
+ const hit = healthCache.get(m.id);
117
+ if (hit && Date.now() - hit.at < ttlMs) {
118
+ return { ok: hit.ok, ...(hit.category !== undefined ? { category: hit.category } : {}) };
119
+ }
120
+ const h = await probeFn(m, { timeoutMs: probeTimeoutMs });
121
+ const entry = { ok: h.ok, at: Date.now(), ...(h.errorCategory !== undefined ? { category: h.errorCategory } : {}) };
122
+ healthCache.set(m.id, entry);
123
+ return { ok: entry.ok, ...(entry.category !== undefined ? { category: entry.category } : {}) };
124
+ };
125
+ const withMeta = (d, req) => ({
126
+ ...d,
127
+ ...(req.metadata?.turnId !== undefined ? { turnId: req.metadata.turnId } : {}),
128
+ ...(req.metadata?.sessionId !== undefined ? { sessionId: req.metadata.sessionId } : {}),
129
+ });
130
+ return {
131
+ id,
132
+ members: ordered.map(x => x.member),
133
+ chat(req) {
134
+ return (async function* () {
135
+ const attempts = [];
136
+ let failedCount = 0;
137
+ let requestAttempts = 0;
138
+ for (const [pos, entry] of ordered.entries()) {
139
+ if (requestAttempts >= maxAttempts)
140
+ break;
141
+ if (probeEnabled) {
142
+ const h = await healthOf(entry.member);
143
+ if (!h.ok) {
144
+ attempts.push({ id: entry.member.id, outcome: 'skipped-unhealthy', ...(h.category !== undefined ? { detail: h.category } : {}) });
145
+ continue;
146
+ }
147
+ }
148
+ requestAttempts++;
149
+ // 成员请求:req.model/maxTokens 按成员改写(usage 按成员模型落账)
150
+ let it;
151
+ try {
152
+ it = entry.adapter.chat({
153
+ ...req,
154
+ model: entry.member.model,
155
+ ...(entry.member.maxTokens != null ? { maxTokens: entry.member.maxTokens } : {}),
156
+ })[Symbol.asyncIterator]();
157
+ }
158
+ catch (e) {
159
+ // 适配器同步抛错(契约违规/装配错误)= 候选不可用,降级下一候选
160
+ attempts.push({ id: entry.member.id, outcome: 'failed', detail: errorCategory(e) });
161
+ failedCount++;
162
+ continue;
163
+ }
164
+ try {
165
+ let first;
166
+ try {
167
+ first = await it.next();
168
+ }
169
+ catch (e) {
170
+ // 首块前失败(网络/429/5xx 重试耗尽/凭据缺失)→ 降级下一候选
171
+ attempts.push({ id: entry.member.id, outcome: 'failed', detail: errorCategory(e) });
172
+ failedCount++;
173
+ continue;
174
+ }
175
+ if (first.done || !first.value) {
176
+ attempts.push({ id: entry.member.id, outcome: 'failed', detail: 'empty-stream' });
177
+ failedCount++;
178
+ continue;
179
+ }
180
+ const fc = first.value;
181
+ // 首 chunk 即 error chunk 且零内容消费(4xx 业务错)→ 候选不可用,降级(零 chunk 已出——可重放安全)
182
+ const errorOnly = fc.finishReason === 'error' && fc.delta === undefined && fc.toolCallDelta === undefined;
183
+ if (errorOnly) {
184
+ attempts.push({ id: entry.member.id, outcome: 'failed', detail: fc.error ? 'http-4xx' : 'error-chunk' });
185
+ failedCount++;
186
+ continue;
187
+ }
188
+ // 选中:routing/decide 落链(禁静默换模型——选中端点/原因/重试次数显性化)
189
+ attempts.push({ id: entry.member.id, outcome: 'selected' });
190
+ opts.onDecide?.(withMeta({
191
+ selected: entry.member.id,
192
+ reason: pos === 0 ? 'primary' : 'fallback',
193
+ retries: failedCount,
194
+ attempts,
195
+ }, req));
196
+ yield fc;
197
+ while (true) {
198
+ const r = await it.next();
199
+ if (r.done)
200
+ break;
201
+ yield r.value; // 流中途失败 = error chunk 原样收口(已消费 chunk 不可重放——不降级)
202
+ }
203
+ return;
204
+ }
205
+ finally {
206
+ // 成员迭代器收口(首 chunk 后未耗尽路径——4xx 降级/取消/异常;自然结束路径 no-op)
207
+ await it.return?.(undefined);
208
+ }
209
+ }
210
+ // 耗尽:决策落链 + B080001(runTurn error 收口——BD-04)
211
+ opts.onDecide?.(withMeta({ selected: null, reason: 'exhausted', retries: failedCount, attempts }, req));
212
+ const summary = attempts.map(a => `${a.id}:${a.outcome}${a.detail ? `(${a.detail})` : ''}`).join(', ') || '无候选';
213
+ throw providerUnreachable(`模型池全部候选不可用(${summary})`);
214
+ })();
215
+ },
216
+ };
217
+ }
218
+ /** 池装配:config 解析(候选序/重复拒绝)→ 成员适配器(内置两族 + keyEnv)→ 路由适配器 */
219
+ export function assembleModelPool(opts) {
220
+ const { members, error } = resolveModelPoolMembers(opts.pool);
221
+ if (error)
222
+ throw new Error(error);
223
+ const entries = members.map(m => ({
224
+ member: m,
225
+ adapter: (m.adapterId === 'anthropic'
226
+ ? createAnthropicAdapter({
227
+ baseUrl: m.baseUrl,
228
+ credentials: opts.credentials,
229
+ ...(m.keyEnv !== undefined ? { credentialEnvVars: [m.keyEnv] } : {}),
230
+ ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
231
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
232
+ ...(opts.firstByteMs !== undefined ? { firstByteMs: opts.firstByteMs } : {}),
233
+ ...(opts.retries !== undefined ? { retries: opts.retries } : {}),
234
+ ...(opts.backoffMs !== undefined ? { backoffMs: opts.backoffMs } : {}),
235
+ ...(opts.sleep ? { sleep: opts.sleep } : {}),
236
+ })
237
+ : createOpenAICompatAdapter({
238
+ baseUrl: m.baseUrl,
239
+ credentials: opts.credentials,
240
+ provider: 'openai-compat',
241
+ ...(m.keyEnv !== undefined ? { credentialEnvVars: [m.keyEnv] } : {}),
242
+ ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
243
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
244
+ ...(opts.firstByteMs !== undefined ? { firstByteMs: opts.firstByteMs } : {}),
245
+ ...(opts.retries !== undefined ? { retries: opts.retries } : {}),
246
+ ...(opts.backoffMs !== undefined ? { backoffMs: opts.backoffMs } : {}),
247
+ ...(opts.sleep ? { sleep: opts.sleep } : {}),
248
+ })),
249
+ }));
250
+ const adapter = createModelPoolAdapter({
251
+ members: entries,
252
+ probe: {
253
+ ...(opts.pool.probe?.enabled !== undefined ? { enabled: opts.pool.probe.enabled } : {}),
254
+ ...(opts.pool.probe?.timeoutMs !== undefined ? { timeoutMs: opts.pool.probe.timeoutMs } : {}),
255
+ ...(opts.pool.probe?.ttlMs !== undefined ? { ttlMs: opts.pool.probe.ttlMs } : {}),
256
+ },
257
+ ...(opts.pool.maxAttempts !== undefined ? { maxAttempts: opts.pool.maxAttempts } : {}),
258
+ ...(opts.onDecide ? { onDecide: opts.onDecide } : {}),
259
+ });
260
+ return { adapter, members };
261
+ }
package/dist/loop/stop.js CHANGED
@@ -13,6 +13,7 @@ function turnUsageFrom(log, turnId) {
13
13
  let cacheRead = 0;
14
14
  let cacheCreation = 0;
15
15
  let requests = 0;
16
+ let reasoning;
16
17
  for (const e of log.events) {
17
18
  if (e.kind !== 'usage' || e.turnId !== turnId)
18
19
  continue;
@@ -25,9 +26,16 @@ function turnUsageFrom(log, turnId) {
25
26
  cacheRead += p.cacheRead;
26
27
  if (typeof p.cacheCreation === 'number')
27
28
  cacheCreation += p.cacheCreation;
29
+ // 1.14-S3(K-MP-2b / D-41):分段聚合——任一请求携带 segments.reasoningTokens 即求和;
30
+ // 全 turn 无分段事实 = meta.usage 不带 segments 键(禁估算补造)
31
+ const seg = (p.segments ?? null);
32
+ if (seg && typeof seg.reasoningTokens === 'number')
33
+ reasoning = (reasoning ?? 0) + seg.reasoningTokens;
28
34
  requests++;
29
35
  }
30
- return requests ? { input, output, cacheRead, cacheCreation, requests } : undefined;
36
+ if (!requests)
37
+ return undefined;
38
+ return { input, output, cacheRead, cacheCreation, requests, ...(reasoning !== undefined ? { segments: { reasoningTokens: reasoning } } : {}) };
31
39
  }
32
40
  export async function runTurn(opts) {
33
41
  const { log, turnId, model, tools, preset = {}, signal } = opts;
@@ -1,4 +1,4 @@
1
- {"stage":"archive","detail":"git archive HEAD → car-runtime-1.13.0.tgz(340714 bytes,树哈希锚定)","ts":1791704827001}
2
- {"stage":"sha256sums","detail":"SHA256SUMS 3d4f869b251e48c7…(裸字节复核通过)","ts":1791704827002}
3
- {"stage":"sbom","detail":"51 个组件(运行时依赖 0 项)","ts":1791704827009}
4
- {"stage":"cosign-keyless","detail":"signed car-runtime-1.13.0.tgz (keyless OIDC + Rekor)","ts":1791704828841}
1
+ {"stage":"archive","detail":"git archive HEAD → car-runtime-1.14.1.tgz(373973 bytes,树哈希锚定)","ts":1792313224480}
2
+ {"stage":"sha256sums","detail":"SHA256SUMS 88b55ba110b87ca7…(裸字节复核通过)","ts":1792313224481}
3
+ {"stage":"sbom","detail":"53 个组件(运行时依赖 0 项)","ts":1792313224489}
4
+ {"stage":"cosign-keyless","detail":"signed car-runtime-1.14.1.tgz (keyless OIDC + Rekor)","ts":1792313226954}
@@ -35,6 +35,14 @@ export interface ChatStepOptions {
35
35
  signal?: {
36
36
  aborted: boolean;
37
37
  };
38
+ /**
39
+ * 1.14.1(K-MP-1 消费面 / D-39):思考参数透传——chatStep 是宿主 model 回调的唯一路径,
40
+ * 本形参补齐后 reasoning 才经 chatStep 可达(off / 缺席 = 不下发该字段,字节面零痕迹);
41
+ * 模型元数据思考档位判断在宿主侧(models:meta),内核防线 = 未知档位 fail-visible。
42
+ */
43
+ reasoning?: {
44
+ effort: 'off' | 'low' | 'medium' | 'high';
45
+ };
38
46
  }
39
47
  /** ModelStep 加法扩展:脱敏计数留痕(S15 secretsRedacted 同名口径) */
40
48
  export type ChatStepResult = ModelStep & {
@@ -34,6 +34,8 @@ export async function chatStep(opts) {
34
34
  tools,
35
35
  ...(maxTokens != null ? { maxTokens } : {}),
36
36
  ...(adapterId ? { adapterId } : {}),
37
+ // 1.14.1(K-MP-1 消费面):reasoning 透传(off/缺席 = 不下发)
38
+ ...(opts.reasoning ? { reasoning: opts.reasoning } : {}),
37
39
  ...(opts.signal ? { signal: opts.signal } : {}),
38
40
  ...(opts.log.sessionId ? { metadata: { sessionId: opts.log.sessionId, turnId, traceId: `${opts.log.sessionId}:${turnId}` } } : {}),
39
41
  });
@@ -81,6 +83,8 @@ export async function chatStep(opts) {
81
83
  output: usage.outputTokens,
82
84
  cacheRead: usage.cacheReadTokens,
83
85
  cacheCreation: usage.cacheCreationTokens,
86
+ // 1.14-S3(K-MP-2b / D-41):分段事实随事件落链——无分段 = 不带键(禁估算)
87
+ ...(usage.segments ? { segments: usage.segments } : {}),
84
88
  providerRaw: usage.providerRaw,
85
89
  });
86
90
  }
@@ -16,6 +16,8 @@ export interface CredentialResolveOptions {
16
16
  source: CredentialSource;
17
17
  origin: string;
18
18
  }) => void;
19
+ /** 1.14-S4(K-POOL-1):显式 env 变量候选(modelPool keyEnv 精确引用——替代 provider 缺省表;keychain 链保持在前) */
20
+ envVarCandidates?: string[];
19
21
  }
20
22
  /** provider 候选 env 变量(doctor 存在性探测等诊断面复用;值永不打印) */
21
23
  export declare function providerEnvVars(provider: string): string[];
@@ -83,7 +83,8 @@ export class CredentialService {
83
83
  }
84
84
  const allowEnv = opts.allowEnvFallback === true || env.CAR_ALLOW_ENV_CREDENTIALS === '1';
85
85
  if (allowEnv) {
86
- const candidates = providerEnvVars(provider);
86
+ // 1.14-S4(K-POOL-1):显式候选(keyEnv 精确引用)压过 provider 缺省表——用户点名变量即显式意见
87
+ const candidates = opts.envVarCandidates?.length ? opts.envVarCandidates : providerEnvVars(provider);
87
88
  for (const name of candidates) {
88
89
  searched.push(`env:${name}`);
89
90
  if (env[name]) {
@@ -14,6 +14,8 @@ export declare class AdapterRegistry {
14
14
  isDefault: boolean;
15
15
  }>;
16
16
  }
17
+ /** 归一思考档位:缺席/off → undefined(不下发);low/medium/high 原样;未知值 fail-visible(宿主越型传入防线) */
18
+ export declare function normalizeReasoningEffort(effort: unknown): 'off' | 'low' | 'medium' | 'high' | undefined;
17
19
  /** 首个 finishReason 定格;适配器后续给出不同值 = 契约违规显式抛错(不改写不吞没) */
18
20
  export declare function withFinishReasonGuard(stream: AsyncIterable<LlmChunk>): AsyncIterable<LlmChunk>;
19
21
  export declare class RuntimeCore {
@@ -42,6 +44,8 @@ export interface OpenAICompatOptions {
42
44
  /** 凭据服务与 provider 名(Bearer token 来源) */
43
45
  credentials?: CredentialService;
44
46
  provider?: string;
47
+ /** 1.14-S4(K-POOL-1):成员级凭据 env 变量名引用(modelPool keyEnv——resolve 精确候选;keychain 链保持在前) */
48
+ credentialEnvVars?: string[];
45
49
  fetchImpl?: typeof fetch;
46
50
  /** §3.5.4 基线:总 120s / 首字节 30s / 2 次退避 1s-2s(测试可注入缩短) */
47
51
  timeoutMs?: number;
@@ -70,6 +74,8 @@ export interface AnthropicAdapterOptions {
70
74
  sleep?: (ms: number) => Promise<void>;
71
75
  /** D-20:Anthropic API max_tokens 必填——req.maxTokens 缺席时取此缺省(provider 差异登记,缺省 4096) */
72
76
  defaultMaxTokens?: number;
77
+ /** 1.14-S4(K-POOL-1):成员级凭据 env 变量名引用(modelPool keyEnv——resolve 精确候选;keychain 链保持在前) */
78
+ credentialEnvVars?: string[];
73
79
  }
74
80
  export declare function createAnthropicAdapter(opts: AnthropicAdapterOptions): LlmAdapter & {
75
81
  id: string;
@@ -25,6 +25,11 @@
25
25
  * 解析「先摘 evt.usage 再判 choice」(空 choices 尾包不再被吞);anthropic message_start(input 侧)
26
26
  * + message_delta(output 侧)两包合并(message_delta 缺席 = output 不完整,整条不落);
27
27
  * 归一口径 §1.1(openai 减法 / anthropic 透传 + cache_creation),畸形/缺失整条不落(禁补造)。
28
+ * - 1.14-S1(K-MP-1 / D-39):思考参数透传——LlmRequest.reasoning.effort 按家族映射:
29
+ * openai-compat = `reasoning_effort`;anthropic = `thinking:{type:'enabled',budget_tokens}`
30
+ * (ANTHROPIC_THINKING_BUDGET 表);**off / 缺席 = 字节面不下发**;未知 effort 值显式报错
31
+ * (CAR-E-LLM-REASONING,fail-visible 禁静默忽略——宿主越型传入防线);anthropic 侧
32
+ * budget ≥ max_tokens 违反 API 约束 → 显式报错提示调高(禁静默改写 max_tokens)。
28
33
  */
29
34
  import { providerUnreachable } from './errors.js';
30
35
  import { CredentialService } from './credentials.js';
@@ -67,6 +72,21 @@ export class AdapterRegistry {
67
72
  return [...this.#adapters.keys()].map(id => ({ id, isDefault: id === this.#defaultId }));
68
73
  }
69
74
  }
75
+ // ==================== 1.14-S1(K-MP-1 / D-39)思考参数映射 ====================
76
+ /** 归一思考档位:缺席/off → undefined(不下发);low/medium/high 原样;未知值 fail-visible(宿主越型传入防线) */
77
+ export function normalizeReasoningEffort(effort) {
78
+ if (effort === undefined || effort === null)
79
+ return undefined;
80
+ if (effort === 'off' || effort === 'low' || effort === 'medium' || effort === 'high')
81
+ return effort;
82
+ throw new Error(`CAR-E-LLM-REASONING: 未知思考档位 "${String(effort)}"(合法:off/low/medium/high;off/缺席 = 不下发)`);
83
+ }
84
+ /** anthropic effort → thinking.budget_tokens 映射(D-39 登记表);off/缺席不下发 */
85
+ const ANTHROPIC_THINKING_BUDGET = {
86
+ low: 2048,
87
+ medium: 8192,
88
+ high: 16384,
89
+ };
70
90
  // ==================== finishReason 不可变守卫(SDK 边界) ====================
71
91
  /** 首个 finishReason 定格;适配器后续给出不同值 = 契约违规显式抛错(不改写不吞没) */
72
92
  export async function* withFinishReasonGuard(stream) {
@@ -264,6 +284,9 @@ class Retryable extends Error {
264
284
  * 1.11-S1(D-33 / 计量设计 §1.1):openai usage 归一——prompt_tokens 含 cached,inputTokens
265
285
  * 必须做减法(prompt − prompt_tokens_details.cached_tokens);cache_creation 恒 0(无对应字段,
266
286
  * 登记不估算)。usage 对象缺席/畸形(两侧主字段任一非数值)= 整条不落(缺失口径,禁补造)。
287
+ * 1.14-S3(K-MP-2b / D-41):分段计量——completion_tokens_details.reasoning_tokens(思考输出
288
+ * 细分,provider 真实事实)数值才采(segments.reasoningTokens);details/字段缺席或非数值 =
289
+ * segments 不带键(禁估算补造)。
267
290
  */
268
291
  function normalizeOpenAIUsage(u, model, adapterId) {
269
292
  if (!u || typeof u !== 'object')
@@ -273,6 +296,8 @@ function normalizeOpenAIUsage(u, model, adapterId) {
273
296
  return undefined;
274
297
  const details = (r.prompt_tokens_details ?? null);
275
298
  const cached = details && typeof details.cached_tokens === 'number' ? details.cached_tokens : 0;
299
+ const compDetails = (r.completion_tokens_details ?? null);
300
+ const reasoning = compDetails && typeof compDetails.reasoning_tokens === 'number' ? compDetails.reasoning_tokens : undefined;
276
301
  return {
277
302
  inputTokens: r.prompt_tokens - cached,
278
303
  outputTokens: r.completion_tokens,
@@ -281,6 +306,7 @@ function normalizeOpenAIUsage(u, model, adapterId) {
281
306
  model,
282
307
  adapterId,
283
308
  providerRaw: u,
309
+ ...(reasoning !== undefined ? { segments: { reasoningTokens: reasoning } } : {}),
284
310
  };
285
311
  }
286
312
  /** 明文 SSE → LlmChunk 流解析(增量容错:半行驻留到下一 chunk;1.11-S1:model/adapterId 供 usage 归一) */
@@ -383,6 +409,10 @@ export function createOpenAICompatAdapter(opts) {
383
409
  const firstByteMs = opts.firstByteMs ?? 30_000;
384
410
  const retries = opts.retries ?? 2;
385
411
  const backoffMs = opts.backoffMs ?? [1_000, 2_000];
412
+ // 1.14-S4(K-POOL-1):成员级 keyEnv 精确候选——keyEnv 引用即显式意见(env 通道开启;候选=仅该变量,keychain 链保持在前)
413
+ const credResolveOpts = opts.credentialEnvVars?.length
414
+ ? { envVarCandidates: opts.credentialEnvVars, allowEnvFallback: true }
415
+ : {};
386
416
  const toOpenAI = (req, apiKey) => ({
387
417
  // 1.4-BUG-1:method: 'POST' 缺失——M8 最小实现潜伏缺陷(注入式 fetch 的单测忽略 method,
388
418
  // 真 undici 对 GET+body 直接拒绝);car run 真路径接线时暴露(生产调用点核查产出)
@@ -394,6 +424,11 @@ export function createOpenAICompatAdapter(opts) {
394
424
  body: JSON.stringify({
395
425
  model: req.model,
396
426
  stream: true,
427
+ // 1.14-S1(K-MP-1 / D-39):思考参数——effort 非 off/缺席才下发 reasoning_effort(off = 字节面零痕迹)
428
+ ...(normalizeReasoningEffort(req.reasoning?.effort) !== undefined &&
429
+ normalizeReasoningEffort(req.reasoning?.effort) !== 'off'
430
+ ? { reasoning_effort: normalizeReasoningEffort(req.reasoning?.effort) }
431
+ : {}),
397
432
  // 1.11-S1(D-33 / 计量设计 §9-2 推荐A):usage 采集默认开启;第三方网关不兼容时的
398
433
  // 回退方案(baseUrl 白名单)登记可回退项——不支持网关走缺失口径(禁估算补造)
399
434
  stream_options: { include_usage: true },
@@ -426,7 +461,7 @@ export function createOpenAICompatAdapter(opts) {
426
461
  buildRequest: async () => {
427
462
  let apiKey = '';
428
463
  if (opts.credentials) {
429
- const ref = opts.credentials.resolve(opts.provider ?? id, {});
464
+ const ref = opts.credentials.resolve(opts.provider ?? id, credResolveOpts);
430
465
  apiKey = opts.credentials.reveal(ref, {});
431
466
  }
432
467
  return toOpenAI(req, apiKey);
@@ -457,6 +492,8 @@ function mergeAnthropicUsage(start, delta, model, adapterId) {
457
492
  return undefined;
458
493
  if (typeof start.input_tokens !== 'number' || typeof delta.output_tokens !== 'number')
459
494
  return undefined;
495
+ // 1.14-S3(K-MP-2b / D-41):anthropic 无输出细分事实(thinking 计入 output_tokens)→
496
+ // segments 不带键(禁估算补造——桌面色条退化单色,登记口径)
460
497
  return {
461
498
  inputTokens: start.input_tokens,
462
499
  outputTokens: delta.output_tokens,
@@ -508,6 +545,15 @@ function toAnthropic(req, apiKey) {
508
545
  }
509
546
  }
510
547
  flushToolResults();
548
+ // 1.14-S1(K-MP-1 / D-39):思考参数——effort → thinking.budget_tokens(ANTHROPIC_THINKING_BUDGET 表);
549
+ // off/缺席不下发;budget ≥ max_tokens 违反 Messages API 约束(max_tokens 必须大于 budget_tokens)→
550
+ // 显式报错提示调高(fail-visible,禁静默改写 max_tokens)
551
+ const reasoningEffort = normalizeReasoningEffort(req.reasoning?.effort);
552
+ const thinkingBudget = reasoningEffort && reasoningEffort !== 'off' ? ANTHROPIC_THINKING_BUDGET[reasoningEffort] : undefined;
553
+ const maxTokens = req.maxTokens ?? 4096;
554
+ if (thinkingBudget !== undefined && thinkingBudget >= maxTokens) {
555
+ throw new Error(`CAR-E-LLM-REASONING: thinking budget_tokens(${thinkingBudget}) 必须小于 max_tokens(${maxTokens})——请调高 maxTokens(禁静默改写请求参数)`);
556
+ }
511
557
  return {
512
558
  method: 'POST',
513
559
  headers: {
@@ -518,7 +564,8 @@ function toAnthropic(req, apiKey) {
518
564
  body: JSON.stringify({
519
565
  model: req.model,
520
566
  stream: true,
521
- max_tokens: req.maxTokens ?? 4096,
567
+ max_tokens: maxTokens,
568
+ ...(thinkingBudget !== undefined ? { thinking: { type: 'enabled', budget_tokens: thinkingBudget } } : {}),
522
569
  ...(system.length ? { system: system.join('\n\n') } : {}),
523
570
  messages: turns,
524
571
  ...(req.tools.length
@@ -614,6 +661,10 @@ export function createAnthropicAdapter(opts) {
614
661
  const firstByteMs = opts.firstByteMs ?? 30_000;
615
662
  const retries = opts.retries ?? 2;
616
663
  const backoffMs = opts.backoffMs ?? [1_000, 2_000];
664
+ // 1.14-S4(K-POOL-1):成员级 keyEnv 精确候选(同 openai-compat 口径)
665
+ const credResolveOpts = opts.credentialEnvVars?.length
666
+ ? { envVarCandidates: opts.credentialEnvVars, allowEnvFallback: true }
667
+ : {};
617
668
  return {
618
669
  id,
619
670
  chat(req) {
@@ -624,7 +675,7 @@ export function createAnthropicAdapter(opts) {
624
675
  buildRequest: async () => {
625
676
  let apiKey = '';
626
677
  if (opts.credentials) {
627
- const ref = opts.credentials.resolve(opts.provider ?? id, {});
678
+ const ref = opts.credentials.resolve(opts.provider ?? id, credResolveOpts);
628
679
  apiKey = opts.credentials.reveal(ref, {});
629
680
  }
630
681
  return toAnthropic(req, apiKey);
@@ -32,6 +32,12 @@ export interface LlmRequest {
32
32
  signal?: {
33
33
  aborted: boolean;
34
34
  };
35
+ /** 1.14-S1(K-MP-1 / D-39):思考参数——适配器按家族映射(openai-compat = reasoning_effort;
36
+ * anthropic = thinking.budget_tokens);**off / 缺席 = 不下发该字段**(字节面零痕迹,非「effort:none」);
37
+ * 模型思考档位(模型级思考如 R1)为宿主侧模型元数据事实源(桌面 models:meta),内核不做档位判断 */
38
+ reasoning?: {
39
+ effort: 'off' | 'low' | 'medium' | 'high';
40
+ };
35
41
  }
36
42
  /** 工具调用流式增量(跨 chunk 聚合出完整 ToolCall) */
37
43
  export interface ToolCallDelta {
@@ -58,6 +64,15 @@ export interface LlmUsage {
58
64
  adapterId: string;
59
65
  /** provider 原始 usage 对象(anthropic 为 messageStart/messageDelta 两原始片段) */
60
66
  providerRaw: unknown;
67
+ /**
68
+ * 1.14-S3(K-MP-2b / D-41):请求分段计量——仅采 provider 真实返回的分段事实(归一化后),
69
+ * **无分段事实 = 不带键(禁静态估算,桌面色条退化单色不造假)**。v1 分段 = reasoningTokens
70
+ * (openai completion_tokens_details.reasoning_tokens——K-MP-1 思考参数的计量配套,thinking
71
+ * 输出细分;anthropic thinking 计入 output_tokens 无细分事实 → 缺省不带键,登记)。
72
+ */
73
+ segments?: {
74
+ reasoningTokens?: number;
75
+ };
61
76
  }
62
77
  /** 流式块(finishReason 一经产生不可被任何中间层改写) */
63
78
  export interface LlmChunk {