@lqc123qwe/car-runtime 1.9.0 → 1.10.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/README.md +17 -1
- package/dist/SHA256SUMS +1 -0
- package/dist/authz/authz.d.ts +44 -0
- package/dist/authz/authz.js +65 -0
- package/dist/car-runtime-1.10.0.tgz +0 -0
- package/dist/car-runtime-1.10.0.tgz.sig.bundle +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +825 -0
- package/dist/dx/doctor.d.ts +62 -0
- package/dist/dx/doctor.js +127 -0
- package/dist/governance/dump.d.ts +47 -0
- package/dist/governance/dump.js +17 -0
- package/dist/host/facade.d.ts +13 -0
- package/dist/host/facade.js +154 -0
- package/dist/host/hostGateway.d.ts +117 -0
- package/dist/host/hostGateway.js +65 -0
- package/dist/host/mappings.d.ts +27 -0
- package/dist/host/mappings.js +52 -0
- package/dist/host/stdio.d.ts +14 -0
- package/dist/host/stdio.js +83 -0
- package/dist/kernel/context.d.ts +76 -0
- package/dist/kernel/context.js +203 -0
- package/dist/kernel/events.d.ts +37 -0
- package/dist/kernel/events.js +82 -0
- package/dist/load/config.d.ts +79 -0
- package/dist/load/config.js +285 -0
- package/dist/load/loader.d.ts +72 -0
- package/dist/load/loader.js +162 -0
- package/dist/load/registry.d.ts +44 -0
- package/dist/load/registry.js +58 -0
- package/dist/load/report.d.ts +98 -0
- package/dist/load/report.js +301 -0
- package/dist/load/sigGate.d.ts +22 -0
- package/dist/load/sigGate.js +68 -0
- package/dist/load/sign.d.ts +25 -0
- package/dist/load/sign.js +47 -0
- package/dist/load/verifier.d.ts +35 -0
- package/dist/load/verifier.js +56 -0
- package/dist/loop/goal.d.ts +31 -0
- package/dist/loop/goal.js +34 -0
- package/dist/loop/recover.d.ts +17 -0
- package/dist/loop/recover.js +39 -0
- package/dist/loop/stop.d.ts +79 -0
- package/dist/loop/stop.js +151 -0
- package/dist/mcp/gateway.d.ts +96 -0
- package/dist/mcp/gateway.js +69 -0
- package/dist/mcp/httpClient.d.ts +27 -0
- package/dist/mcp/httpClient.js +196 -0
- package/dist/mcp/stdioClient.d.ts +8 -0
- package/dist/mcp/stdioClient.js +73 -0
- package/dist/ptc/budget.d.ts +21 -0
- package/dist/ptc/budget.js +36 -0
- package/dist/ptc/erasable.d.ts +14 -0
- package/dist/ptc/erasable.js +27 -0
- package/dist/ptc/runCode.d.ts +65 -0
- package/dist/ptc/runCode.js +140 -0
- package/dist/ptc/schemaRender.d.ts +15 -0
- package/dist/ptc/schemaRender.js +90 -0
- package/dist/ptc/sdk.d.ts +27 -0
- package/dist/ptc/sdk.js +23 -0
- package/dist/ptc/worker-entry.d.ts +1 -0
- package/dist/ptc/worker-entry.js +71 -0
- package/dist/release-audit.jsonl +4 -0
- package/dist/runtime-core/chatStep.d.ts +40 -0
- package/dist/runtime-core/chatStep.js +104 -0
- package/dist/runtime-core/credentials.d.ts +49 -0
- package/dist/runtime-core/credentials.js +118 -0
- package/dist/runtime-core/errors.d.ts +30 -0
- package/dist/runtime-core/errors.js +36 -0
- package/dist/runtime-core/llm.d.ts +76 -0
- package/dist/runtime-core/llm.js +553 -0
- package/dist/runtime-core/redaction.d.ts +39 -0
- package/dist/runtime-core/redaction.js +79 -0
- package/dist/runtime-core/telemetry.d.ts +88 -0
- package/dist/runtime-core/telemetry.js +332 -0
- package/dist/runtime-core/tools.d.ts +15 -0
- package/dist/runtime-core/tools.js +12 -0
- package/dist/runtime-core/trace.d.ts +55 -0
- package/dist/runtime-core/trace.js +137 -0
- package/dist/runtime-core/types.d.ts +84 -0
- package/dist/runtime-core/types.js +13 -0
- package/dist/sandbox/sandbox.d.ts +68 -0
- package/dist/sandbox/sandbox.js +153 -0
- package/dist/sbom.json +263 -0
- package/dist/seam.d.ts +21 -0
- package/dist/seam.js +20 -0
- package/dist/security/secrets-baseline.d.ts +43 -0
- package/dist/security/secrets-baseline.js +176 -0
- package/dist/security/secrets.d.ts +45 -0
- package/dist/security/secrets.js +138 -0
- package/dist/session/export.d.ts +55 -0
- package/dist/session/export.js +81 -0
- package/dist/session/fork.d.ts +51 -0
- package/dist/session/fork.js +63 -0
- package/dist/session/format.d.ts +21 -0
- package/dist/session/format.js +49 -0
- package/dist/session/indexStore.d.ts +44 -0
- package/dist/session/indexStore.js +265 -0
- package/dist/session/log.d.ts +79 -0
- package/dist/session/log.js +157 -0
- package/dist/session/store.d.ts +9 -0
- package/dist/session/store.js +37 -0
- package/dist/telemetry/metrics.d.ts +25 -0
- package/dist/telemetry/metrics.js +26 -0
- package/package.json +17 -1
- package/src/seam.ts +21 -0
- package/.gitattributes +0 -11
- package/.github/workflows/ci.yml +0 -293
- package/.github/workflows/release.yml +0 -54
- package/adr/ADR-001.md +0 -4
- package/adr/ADR-002.md +0 -5
- package/adr/ADR-003.md +0 -35
- package/adr/host-onboarding.md +0 -68
- package/adr/mlps-audit-checklist.md +0 -41
- package/adr/win32-spike.md +0 -45
- package/docs/plugin-authoring.md +0 -82
- package/native/landlock-run/Makefile +0 -19
- package/native/landlock-run/landlock-run.c +0 -162
- package/scripts/contract-check.ts +0 -56
- package/scripts/invariant-n1.ts +0 -96
- package/scripts/package-check.ts +0 -29
- package/scripts/ptc-baseline/run-baseline.ts +0 -118
- package/scripts/release-artifacts.ts +0 -111
- package/scripts/release-pipeline.ts +0 -245
- package/scripts/secrets-baseline.ts +0 -33
- package/scripts/secrets-scan.ts +0 -56
- package/scripts/win32-ffi-debug.ts +0 -143
- package/scripts/win32-koffi-spike.ts +0 -537
- package/test/contract/eventmap.golden.json +0 -7
- package/test/fixtures/hello-plugin.ts +0 -5
- package/test/kernel.spec.ts +0 -136
- package/test/registry-e2e/run-e2e.ts +0 -213
- package/test/s11.spec.ts +0 -129
- package/test/s12.spec.ts +0 -135
- package/test/s13.spec.ts +0 -117
- package/test/s14.spec.ts +0 -117
- package/test/s15.spec.ts +0 -80
- package/test/s16.spec.ts +0 -133
- package/test/s17.spec.ts +0 -102
- package/test/s18.spec.ts +0 -81
- package/test/s19.spec.ts +0 -138
- package/test/s2.spec.ts +0 -245
- package/test/s20.spec.ts +0 -241
- package/test/s21.spec.ts +0 -231
- package/test/s22-m7-store-format.spec.ts +0 -135
- package/test/s23-m7-export-index.spec.ts +0 -189
- package/test/s24-signature-gate.spec.ts +0 -185
- package/test/s25-m8-runtime-core.spec.ts +0 -600
- package/test/s26-signature-closeout.spec.ts +0 -349
- package/test/s27-host-load-wiring.spec.ts +0 -134
- package/test/s28-session-index-followup.spec.ts +0 -236
- package/test/s29-otel-producer.spec.ts +0 -252
- package/test/s3.spec.ts +0 -179
- package/test/s30-release-artifacts.spec.ts +0 -127
- package/test/s31-real-model-wiring.spec.ts +0 -329
- package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
- package/test/s33-anthropic-adapter.spec.ts +0 -354
- package/test/s34-mcp-remote-transport.spec.ts +0 -250
- package/test/s35-config-ts-zod.spec.ts +0 -145
- package/test/s36-host-tool-surface.spec.ts +0 -250
- package/test/s37-projection-dedupe.spec.ts +0 -161
- package/test/s38-config-jiti.spec.ts +0 -116
- package/test/s39-otlp-span-semantics.spec.ts +0 -100
- package/test/s4.spec.ts +0 -169
- package/test/s40-metrics-cumulative.spec.ts +0 -67
- package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
- package/test/s42-traceparent-propagation.spec.ts +0 -415
- package/test/s43-remote-http-traceparent.spec.ts +0 -262
- package/test/s5.spec.ts +0 -95
- package/test/s6.spec.ts +0 -169
- package/test/s7.spec.ts +0 -136
- package/test/s7b-baseline.spec.ts +0 -20
- package/test/s9.spec.ts +0 -89
- package/test/sandbox-escape/j17-orchestrate.sh +0 -30
- package/test/sandbox-escape/run-matrix.sh +0 -98
- package/tsconfig.json +0 -18
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { CredentialService } from './credentials.ts';
|
|
2
|
+
import type { Disposable, LlmAdapter, LlmChunk, LlmRequest } from './types.ts';
|
|
3
|
+
import type { PluginContext } from '../kernel/context.ts';
|
|
4
|
+
export declare class AdapterRegistry {
|
|
5
|
+
#private;
|
|
6
|
+
/** 注册(幂等语义:同 id 重复注册显式报错——冲突链定位风格) */
|
|
7
|
+
register(adapter: LlmAdapter, opts?: {
|
|
8
|
+
default?: boolean;
|
|
9
|
+
}): Disposable;
|
|
10
|
+
/** 解析:显式 id > 默认适配器;无可用适配器显式报错(禁静默降级) */
|
|
11
|
+
get(id?: string): LlmAdapter;
|
|
12
|
+
list(): Array<{
|
|
13
|
+
id: string;
|
|
14
|
+
isDefault: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
/** 首个 finishReason 定格;适配器后续给出不同值 = 契约违规显式抛错(不改写不吞没) */
|
|
18
|
+
export declare function withFinishReasonGuard(stream: AsyncIterable<LlmChunk>): AsyncIterable<LlmChunk>;
|
|
19
|
+
export declare class RuntimeCore {
|
|
20
|
+
#private;
|
|
21
|
+
readonly registry: AdapterRegistry;
|
|
22
|
+
readonly credentials: CredentialService;
|
|
23
|
+
constructor(credentials?: CredentialService);
|
|
24
|
+
/** 绑定插件作用域:服务注册 + 适配器注册进入 Effect 可逆通道(宿主传 Context——根作用域 effect 同纪律) */
|
|
25
|
+
bindContext(ctx: Pick<PluginContext, 'provide' | 'effect'>): void;
|
|
26
|
+
/** §3.2.M8.2:registerLlmAdapter(adapter): Disposable——Effect 可逆(卸载回卷后可重注册) */
|
|
27
|
+
registerLlmAdapter(adapter: LlmAdapter, opts?: {
|
|
28
|
+
default?: boolean;
|
|
29
|
+
}): Disposable;
|
|
30
|
+
/** §3.2.M8.2:chat(req): AsyncIterable<LlmChunk>——统一入口 + finishReason 守卫 */
|
|
31
|
+
chat(req: LlmRequest): AsyncIterable<LlmChunk>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* D-12a TLS 强制门(openai-compat / anthropic 共用,口径不变):https 直接通过;
|
|
35
|
+
* http 仅豁免 loopback——本地模型服务(Ollama 等)与本地 E2E;非回环地址维持 TLS 强制
|
|
36
|
+
* (§3.2.M8.4)。豁免集显式列举,不扩大到局域网。
|
|
37
|
+
*/
|
|
38
|
+
export declare function assertTlsOrLoopback(baseUrl: string): void;
|
|
39
|
+
export interface OpenAICompatOptions {
|
|
40
|
+
id?: string;
|
|
41
|
+
baseUrl: string;
|
|
42
|
+
/** 凭据服务与 provider 名(Bearer token 来源) */
|
|
43
|
+
credentials?: CredentialService;
|
|
44
|
+
provider?: string;
|
|
45
|
+
fetchImpl?: typeof fetch;
|
|
46
|
+
/** §3.5.4 基线:总 120s / 首字节 30s / 2 次退避 1s-2s(测试可注入缩短) */
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
firstByteMs?: number;
|
|
49
|
+
retries?: number;
|
|
50
|
+
backoffMs?: number[];
|
|
51
|
+
/** 测试注入睡眠(退避可观测;缺省真实定时器) */
|
|
52
|
+
sleep?: (ms: number) => Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
export declare function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter & {
|
|
55
|
+
id: string;
|
|
56
|
+
};
|
|
57
|
+
export interface AnthropicAdapterOptions {
|
|
58
|
+
id?: string;
|
|
59
|
+
/** API 源(如 https://api.anthropic.com;请求路径 = {baseUrl}/v1/messages,与官方 SDK base_url 同约定) */
|
|
60
|
+
baseUrl: string;
|
|
61
|
+
/** 凭据服务与 provider 名(x-api-key 来源;provider 缺省 'anthropic' → ANTHROPIC_API_KEY) */
|
|
62
|
+
credentials?: CredentialService;
|
|
63
|
+
provider?: string;
|
|
64
|
+
fetchImpl?: typeof fetch;
|
|
65
|
+
/** §3.5.4 基线:总 120s / 首字节 30s / 2 次退避 1s-2s(与 openai-compat 同款) */
|
|
66
|
+
timeoutMs?: number;
|
|
67
|
+
firstByteMs?: number;
|
|
68
|
+
retries?: number;
|
|
69
|
+
backoffMs?: number[];
|
|
70
|
+
sleep?: (ms: number) => Promise<void>;
|
|
71
|
+
/** D-20:Anthropic API max_tokens 必填——req.maxTokens 缺席时取此缺省(provider 差异登记,缺省 4096) */
|
|
72
|
+
defaultMaxTokens?: number;
|
|
73
|
+
}
|
|
74
|
+
export declare function createAnthropicAdapter(opts: AnthropicAdapterOptions): LlmAdapter & {
|
|
75
|
+
id: string;
|
|
76
|
+
};
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M8 · LLM 适配面(chat / registerLlmAdapter ——《系统设计》§3.2.M8.2 模型与适配行 / SQ-07 / O-11)
|
|
3
|
+
*
|
|
4
|
+
* 口径:
|
|
5
|
+
* - 适配器为 ACL(C-02):统一 LlmAdapter 签名消解 Provider 差异;注册经 ctx.effect 可逆
|
|
6
|
+
* (disposeRuntime 回卷后可重注册——注册即逆变换,POC-2 红线同源);重复 id 显式报错禁静默覆盖;
|
|
7
|
+
* - finishReason 不可变透传(ADR-001 / SR-17):SDK 边界流包装做防御性断言——首个
|
|
8
|
+
* finishReason 定格,冲突 = 适配器契约违规显式抛错(改写/吞没均禁止,缺失触发 AL-05 口径);
|
|
9
|
+
* - 重试基线(§3.5.4):仅网络错误/429/5xx、≤2 次、指数退避 1s/2s;**首块前失败才可重试**
|
|
10
|
+
* (已消费的 chunk 不可重放——重试不产生额外副作用);重试耗尽 = CarM8Error B080001
|
|
11
|
+
* (BD-04 error 收口由消费方 runTurn 落 turnEnd);流中途失败 = finishReason 'error' chunk;
|
|
12
|
+
* - 超时基线:总 120s / 首字节 30s(均可注入缩短供测试);TLS 强制(https-only,D-12a
|
|
13
|
+
* loopback 豁免共用 assertTlsOrLoopback);重试/超时/取消引擎单点共用(1.5-S1 streamSsePost
|
|
14
|
+
* ——openai-compat / anthropic 语义不漂移,parse 为唯一 provider 差异点);
|
|
15
|
+
* - declaredSideEffect 不出站(权限面只进权限门,不进模型请求体);
|
|
16
|
+
* - 1.5-S1(D-20):内置 anthropic 适配器(Messages API 流式;max_tokens 必填缺省 4096;
|
|
17
|
+
* system 顶层化;凭据 provider='anthropic')。
|
|
18
|
+
* - 1.8-BUG-1:出站缺省通道 node:http(s)(nodeHttpSseFetch——1.4-BUG-2 口径延伸到 LLM 面):
|
|
19
|
+
* 全局 fetch/undici socket 在 win32 进程退出时触发 libuv 断言崩溃(UV_HANDLE_CLOSING,
|
|
20
|
+
* 0xC0000409)——car run 真路径 ≥3 次模型请求 + PTC worker/MCP 子进程并存即确定性复现
|
|
21
|
+
* (1.4 彼时仅修遥测出站,LLM 出站同为 undici 默认——潜伏至 1.8 E2E 三请求形态暴露);
|
|
22
|
+
* 流式契约 body = AsyncIterable<Uint8Array> 不变;测试注入缝 fetchImpl 保留。
|
|
23
|
+
*/
|
|
24
|
+
import { providerUnreachable } from './errors.js';
|
|
25
|
+
import { CredentialService } from './credentials.js';
|
|
26
|
+
import { request as httpRequest } from 'node:http';
|
|
27
|
+
import { request as httpsRequest } from 'node:https';
|
|
28
|
+
// ==================== 适配器注册表(registerLlmAdapter) ====================
|
|
29
|
+
export class AdapterRegistry {
|
|
30
|
+
#adapters = new Map();
|
|
31
|
+
#defaultId = null;
|
|
32
|
+
/** 注册(幂等语义:同 id 重复注册显式报错——冲突链定位风格) */
|
|
33
|
+
register(adapter, opts = {}) {
|
|
34
|
+
if (this.#adapters.has(adapter.id)) {
|
|
35
|
+
throw new Error(`CAR-E-LLM-DUP: LLM adapter "${adapter.id}" already registered (duplicate registration is blocked)`);
|
|
36
|
+
}
|
|
37
|
+
this.#adapters.set(adapter.id, adapter);
|
|
38
|
+
if (opts.default || this.#defaultId === null)
|
|
39
|
+
this.#defaultId = adapter.id;
|
|
40
|
+
let disposed = false;
|
|
41
|
+
return {
|
|
42
|
+
dispose: () => {
|
|
43
|
+
if (disposed)
|
|
44
|
+
return; // 幂等回卷(二次 dispose 静默收敛,登记口径)
|
|
45
|
+
disposed = true;
|
|
46
|
+
this.#adapters.delete(adapter.id);
|
|
47
|
+
if (this.#defaultId === adapter.id)
|
|
48
|
+
this.#defaultId = this.#adapters.keys().next().value ?? null;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** 解析:显式 id > 默认适配器;无可用适配器显式报错(禁静默降级) */
|
|
53
|
+
get(id) {
|
|
54
|
+
const target = id ?? this.#defaultId;
|
|
55
|
+
if (!target || !this.#adapters.has(target)) {
|
|
56
|
+
const available = [...this.#adapters.keys()].join(', ') || '无';
|
|
57
|
+
throw new Error(`CAR-E-LLM-NOADAPTER: LLM adapter "${target ?? '<default>'}" not registered(在册:${available})`);
|
|
58
|
+
}
|
|
59
|
+
return this.#adapters.get(target);
|
|
60
|
+
}
|
|
61
|
+
list() {
|
|
62
|
+
return [...this.#adapters.keys()].map(id => ({ id, isDefault: id === this.#defaultId }));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ==================== finishReason 不可变守卫(SDK 边界) ====================
|
|
66
|
+
/** 首个 finishReason 定格;适配器后续给出不同值 = 契约违规显式抛错(不改写不吞没) */
|
|
67
|
+
export async function* withFinishReasonGuard(stream) {
|
|
68
|
+
let settled;
|
|
69
|
+
for await (const chunk of stream) {
|
|
70
|
+
if (chunk.finishReason !== undefined) {
|
|
71
|
+
if (settled === undefined)
|
|
72
|
+
settled = chunk.finishReason;
|
|
73
|
+
else if (settled !== chunk.finishReason) {
|
|
74
|
+
throw new Error(`CAR-E-LLM-FINISH: finishReason 已定格为 "${settled}",适配器试图给出 "${chunk.finishReason}"(不可变透传契约违规)`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
yield chunk;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// ==================== RuntimeCore(M8 SDK 面) ====================
|
|
81
|
+
export class RuntimeCore {
|
|
82
|
+
registry = new AdapterRegistry();
|
|
83
|
+
credentials;
|
|
84
|
+
#ctx = null;
|
|
85
|
+
constructor(credentials = new CredentialService()) {
|
|
86
|
+
this.credentials = credentials;
|
|
87
|
+
}
|
|
88
|
+
/** 绑定插件作用域:服务注册 + 适配器注册进入 Effect 可逆通道(宿主传 Context——根作用域 effect 同纪律) */
|
|
89
|
+
bindContext(ctx) {
|
|
90
|
+
this.#ctx = ctx;
|
|
91
|
+
ctx.provide('runtime-core', this);
|
|
92
|
+
}
|
|
93
|
+
/** §3.2.M8.2:registerLlmAdapter(adapter): Disposable——Effect 可逆(卸载回卷后可重注册) */
|
|
94
|
+
registerLlmAdapter(adapter, opts = {}) {
|
|
95
|
+
if (!this.#ctx)
|
|
96
|
+
return this.registry.register(adapter, opts);
|
|
97
|
+
let handle = null;
|
|
98
|
+
this.#ctx.effect(() => {
|
|
99
|
+
handle = this.registry.register(adapter, opts);
|
|
100
|
+
return () => handle?.dispose();
|
|
101
|
+
}, `llm-adapter:${adapter.id}`);
|
|
102
|
+
return { dispose: () => handle?.dispose() };
|
|
103
|
+
}
|
|
104
|
+
/** §3.2.M8.2:chat(req): AsyncIterable<LlmChunk>——统一入口 + finishReason 守卫 */
|
|
105
|
+
chat(req) {
|
|
106
|
+
const adapter = this.registry.get(req.adapterId);
|
|
107
|
+
return withFinishReasonGuard(adapter.chat(req));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// ==================== 适配器共用基线(1.5-S1 抽取:D-12a TLS 门 + §3.5.4 流式引擎) ====================
|
|
111
|
+
/**
|
|
112
|
+
* D-12a TLS 强制门(openai-compat / anthropic 共用,口径不变):https 直接通过;
|
|
113
|
+
* http 仅豁免 loopback——本地模型服务(Ollama 等)与本地 E2E;非回环地址维持 TLS 强制
|
|
114
|
+
* (§3.2.M8.4)。豁免集显式列举,不扩大到局域网。
|
|
115
|
+
*/
|
|
116
|
+
export function assertTlsOrLoopback(baseUrl) {
|
|
117
|
+
if (baseUrl.startsWith('https://'))
|
|
118
|
+
return;
|
|
119
|
+
let loopback = false;
|
|
120
|
+
try {
|
|
121
|
+
const host = new URL(baseUrl).hostname.replace(/^\[|\]$/g, '');
|
|
122
|
+
loopback = host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
|
123
|
+
}
|
|
124
|
+
catch { /* 解析失败 = 非法 URL,按非回环拒绝 */ }
|
|
125
|
+
if (!loopback) {
|
|
126
|
+
throw new Error(`CAR-E-LLM-TLS: baseUrl 必须 https(TLS 强制,§3.2.M8.4);http 仅豁免 loopback(127.0.0.1/[::1]/localhost,D-12a)——远程明文部署请经 TLS 代理或自建适配器`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 1.8-BUG-1:LLM 出站缺省通道——node:http(s) 流式 POST(1.4-BUG-2 同源口径:全局 fetch/undici
|
|
131
|
+
* socket + win32 进程退出触发 libuv 断言崩溃 0xC0000409;详见面头注记)。流式契约:IncomingMessage
|
|
132
|
+
* 本体即 AsyncIterable<Uint8Array>(Buffer 子类);取消经 AbortSignal → req.destroy(流中途 destroy
|
|
133
|
+
* = 迭代器抛错,streamSsePost D-19 路径收口);传输层超时不设——§3.5.4 firstByte/overall 由调用方
|
|
134
|
+
* AbortController 单点承载。
|
|
135
|
+
*/
|
|
136
|
+
const nodeHttpSseFetch = (url, init = {}) => new Promise((resolve, reject) => {
|
|
137
|
+
try {
|
|
138
|
+
const u = new URL(url);
|
|
139
|
+
const mod = u.protocol === 'https:' ? httpsRequest : httpRequest;
|
|
140
|
+
const req = mod(u, { method: init.method ?? 'GET', headers: init.headers }, res => {
|
|
141
|
+
const status = res.statusCode ?? 0;
|
|
142
|
+
resolve({
|
|
143
|
+
ok: status >= 200 && status < 300,
|
|
144
|
+
status,
|
|
145
|
+
text: () => new Promise((res2, rej2) => {
|
|
146
|
+
let s = '';
|
|
147
|
+
res.on('data', c => { s += c.toString('utf-8'); });
|
|
148
|
+
res.on('end', () => res2(s));
|
|
149
|
+
res.on('error', rej2);
|
|
150
|
+
}),
|
|
151
|
+
body: res,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
const onAbort = () => { try {
|
|
155
|
+
req.destroy(new Error('aborted'));
|
|
156
|
+
}
|
|
157
|
+
catch { /* 已完成 */ } };
|
|
158
|
+
if (init.signal) {
|
|
159
|
+
if (init.signal.aborted)
|
|
160
|
+
onAbort();
|
|
161
|
+
else
|
|
162
|
+
init.signal.addEventListener('abort', onAbort, { once: true });
|
|
163
|
+
}
|
|
164
|
+
req.on('error', reject);
|
|
165
|
+
req.end(init.body ?? null);
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
reject(e);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
/**
|
|
172
|
+
* §3.5.4 流式 POST 引擎(openai-compat / anthropic 单点共用,1.5-S1 抽取——两适配器重试/
|
|
173
|
+
* 超时/取消语义不漂移):仅网络错误/429/5xx、≤2 次、指数退避 1s/2s;首块前失败才可重试
|
|
174
|
+
* (已消费 chunk 不可重放);重试耗尽 = CarM8Error B080001;外部取消 ≠ 失败(D-19:静默
|
|
175
|
+
* 收口不 retry 不 error chunk);流自然结束未携带 finishReason = AL-05 显式 error chunk。
|
|
176
|
+
* `parse` 为唯一 provider 差异点(SSE 字节流 → LlmChunk)。
|
|
177
|
+
*/
|
|
178
|
+
async function* streamSsePost(spec, parse) {
|
|
179
|
+
const payload = await spec.buildRequest();
|
|
180
|
+
for (let attempt = 0;; attempt++) {
|
|
181
|
+
// D-19:外部取消 ≠ 失败——attempt 前检查,静默收口(无 chunk、无 retry)
|
|
182
|
+
if (spec.signal?.aborted)
|
|
183
|
+
return;
|
|
184
|
+
const controller = new AbortController();
|
|
185
|
+
const started = Date.now();
|
|
186
|
+
let firstByteSeen = false;
|
|
187
|
+
const firstByteTimer = setTimeout(() => controller.abort(), spec.firstByteMs);
|
|
188
|
+
const overallTimer = setTimeout(() => controller.abort(), spec.timeoutMs);
|
|
189
|
+
try {
|
|
190
|
+
const res = await spec.fetchImpl(spec.url, { ...payload, signal: controller.signal });
|
|
191
|
+
if (res.status === 429 || res.status >= 500)
|
|
192
|
+
throw new Retryable(`HTTP ${res.status}`);
|
|
193
|
+
if (!res.ok) {
|
|
194
|
+
const text = await res.text().catch(() => '');
|
|
195
|
+
// 4xx 业务错不重试(§3.5.4);错误信息不含凭据(凭据值不进错误路径)
|
|
196
|
+
yield { finishReason: 'error', error: { code: 'B080001', message: `provider HTTP ${res.status}${text ? `:${text.slice(0, 200)}` : ''}` } };
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!res.body)
|
|
200
|
+
throw new Retryable('响应无 body 流');
|
|
201
|
+
let sawFinish = false;
|
|
202
|
+
for await (const chunk of parse(res.body)) {
|
|
203
|
+
// D-19:chunk 间隙检查取消——abort 连接并静默收口(不产 error chunk,不违反
|
|
204
|
+
// finishReason 不可变;上层 chatStep 以自身 signal 检查抛 TurnAborted)
|
|
205
|
+
if (spec.signal?.aborted) {
|
|
206
|
+
controller.abort();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!firstByteSeen) {
|
|
210
|
+
firstByteSeen = true;
|
|
211
|
+
clearTimeout(firstByteTimer);
|
|
212
|
+
}
|
|
213
|
+
if (chunk.finishReason !== undefined)
|
|
214
|
+
sawFinish = true;
|
|
215
|
+
yield chunk;
|
|
216
|
+
}
|
|
217
|
+
// 流自然结束但未给 finishReason = 异常终止(AL-05 口径:缺失显式化,禁吞没);
|
|
218
|
+
// 已携带 finishReason 的流原样收口——兜底不得追加(M8-BUG-1 口径延续)
|
|
219
|
+
if (!sawFinish) {
|
|
220
|
+
yield { finishReason: 'error', error: { code: 'B080001', message: '流结束未携带 finish_reason(AL-05)' } };
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
// D-19:外部取消(abort 连锁的 fetch 异常)≠ 失败——静默收口,不走 retry
|
|
226
|
+
if (spec.signal?.aborted)
|
|
227
|
+
return;
|
|
228
|
+
const beforeFirstByte = !firstByteSeen;
|
|
229
|
+
clearTimeout(firstByteTimer);
|
|
230
|
+
// 首块前失败 = 网络错误/429/5xx/首字节超时(§3.5.4 重试条件全集)——未消费任何 chunk
|
|
231
|
+
if (beforeFirstByte && attempt < spec.retries) {
|
|
232
|
+
await spec.sleep(spec.backoffMs[Math.min(attempt, spec.backoffMs.length - 1)]);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (beforeFirstByte) {
|
|
236
|
+
// 重试耗尽 → B080001(BD-04:消费方 runTurn 以 error 收口)
|
|
237
|
+
throw providerUnreachable(`${e.name}: ${e.message}(尝试 ${attempt + 1}/${spec.retries + 1},${Date.now() - started}ms)`);
|
|
238
|
+
}
|
|
239
|
+
// 流中途失败:已消费 chunk 不可重放 → error chunk 收口
|
|
240
|
+
yield { finishReason: 'error', error: { code: 'B080001', message: `流中途失败:${e.message}` } };
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
clearTimeout(firstByteTimer);
|
|
245
|
+
clearTimeout(overallTimer);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const FINISH_MAP = {
|
|
250
|
+
stop: 'stop',
|
|
251
|
+
length: 'length',
|
|
252
|
+
tool_calls: 'toolUse',
|
|
253
|
+
function_call: 'toolUse',
|
|
254
|
+
content_filter: 'aborted',
|
|
255
|
+
};
|
|
256
|
+
class Retryable extends Error {
|
|
257
|
+
}
|
|
258
|
+
/** 明文 SSE → LlmChunk 流解析(增量容错:半行驻留到下一 chunk) */
|
|
259
|
+
async function* parseSseStream(body) {
|
|
260
|
+
const decoder = new TextDecoder();
|
|
261
|
+
let buf = '';
|
|
262
|
+
for await (const bytes of body) {
|
|
263
|
+
buf += decoder.decode(bytes, { stream: true });
|
|
264
|
+
let nl;
|
|
265
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
266
|
+
const line = buf.slice(0, nl).replace(/\r$/, '');
|
|
267
|
+
buf = buf.slice(nl + 1);
|
|
268
|
+
if (!line.startsWith('data:'))
|
|
269
|
+
continue;
|
|
270
|
+
const data = line.slice(5).trim();
|
|
271
|
+
if (data === '[DONE]')
|
|
272
|
+
return;
|
|
273
|
+
if (!data)
|
|
274
|
+
continue;
|
|
275
|
+
let evt;
|
|
276
|
+
try {
|
|
277
|
+
evt = JSON.parse(data);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
continue;
|
|
281
|
+
} // 心跳/注释行容错(显式跳过非 JSON data)
|
|
282
|
+
const choice = evt.choices?.[0];
|
|
283
|
+
if (!choice)
|
|
284
|
+
continue;
|
|
285
|
+
const delta = choice.delta ?? {};
|
|
286
|
+
const raw = choice.finish_reason;
|
|
287
|
+
const finish = raw == null ? undefined : (() => {
|
|
288
|
+
const mapped = FINISH_MAP[String(raw)];
|
|
289
|
+
return mapped
|
|
290
|
+
? mapped
|
|
291
|
+
: 'error'; // 未映射信号 fail-visible(禁静默改写),raw 附在 error.message
|
|
292
|
+
})();
|
|
293
|
+
const toolCallDeltas = Array.isArray(delta.tool_calls)
|
|
294
|
+
? delta.tool_calls.map((tc) => ({
|
|
295
|
+
index: tc.index,
|
|
296
|
+
id: tc.id,
|
|
297
|
+
name: tc.function?.name ?? tc.name,
|
|
298
|
+
argumentsDelta: tc.function?.arguments,
|
|
299
|
+
}))
|
|
300
|
+
: [];
|
|
301
|
+
const text = typeof delta.content === 'string' && delta.content.length ? delta.content : undefined;
|
|
302
|
+
// LlmChunk 单 toolCallDelta 口径:同事件多条 tool_calls 顺序展开为多 chunk
|
|
303
|
+
if (!text && !toolCallDeltas.length && finish === undefined)
|
|
304
|
+
continue;
|
|
305
|
+
if (toolCallDeltas.length === 0) {
|
|
306
|
+
yield {
|
|
307
|
+
...(text !== undefined ? { delta: text } : {}),
|
|
308
|
+
...(finish !== undefined
|
|
309
|
+
? { finishReason: finish, ...(finish === 'error' ? { error: { code: 'B080001', message: `未映射的 finish_reason "${raw}"` } } : {}) }
|
|
310
|
+
: {}),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
for (let i = 0; i < toolCallDeltas.length; i++) {
|
|
315
|
+
const last = i === toolCallDeltas.length - 1;
|
|
316
|
+
yield {
|
|
317
|
+
...(i === 0 && text !== undefined ? { delta: text } : {}),
|
|
318
|
+
toolCallDelta: toolCallDeltas[i],
|
|
319
|
+
...(last && finish !== undefined
|
|
320
|
+
? { finishReason: finish, ...(finish === 'error' ? { error: { code: 'B080001', message: `未映射的 finish_reason "${raw}"` } } : {}) }
|
|
321
|
+
: {}),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
export function createOpenAICompatAdapter(opts) {
|
|
329
|
+
const id = opts.id ?? 'openai-compat';
|
|
330
|
+
const baseUrl = opts.baseUrl.replace(/\/+$/, '');
|
|
331
|
+
// 1.4-S4(D-12a,1.2 规划期裁决):TLS 门 1.5-S1 抽取共用(口径与报错文案不变)
|
|
332
|
+
assertTlsOrLoopback(baseUrl);
|
|
333
|
+
const url = `${baseUrl}/chat/completions`;
|
|
334
|
+
// 1.8-BUG-1:缺省 node:http(s)(undici 退出崩溃——面头注记);注入缝保留
|
|
335
|
+
const fetchImpl = opts.fetchImpl ?? nodeHttpSseFetch;
|
|
336
|
+
const sleep = opts.sleep ?? (ms => new Promise(r => setTimeout(r, ms)));
|
|
337
|
+
const timeoutMs = opts.timeoutMs ?? 120_000;
|
|
338
|
+
const firstByteMs = opts.firstByteMs ?? 30_000;
|
|
339
|
+
const retries = opts.retries ?? 2;
|
|
340
|
+
const backoffMs = opts.backoffMs ?? [1_000, 2_000];
|
|
341
|
+
const toOpenAI = (req, apiKey) => ({
|
|
342
|
+
// 1.4-BUG-1:method: 'POST' 缺失——M8 最小实现潜伏缺陷(注入式 fetch 的单测忽略 method,
|
|
343
|
+
// 真 undici 对 GET+body 直接拒绝);car run 真路径接线时暴露(生产调用点核查产出)
|
|
344
|
+
method: 'POST',
|
|
345
|
+
headers: {
|
|
346
|
+
'content-type': 'application/json',
|
|
347
|
+
authorization: `Bearer ${apiKey}`,
|
|
348
|
+
},
|
|
349
|
+
body: JSON.stringify({
|
|
350
|
+
model: req.model,
|
|
351
|
+
stream: true,
|
|
352
|
+
...(req.maxTokens != null ? { max_tokens: req.maxTokens } : {}),
|
|
353
|
+
messages: req.messages.map(m => {
|
|
354
|
+
if (m.role === 'toolResult') {
|
|
355
|
+
const p = m.content;
|
|
356
|
+
return { role: 'tool', tool_call_id: p?.id, content: p?.error ?? JSON.stringify(p?.result ?? null) };
|
|
357
|
+
}
|
|
358
|
+
if (m.role === 'assistant' && m.content != null && typeof m.content === 'object' && 'toolCall' in m.content) {
|
|
359
|
+
const tc = m.content.toolCall;
|
|
360
|
+
return {
|
|
361
|
+
role: 'assistant',
|
|
362
|
+
content: null,
|
|
363
|
+
tool_calls: [{ id: tc.id, type: 'function', function: { name: tc.tool, arguments: JSON.stringify(tc.args ?? {}) } }],
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return { role: m.role, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) };
|
|
367
|
+
}),
|
|
368
|
+
// declaredSideEffect 不出站:权限面只进权限门(§3.2.M8.3 字段表约束方向)
|
|
369
|
+
...(req.tools.length ? { tools: req.tools.map(t => ({ type: 'function', function: { name: t.name, description: t.description, parameters: t.parameters } })) } : {}),
|
|
370
|
+
}),
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
id,
|
|
374
|
+
chat(req) {
|
|
375
|
+
return streamSsePost({
|
|
376
|
+
url,
|
|
377
|
+
// 凭据门:resolve → reveal(SQ-07 凭据解析;A080001 在此抛出——首块前,runTurn 可 error 收口)
|
|
378
|
+
buildRequest: async () => {
|
|
379
|
+
let apiKey = '';
|
|
380
|
+
if (opts.credentials) {
|
|
381
|
+
const ref = opts.credentials.resolve(opts.provider ?? id, {});
|
|
382
|
+
apiKey = opts.credentials.reveal(ref, {});
|
|
383
|
+
}
|
|
384
|
+
return toOpenAI(req, apiKey);
|
|
385
|
+
},
|
|
386
|
+
fetchImpl,
|
|
387
|
+
signal: req.signal,
|
|
388
|
+
timeoutMs, firstByteMs, retries, backoffMs, sleep,
|
|
389
|
+
}, parseSseStream);
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/** stop_reason → CAR finishReason(未映射 fail-visible——openai FINISH_MAP 同纪律) */
|
|
394
|
+
const ANTHROPIC_STOP_REASON_MAP = {
|
|
395
|
+
end_turn: 'stop',
|
|
396
|
+
stop_sequence: 'stop',
|
|
397
|
+
max_tokens: 'length',
|
|
398
|
+
tool_use: 'toolUse',
|
|
399
|
+
refusal: 'aborted',
|
|
400
|
+
};
|
|
401
|
+
/**
|
|
402
|
+
* Anthropic Messages API 请求体(D-20):system 消息提升为顶层 `system`(多条 \n\n 连接);
|
|
403
|
+
* toolResult 收敛进 user 消息 content(连续多条合并为单 user 多 tool_result 块——Anthropic
|
|
404
|
+
* 消息序列约束);assistant 工具调用 = content:[{type:'tool_use',id,name,input}];
|
|
405
|
+
* declaredSideEffect 不出站(与 openai-compat 同红线)。
|
|
406
|
+
*/
|
|
407
|
+
function toAnthropic(req, apiKey) {
|
|
408
|
+
const system = [];
|
|
409
|
+
const turns = [];
|
|
410
|
+
let pendingToolResults = [];
|
|
411
|
+
const flushToolResults = () => {
|
|
412
|
+
if (!pendingToolResults.length)
|
|
413
|
+
return;
|
|
414
|
+
turns.push({ role: 'user', content: pendingToolResults });
|
|
415
|
+
pendingToolResults = [];
|
|
416
|
+
};
|
|
417
|
+
for (const m of req.messages) {
|
|
418
|
+
if (m.role === 'system') {
|
|
419
|
+
system.push(typeof m.content === 'string' ? m.content : JSON.stringify(m.content));
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (m.role === 'toolResult') {
|
|
423
|
+
const p = m.content;
|
|
424
|
+
pendingToolResults.push({
|
|
425
|
+
type: 'tool_result',
|
|
426
|
+
tool_use_id: p?.id ?? '',
|
|
427
|
+
content: p?.error ?? JSON.stringify(p?.result ?? null),
|
|
428
|
+
...(p?.error ? { is_error: true } : {}),
|
|
429
|
+
});
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
flushToolResults();
|
|
433
|
+
if (m.role === 'assistant' && m.content != null && typeof m.content === 'object' && 'toolCall' in m.content) {
|
|
434
|
+
const tc = m.content.toolCall;
|
|
435
|
+
turns.push({ role: 'assistant', content: [{ type: 'tool_use', id: tc.id, name: tc.tool, input: tc.args ?? {} }] });
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
turns.push({ role: m.role, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
flushToolResults();
|
|
442
|
+
return {
|
|
443
|
+
method: 'POST',
|
|
444
|
+
headers: {
|
|
445
|
+
'content-type': 'application/json',
|
|
446
|
+
'x-api-key': apiKey,
|
|
447
|
+
'anthropic-version': '2023-06-01',
|
|
448
|
+
},
|
|
449
|
+
body: JSON.stringify({
|
|
450
|
+
model: req.model,
|
|
451
|
+
stream: true,
|
|
452
|
+
max_tokens: req.maxTokens ?? 4096,
|
|
453
|
+
...(system.length ? { system: system.join('\n\n') } : {}),
|
|
454
|
+
messages: turns,
|
|
455
|
+
...(req.tools.length
|
|
456
|
+
? { tools: req.tools.map(t => ({ name: t.name, description: t.description, input_schema: t.parameters })) }
|
|
457
|
+
: {}),
|
|
458
|
+
}),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Anthropic SSE → LlmChunk 流解析(data-only:data JSON 自带 `type` 字段,`event:` 行不依赖)。
|
|
463
|
+
* content_block_start(tool_use) → toolCallDelta{id,name};input_json_delta → toolCallDelta{argumentsDelta};
|
|
464
|
+
* message_delta.stop_reason → finishReason(终态信号,单次);ping/message_stop/未知事件容错跳过。
|
|
465
|
+
*/
|
|
466
|
+
async function* parseAnthropicSseStream(body) {
|
|
467
|
+
const decoder = new TextDecoder();
|
|
468
|
+
let buf = '';
|
|
469
|
+
for await (const bytes of body) {
|
|
470
|
+
buf += decoder.decode(bytes, { stream: true });
|
|
471
|
+
let nl;
|
|
472
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
473
|
+
const line = buf.slice(0, nl).replace(/\r$/, '');
|
|
474
|
+
buf = buf.slice(nl + 1);
|
|
475
|
+
if (!line.startsWith('data:'))
|
|
476
|
+
continue;
|
|
477
|
+
const data = line.slice(5).trim();
|
|
478
|
+
if (!data)
|
|
479
|
+
continue;
|
|
480
|
+
let evt;
|
|
481
|
+
try {
|
|
482
|
+
evt = JSON.parse(data);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
continue;
|
|
486
|
+
} // 心跳/注释行容错跳过
|
|
487
|
+
const type = evt?.type;
|
|
488
|
+
if (type === 'content_block_start') {
|
|
489
|
+
const block = evt.content_block;
|
|
490
|
+
if (block?.type === 'tool_use') {
|
|
491
|
+
yield { toolCallDelta: { index: evt.index, id: block.id, name: block.name } };
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else if (type === 'content_block_delta') {
|
|
495
|
+
const d = evt.delta;
|
|
496
|
+
if (d?.type === 'text_delta' && typeof d.text === 'string' && d.text.length) {
|
|
497
|
+
yield { delta: d.text };
|
|
498
|
+
}
|
|
499
|
+
else if (d?.type === 'input_json_delta' && typeof d.partial_json === 'string' && d.partial_json.length) {
|
|
500
|
+
yield { toolCallDelta: { index: evt.index, argumentsDelta: d.partial_json } };
|
|
501
|
+
}
|
|
502
|
+
// thinking_delta / citations_delta 等非 CAR 消费面:容错跳过
|
|
503
|
+
}
|
|
504
|
+
else if (type === 'message_delta') {
|
|
505
|
+
const raw = evt.delta?.stop_reason;
|
|
506
|
+
if (raw != null) {
|
|
507
|
+
const finish = ANTHROPIC_STOP_REASON_MAP[String(raw)];
|
|
508
|
+
yield finish
|
|
509
|
+
? { finishReason: finish }
|
|
510
|
+
: { finishReason: 'error', error: { code: 'B080001', message: `未映射的 stop_reason "${raw}"` } };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else if (type === 'error') {
|
|
514
|
+
// 流内 error 事件(overloaded_error 等)→ error chunk 收口(后续 message_delta 由守卫拦冲突)
|
|
515
|
+
yield { finishReason: 'error', error: { code: 'B080001', message: `provider 流错误:${evt.error?.message ?? JSON.stringify(evt).slice(0, 200)}` } };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
export function createAnthropicAdapter(opts) {
|
|
521
|
+
const id = opts.id ?? 'anthropic';
|
|
522
|
+
const baseUrl = opts.baseUrl.replace(/\/+$/, '');
|
|
523
|
+
assertTlsOrLoopback(baseUrl);
|
|
524
|
+
const url = `${baseUrl}/v1/messages`;
|
|
525
|
+
// 1.8-BUG-1:缺省 node:http(s)(undici 退出崩溃——面头注记);注入缝保留
|
|
526
|
+
const fetchImpl = opts.fetchImpl ?? nodeHttpSseFetch;
|
|
527
|
+
const sleep = opts.sleep ?? (ms => new Promise(r => setTimeout(r, ms)));
|
|
528
|
+
const timeoutMs = opts.timeoutMs ?? 120_000;
|
|
529
|
+
const firstByteMs = opts.firstByteMs ?? 30_000;
|
|
530
|
+
const retries = opts.retries ?? 2;
|
|
531
|
+
const backoffMs = opts.backoffMs ?? [1_000, 2_000];
|
|
532
|
+
return {
|
|
533
|
+
id,
|
|
534
|
+
chat(req) {
|
|
535
|
+
return streamSsePost({
|
|
536
|
+
url,
|
|
537
|
+
// 凭据门:resolve → reveal(provider='anthropic' → keychain car-runtime/anthropic →
|
|
538
|
+
// env ANTHROPIC_API_KEY——credentials.ts 1.4 既有登记;A080001 首块前抛出)
|
|
539
|
+
buildRequest: async () => {
|
|
540
|
+
let apiKey = '';
|
|
541
|
+
if (opts.credentials) {
|
|
542
|
+
const ref = opts.credentials.resolve(opts.provider ?? id, {});
|
|
543
|
+
apiKey = opts.credentials.reveal(ref, {});
|
|
544
|
+
}
|
|
545
|
+
return toAnthropic(req, apiKey);
|
|
546
|
+
},
|
|
547
|
+
fetchImpl,
|
|
548
|
+
signal: req.signal,
|
|
549
|
+
timeoutMs, firstByteMs, retries, backoffMs, sleep,
|
|
550
|
+
}, parseAnthropicSseStream);
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M8 · 脱敏钩子(redact ——《系统设计》§3.2.M8.2 脱敏行 / SQ-07 输出敏感扫描 / O-14)
|
|
3
|
+
*
|
|
4
|
+
* 口径:
|
|
5
|
+
* - 检测器单一权威:本层只做 M8 面(RedactionResult 形态 + 流式 hold-back),
|
|
6
|
+
* 模式库/三层检测/遮蔽口径全部委托 F12 scanSecrets(security/secrets.ts)——
|
|
7
|
+
* 双实现必然漂移,S15 已确立「回填前强制 redact」先例;
|
|
8
|
+
* - 遮蔽口径(§3.2.M8.3 字段表):API Key 形如 sk-****(保留首尾 ≤4 字符)= F12 maskToken;
|
|
9
|
+
* - 流式扫描(StreamRedactor):delta 直发会造成跨 chunk 命中逃逸——hold-back 窗口
|
|
10
|
+
* 「从最早疑似锚点起驻留」,命中补齐即遮蔽放行,finishReason 收口时 flush 全量扫描;
|
|
11
|
+
* 结构标记类模式(GCP service_account 等)的匹配起点先于锚点,流式模式不保证完整遮蔽
|
|
12
|
+
* (登记为流式已知边界;非流式 redact() 为权威口径)。
|
|
13
|
+
*/
|
|
14
|
+
import { type ScanHit } from '../security/secrets.ts';
|
|
15
|
+
export interface RedactionResult {
|
|
16
|
+
/** 脱敏后文本(§3.2.M8.3 字段表口径:RedactionResult.redacted) */
|
|
17
|
+
redacted: string;
|
|
18
|
+
/** 命中遮蔽次数 */
|
|
19
|
+
redactedCount: number;
|
|
20
|
+
/** 命中明细(审计留痕用;不含原文) */
|
|
21
|
+
hits: Array<Pick<ScanHit, 'category' | 'name' | 'start' | 'end' | 'masked' | 'score'>>;
|
|
22
|
+
}
|
|
23
|
+
/** 非流式脱敏钩子(§3.2.M8.2:redact(text): RedactionResult,天然幂等) */
|
|
24
|
+
export declare function redact(text: string): RedactionResult;
|
|
25
|
+
/**
|
|
26
|
+
* 流式脱敏器:push() 返回「当前可安全放行」的文本(已遮蔽),flush() 收口全量扫描。
|
|
27
|
+
* 语义:不泄漏(任何已放行字节不可能成为命中的一部分)、不误遮(锚点未补齐成命中时
|
|
28
|
+
* 原文放行)、粒度换正确性(疑似锚点驻留直到命中补齐或 flush——长行含假锚点时
|
|
29
|
+
* 流式粒度退化到收口时点,登记为已接受折衷)。
|
|
30
|
+
*/
|
|
31
|
+
export declare class StreamRedactor {
|
|
32
|
+
#private;
|
|
33
|
+
/** 追加 delta,返回可安全放行的文本(可能为空串) */
|
|
34
|
+
push(text: string): string;
|
|
35
|
+
/** 收口(finishReason 时点):全量扫描遮蔽剩余驻留 */
|
|
36
|
+
flush(): string;
|
|
37
|
+
/** 已遮蔽计数口径(测试/审计观察面):flush 前的累计放行不含命中原文 */
|
|
38
|
+
get pendingLength(): number;
|
|
39
|
+
}
|