Trajecta 是本地优先(local-first)的 LLM API record/replay 代理,覆盖 OpenAI-compatible 及 Anthropic Messages、Google Gemini、Vertex native 等主流协议族。典型用法:
- 开发期让 SDK 流量经过本地代理;
- 把原始 HTTP 交换持久化为
.httpcassette; - 在单元测试中用
pkg/replay回放 cassette,不访问上游模型。
目标是可靠测试、降低 API 成本、快速排障。生产形态同时是 Postgres-first 网关:承载渠道/模型路由、本地 Responses 运行时、审计与 Monitor 查询。
架构原则:
- 转发路径保持简单可靠,不在热路径做深度协议解析。
- raw cassette 是 replay 与详情页的事实源。
- 派生结果(usage、timeline、Observation IR、findings)可失败、可重试、可从 cassette 重算。
- 测试回放不依赖网络。
顶层目录与包职责:
cmd/server:服务端二进制入口与serve装配。main.go只通过run退出,root.go只接线serve、version、completion与--config/--format——除serve外不再承载运维子命令。management.go提供 serve 与测试共用的管理 HTTP/MCP 装配,provider_startup_probe.go提供启动期 provider probe 辅助。cmd/trajecta:运维 CLI 入口。除既有的upgrade、layout、version外,还通过internal/cli暴露全部不需要代理进程的运维命令组:analyze、audit、auth、config、db、doctor、migrate、models、provider、schema、tools。root.go用 viper 绑定--config/--format后交给internal/cli.Register。internal/cli:两个二进制共用的 CLI 契约与运维命令实现。契约是退出码表、JSON envelope、Runtime(--config/--format)、Execute顶层错误路径与 ERROR 日志 recorder;命令文件是analyze.go、audit.go、auth.go、config.go、db.go、doctor.go、migrate.go、models.go、provider.go、schema.go、tools.go。cmd/server只引用契约,不注册其中任何命令。internal/appdb:按配置打开 application database 的共用入口——驱动规范化、迁移模式判定、MigrateUp/OptimizeIndexes、Open/Initialize与 auth store 打开;serve 启动与 trajecta 的db/auth/audit/analyze都走这里。internal/proxy:反向代理与/v1/responses入口,负责鉴权、转发、响应截获与协议感知透传调整。internal/recorder:cassette 写入与 metadata finalization。internal/store:application DB(Postgres/SQLite)schema 初始化、索引、查询与派生状态,并拥有ConfigurationTransaction;output_dir.go的Store.OutputDir()暴露配置的trace.output_dir,供读路径缓存(会话轨迹缓存)与 cassette 放在同一目录而不新增配置项。internal/upstream:上游解析、协议族、能力/路由 profile 与鉴权/URL 规则。internal/channel:渠道(provider)配置与 probe 用例、legacy YAML bootstrap、runtime targets。internal/responses:本地 Responses 运行时及其子系统——runtime(编排与 state store)、httpapi(HTTP handler)、chatclient(内部 Chat Completions client)、audit(审计查询与写入)、functionexec(本地函数执行器)、tools(hosted tools:hosted、mcp、websearch)、protocol(类型定义)、codexfixtures(Codex 兼容 fixture)。internal/router:多上游选择、健康、重试、sticky、决策记录与路由快照发布。internal/routeplan:入口 endpoint、执行模式与responses_strategy的路由决策计划。internal/appdbmigrate:应用 Postgres checked-in migration 的加载与应用。internal/monitor:Monitor API 与内嵌 React UI(go:embed ui/dist/*)。internal/netproxy:出站代理策略(app_settings键network.proxy)。按 Linuxhttp_proxy/https_proxy/no_proxy语义编译成不可变 resolver,Manager原子替换策略;进程启动时读库并安装http.DefaultTransport,透传路径的 transport 直接把Proxy指向它,因此保存设置无需重启。详见 Monitor 使用指南 的系统页一节。internal/mcpserver:受限 MCP 工具层。internal/analyzer:确定性 detector,产出 Observation findings。internal/resources:从 Observation IR 的模型工具调用中提取本地路径与 URL(不含文件内容),由解析 worker 写入resources/trace_resources资源索引;工具结果、消息正文与代码内容不参与。internal/observeworker:从 cassette 解析 Observation IR 并落库的后台 worker。internal/reanalysis:trace/session/batch 重分析任务(service 与 worker)。internal/sessionanalysis:session 级聚合分析。internal/trajectory:从客户端可见的 Responses cassette 重建 ATIF-v1.8 会话轨迹(不调用模型、不修改 cassette);scripts/validate_atif.py与scripts/atif_tests用固定版本官方模型做离线格式校验。internal/providerprobe:provider 能力探测。internal/evals:基于 replay 的确定性 eval 基线执行(HTTP 状态、TTFT/token 预算、tool-call 校验等)。internal/migrate:把旧 V2 cassette 转换为 V3,并可选重建索引行。internal/auth:认证 user/token 与 JWT。internal/config:YAML/env 配置装载(含 legacy bootstrap 输入)。internal/limit:请求限流;internal/redaction:URL 与敏感参数脱敏;internal/chaos:配置驱动的故障注入(action取delay/error,大小写不敏感;rate必须在 0.01.0,999,否则status_code必须是 0 或 100Load报错而不让规则静默失效或让WriteHeaderpanic)。pkg/recordfile:V2/V3 cassette 解析与写入。pkg/replay:测试回放 transport。pkg/llm:协议识别、adapter、usage 归一化与 streamResponsePipeline。pkg/observe:Observation IR 定义与各协议 parser。pkg/secretmask:凭据与个人数据(当前为中国居民身份证号与手机号)形状识别与保形假值遮蔽——Ragel 编译的 DFA(rules.rl是唯一模式来源)、规则元数据表、离线校验器、上下文评分与可逆假值 vault;生成的rules_ragel.go已提交且不手工编辑。数字类形状共用一条粗规则digit_run,由scan.go的digitClasses细分类;PEM 私钥块由pem.go从 DFA 命中的头行原生扩展到配对 footer(标签必须一致,反向引用无法用 DFA 表达),不新增 DFA 状态。vault 的real -> fake映射由internal/store的 bbolt 文件持久化(默认开启),启动默认加载全部、首次遇到未知真值时回查,因此假值跨重启与上下文压缩保持稳定;正数load_limit可限制启动内存并形成识别窗口。ent/schema与ent/postgres-migrations:ent 表结构定义与 checked-in Postgres migration。web/monitor-ui:Monitor 前端源码(React + Vite 8,Tailwind v4 + Radix primitives,TanStack Query 管服务端状态,Playwright 与node --test测试)。样式令牌由src/styles/tokens.css(生成的 OKLCH 调色板)提供,vite build的产物写入internal/monitor/ui/dist。
协议族与 provider 细节见 协议与 Provider,路由与凭据规则见 路由与凭据。
请求转发 → 录制 → 解析 → 索引 → 展示:
- SDK、CLI 或应用把 LLM API 请求发到本地代理(如
/v1/chat/completions、/v1/responses、/v1/messages)。 - proxy 归一化入口 endpoint,并按请求路径与上游配置识别协议族。
- routeplan/router 依据入口、模型、能力与
responses_strategy选择 route target,并记录决策与候选。 - proxy 做少量透传型调整,例如为 OpenAI-compatible chat stream 注入
stream_options.include_usage=true。凭据与个人数据遮蔽也在这一步、真正发包之前发生,且默认开启:通过规则与置信度门限的值(API key、SSH/PEM 私钥块、身份证号、手机号等)被替换为等长保形假值后才发给上游,cassette 记录的仍是原始请求体,响应在写回 client 之前把假值换回真值(流式响应按跨 delta 重组);两个方向各写secret.masked/secret.restored事件且不携带凭据字节。细节与边界见 敏感凭据遮蔽。 - 请求转发到上游,响应以流式或非流式返回 client。若某次
/v1/responses请求被判定为原生直通,则该请求只会在「自己实现 Responses API」的目标里选择:只能靠本地 runtime 翻译的 Chat Completions 目标在这一请求上不可选(重试重新选择时同样不可选),否则原样转发会把 Responses 请求发给不实现该协议的上游。转发路径自己写响应体(不经httputil.ReverseProxy的拷贝),所以「流式响应逐次 flush」这条契约由它承担:流式(或长度未知)的响应每写一次就 flush 一次,否则小于服务端输出缓冲的写入会被攒着,client 在 handler 返回前收不到任何东西——连响应头都收不到,stream: true就退化成「最后一个 token 之后一次性收到完整回答」,首字节时间等于总时长。 - recorder 把原始 HTTP 请求/响应写入
.httpcassette,并在 prelude 追加 meta 与 event。 pkg/llm.ResponsePipeline从响应流中抽取 usage 与llm.*timeline 事件。- application DB 写入 trace、路由、usage、session、upstream 等索引字段;生产部署使用 Postgres。代理请求路径只把完成的录制投入有界后台 finalize 队列(
internal/recorder/finalize_queue.go:2 个 worker、深度 256),worker 负责补 prelude、写logs(含 routing 列)并入队 parse_job;队列满时回退到请求内联完成,进程正常关闭时排空队列。 - observeworker 与 reanalysis 从 raw cassette 解析 Observation IR、findings、资源索引和分析任务结果。
- Monitor 与 MCP 从 application DB 查询列表/聚合,从 cassette 读取详情。
- Monitor 的会话轨迹导出用
internal/trajectory从该会话的客户端可见 cassette 重建 ATIF-v1.8 JSONL(不调用模型、不修改 cassette)。 - 单元测试通过
pkg/replay.Transport从 cassette 回放响应。
Timeline 事件(如 llm.output_text.delta、llm.reasoning.delta、llm.tool_call、llm.usage、routing.selection、routing.filtered、routing.retry_candidate、routing.failure)写入 cassette prelude,并被 Monitor/MCP 用于 trace 详情、路由排障和失败聚类。同一份 OpenAI 兼容字节有三个读取端(记录端 pkg/llm 的 pipeline、replay/解析端 pkg/llm 的 stream 解析、Monitor trace 视图 internal/monitor),它们与观测解析端 pkg/observe 一样都把 reasoning 字段的两种拼写 delta.reasoning_content(DeepSeek/vLLM)与 delta.reasoning(OpenRouter)折叠为「先 reasoning_content,后 reasoning」;只认其中一种会让 cassette 事件、解析结果与观测 IR 对同一份字节给出不同答案。Gemini/Vertex 的 Part 同理:thought: true 是 reasoning,functionCall 是一次完整工具调用(参数是对象),其余 text 才是输出文本;三端都按这个规则分类。
当前是协议族感知的透传代理,不是跨协议转换网关。已实现协议族与 endpoint 覆盖见 协议参考。
openai_compatible、anthropic_messages、google_genai、vertex_native在转发热路径中互不转换;上游必须支持 client 实际请求的 endpoint(例如 Claude Code 的/v1/messages需要 Anthropic Messages 兼容上游)。pkg/llm/pkg/observe可以把不同协议归一化为统一摘要与 Observation IR,但这不等于可以无损转发到另一个协议。- proxy 无条件接受
/v1/chat/completions、/v1/responses、/v1/messages。
/v1/responses 是唯一例外,按请求在 native 与本地 runtime 之间二选一:
- native:匹配到支持 Responses 的上游时直接透传。
- 本地 runtime:把请求编排为一次内部上游
/v1/chat/completions调用(responses_server执行模式)。该模式始终可用且没有配置开关;旧的responses_server.enabled与TRAJECTA_RESPONSES_ENABLED已移除。本地 runtime 延迟构建,构建失败只影响触发该次构建的请求,不阻塞启动。 - 策略存于 application DB 的
app_settings键routing.settings,经GET/PATCH /api/settings/routing读写,不是 YAML 键;取值为auto、prefer_native、prefer_local_server、native_only、local_server_only。 - native 与 local 的选择按模型解析而非按渠道:
channel_models.supports_responses/supports_chat_completions(Monitor UI 可编辑)覆盖渠道级api_type/capabilities,未声明值的模型回落到渠道级行为;YAML 侧对应upstream.model_capabilities。
本地 Responses 运行时的编排、审计与 hosted tools 细节见 Responses 运行时。
新录制只写 LLM_PROXY_V3:
- 以
# trajecta/v3开头的短 prelude; - 一行
# meta: {...}JSON; - 零或多行
# event: {...}JSON; - 一个空行;
- 原始 HTTP 请求字节;
- 一个分隔换行;
- 原始 HTTP 响应字节。
单行 SSE 帧的上限由 recordfile.MaxStreamLineBytes(4 MiB)统一:录制端一直按这个界限接收,读取端(pkg/llm 的 stream 解析、pkg/observe、Monitor 的三个 stream parser、runtime 的 chatclient)都不得低于它,凡是手里已有完整 body 的读取端按 body 长度放宽。录制端接受的帧必须能被读取端完整读出——上限偏小的一侧会静默丢帧(bufio.Scanner 返回 ErrTooLong,而这些解析不检查 scanner.Err()),表现为 cassette 完整、但 Monitor 与派生元数据里的内容和 usage 缺失。
读取端继续支持 legacy LLM_PROXY_V2(固定 2KB JSON header block),并把改名前的 prelude magic # llm-tracelab/v3 同样识别为 V3;写入端只产出 # trajecta/v3。LLM_PROXY_V3 这个 meta header version 值是稳定的格式标识,刻意不随项目改名。cassette 保持人类可读;修改格式时先改 pkg/recordfile,再同步 recorder、monitor、replay。
raw .http cassette 是事实源:
- replay 事实源;
- raw protocol 详情源;
- Observation IR / findings / 资源索引 / usage repair 的重建来源。
application DB 是结构化查询源:
- trace/session/upstream/model/channel 的列表、聚合、过滤与分页;
- auth user/token、channel/model 配置、system events、analysis jobs、Observation 摘要(
trace_observations,节点树不落库)、findings、资源索引(resources/trace_resources)、eval; - Responses semantic state(
responses、response_items)与 Responses audit(request_audits、execution_events、upstream_exchanges、tool_call_audits); - 凭据与个人数据遮蔽审计(
credential_mask_audits):每个被替换的值一行,存真值与保形假值(按SecretStorageMode()加密并逐行记storage_mode),(trace_id, event_index)唯一。只有真正执行改写的那次请求能写它——假值由进程内 vault 生成,cassette 只记录原始请求体,回填路径不读映射文件,所以后台重建索引仍然会跳过;cassette prelude 的secret.masked事件因此仍然只带规则与偏移,凭据字节不进入 prelude、trace 索引与导出(见 凭据遮蔽)。审计行与credential_masked系统事件在同一事务提交;写入由录制收尾的独立旁路队列执行并重试,不占转发或 finalize worker;serve在启动和每 5 分钟运行ReconcileCredentialMaskEvents,只为已有审计行补缺失事件。系统事件仍按规则一行,trace_id只保留最近一次;按 trace 读取时由credential_mask_audits的trace_id索引派生 occurrence,不按 trace 复制事件行。credential_masked的读/解决/忽略按(trace_id, rule)存在credential_mask_event_states(只写非默认状态,没有行即unread),因此一条 trace 的操作不影响其他 trace;事件行的status/时间只是最近一次 occurrence 的缓存,写路径在 upsert 和最近一次操作后同步。API 的 trace 范围用 query:/api/events?trace_id=、/api/events/<id>?trace_id=、/api/events/<id>/read|resolve|ignore?trace_id=;trace_id必须是完整 UUID。serve启动时用两个app_settings标记一次性修复旧版本留下的永久忽略,并把旧规则级状态迁到最近一次 occurrence;更早的 occurrence 按未读处理,其他事件来源的ignored语义不变。 - 凭据遮蔽映射(
{{output_dir}}/secret_mask_mappings.db):bbolt 文件,按真值去重只保存real -> fake,启动时默认加载全部且不淘汰(secret_masking.mapping_store.load_limit: 0;正数只加载最新 N 条,超过余量后淘汰最旧),新映射异步批量写入。它是 Vault 的恢复索引而不是审计表:没有 trace、偏移与命中次数,删掉只影响后续重启能否复用旧假值。同一output_dir只能被一个进程打开(bbolt 排他锁),并且用trace_index.secret加密、用同一把密钥做去重索引的 HMAC。
生产必须使用 Postgres,checked-in migrations 位于 ent/postgres-migrations(db migrate up)。未配置 database.driver 时驱动为 Postgres(缺 DSN 直接报错,不会新建本地文件);SQLite 必须显式选择,用于本地/开发/测试,默认文件为 {{output_dir}}/trajecta.sqlite3,其 schema 在启动时应用而非版本化迁移。列表页不得依赖扫描文件系统;replay 不得依赖 SQLite 或网络。
会话汇总(session_summaries)是唯一仍由写入路径增量维护的派生表。录制路径只把待重建的 session 记进进程内队列,不自己做整会话聚合;serve 通过 Store.StartDerivedRefresh() 让后台 goroutine 成为唯一消费者(写入直接唤醒,2 秒 ticker 兜底),因此派生重建不再由读取请求买单,读取端最多观察到亚秒级滞后。未启动 flusher 的调用方保持同步语义(读取端先 flush)。
派生数据的当前边界:语义节点树(原 semantic_nodes)不再落库,trace 详情按需从 cassette 重解析;Overview 直接聚合 logs,不再维护 overview_metric_buckets / overview_metric_bucket_members;per-request 路由事实(route_target_id、channel_id、credential_id、sticky_status、sticky_previous_upstream_id)是 logs 上的列,/api/routing/summary 直接 GROUP BY 这些列。这三张旧表仍由 Postgres 历史迁移创建(SQLite 启动 schema 不再创建),代码不读取也不写入它们,回收空间是运维的手工决定(见 存储与部署)。
Monitor 的会话轨迹导出在 {{output_dir}}/trajectory-cache/ 维护一个有界磁盘缓存:键为 (session_id, last_trace_id, trace_count, limit),条目以临时文件加 rename 原子写入,最多保留 512 条 / 512 MiB。它是纯派生数据,任何时候都可以删除并按下述 cassette 重建;?full=1 与 ?stream=1 不读写该缓存。
部署与迁移细节见 存储与部署,Postgres 长期运行优化见 Postgres 运维。
所有管理写入(渠道、模型、别名、provider setup 与 probe apply)共用一个 store.ConfigurationTransaction:
- 持进程级配置锁,按
configMu->upstreamMu->reloadMu->router.mu的顺序取锁,并使用单个 SQL 事务。 - 回调必须显式 commit,其它退出路径回滚。
- 只有 commit 成功后才发布新路由快照,运行中的快照绝不描述数据库拒绝的配置。
- 事务内不得再开事务(会返回
store.ErrNestedTransaction),也不得新增reloadMu->upstreamMu的反向获取。
ConfigurationTransaction 串行化 upstream_targets/upstream_models 的管理写入,这类写入全程持有进程级配置锁、upstream 写锁与单个 SQL 事务;后台 upstream refresh 与代理侧 RefreshNow 也会写入这两张表,但以 best-effort 方式通过同一 upstream 写锁落库:配置变更持锁时跳过落库但仍更新内存,并在获锁后按当前 live target 集合过滤,避免为已删除的 target 复活行。
渠道配置的 DB 优先规则:YAML upstream/upstreams 只是首次 bootstrap 输入;首次数据库写入记录 app_settings 键 channels.initialized,此后 DB 是路由配置来源,即使全部渠道停用或删除也不回退 YAML。GET /api/settings/channels 返回 channel.Service.HasConfiguration():channels.initialized 标记为真,或 DB 中已存在任意渠道行,都报告 {"initialized": true};DELETE /api/settings/channels 仅清除标记(只有在 DB 中确实没有渠道时,下次启动才会重新导入)。带显式 credentials 列表的 YAML 配置保持 YAML 管理,并拒绝 Monitor 的渠道/模型/别名写入(返回 409)。
代码是最终事实源,其次是 AGENTS.md;本文档只描述“当前为真”的行为,不承载计划、里程碑或历史方案。
- 结构化查询以 application DB 为准;replay 与 raw 详情以
.http为准。 - 不要让列表页扫描 raw 文件,也不要让 replay 依赖数据库或网络。
- 修改 record format、storage schema、Monitor API 或 MCP 工具面时,必须同步更新对应文档与测试。
- 用户面向入口尽量收敛为“刷新分析”和“修复统计”;
analysis_jobs的 job 类型是审计与兼容层面的实现细节。
- 新录制只写 V3;读取端继续支持 V2,cassette 保持人类可读。
pkg/replay是硬性要求,且不能依赖网络或 Observation IR。- 存储 schema 只做 additive 演进;新列需通过启动时迁移兼容旧 DB(Postgres 走
internal/appdbmigrate,SQLite 走启动 schema)。 - 旧本地 SQLite 应用库(如更早的
trace_index.sqlite3)与显式选择时的默认文件trajecta.sqlite3必须可原地升级;当默认文件不存在而改名前的默认llm_tracelab.sqlite3存在时,直接原地沿用该旧文件而不是新建空库。 - Observation parser 对 unknown fields 保持 tolerant;所有派生分析结果都必须能从 raw cassette 重算。
测试分层:
pkg/recordfile、pkg/replay、pkg/llm、pkg/observe:格式、回放、协议归一化与 Observation IR 的单元基线。internal/proxy、internal/router、internal/store、internal/monitor:转发、路由、存储与 API 的集成基线。web/monitor-ui:node --test tests-unit/单元基线(含调色板对比度)与 Playwright mock 套件、真实服务套件。unittest/与tests/fixtures/:cassette 回放与跨协议矩阵 fixture。
常用入口(命令矩阵见 开发指南):
task check:quick
go test ./pkg/recordfile ./pkg/replay ./pkg/llm ./pkg/observe
go test ./internal/proxy ./internal/router ./internal/store ./internal/monitor改动 streaming、router 或 store 并发时补:
task test:race前端改动补:
task ui:build
task ui:test
go test ./internal/monitorMonitor 与 MCP 的用户面说明见 Monitor 指南、MCP 指南,代理接入示例见 代理使用示例,观测/审计模型见 观测与审计。