跳到主要内容

服务骨架:从 CLI 到 axum 路由装配

30 秒导读: 本章讲 Tabby 这个进程"怎么起来、请求怎么被路由"——从命令行 tabby serve 一路追到 axum 的路由树装配好、开始监听端口。不讲补全算法、不讲索引检索、不讲推理后端, 只讲骨架:进程启动的主线,和各个服务(embedding / code / docsearch / completion / chat / webserver)按什么顺序被创建、注入到哪些路由上。

这是 Tabby 系列的第 1 章。全景与阅读地图见 index.md;本章后面各机制的深入见 02 补全03 检索索引04 推理后端05 企业 webserver


1. 这是什么(零基础也能懂)

一句话定义: Tabby 是一个自托管的 AI 代码补全服务器——你在自己机器上跑起一个 HTTP 服务, IDE 插件(VSCode / Vim / IntelliJ)连上它,就能在编辑器里获得代码补全和对话。

这一章聚焦"骨架"。 一个服务器进程从被敲下命令到能接请求,中间要走一串固定动作:

  • 解析命令行参数(要跑哪个模型?监听哪个端口?用 CPU 还是 GPU?)
  • 读配置文件、把命令行参数合并进去
  • 按需把各种"服务"造出来(补全服务、对话服务、代码检索……)
  • 把这些服务挂到一条条 HTTP 路由上,拼成一棵路由树
  • 绑定端口,开始监听

用起来什么样: 最小的一条启动命令——

# 用 CPU 跑一个补全模型,监听默认 8080 端口
tabby serve --model TabbyML/StarCoder-1B --device cpu

进程起来后会打印一个 ASCII 大字 logo 和监听地址(见 crates/tabby/src/routes/mod.rs:32-44 里那段 println!),然后 IDE 插件就能 POST /v1/completions 拿补全了。

一句话直觉:tabby serve 想成"装配一台流水线"——先备齐零件(各服务), 再把零件按顺序接到传送带(axum Router)上;传送带的每个工位就是一条 HTTP 路由。 本章讲的就是这条装配线怎么搭,不讲每个工位内部的机器怎么运转(那是后面几章)。

本节不出现底层代码细节。目标:完全没接触过 Tabby 的人读完知道"这个进程是干嘛的、本章讲它的哪一段"。


2. 顶层全景(它大概怎么转)

2.1 一张图:main 到监听的主线

先看整条启动主线。怎么读这张图:从上到下是时间顺序,每一步产出下一步要用的东西。

tabby 二进制

┌────▼──────────────────────────┐
│ main() main.rs:54 │ clap 解析 CLI → Commands::Serve / Download
│ · 装 color_eyre / tracing │ 加载全局 Config、建 ~/.tabby 根目录
└────┬──────────────────────────┘
│ Commands::Serve(args)
┌────▼──────────────────────────┐
│ serve::main() serve.rs:117 │ ← 本章主战场
└────┬──────────────────────────┘

① merge_args 把 --model/--chat-model 合进 Config
② load_model 本地模型?没下就下载
③ 造服务(按依赖顺序,见 2.3)
④ api_router 把服务挂成一堆小 Router → merge 成 api
⑤ ApiDoc + SwaggerUi 拼出 ui(OpenAPI 文档 + /swagger-ui)
⑥ [EE] ws.attach 企业版把 GraphQL / 后台任务 / 前端 UI 挂到 api、ui 上
⑦ run_app 加 CORS/Prometheus/metrics,绑端口,axum::serve 开始监听

2.2 部件一句话职责

部件干什么在哪
Cli / Commandsclap 定义的命令行结构,分 Serve / Download 两个子命令crates/tabby/src/main.rs:17,27
Config全局配置(仓库、模型、补全、embedding、answer 等),从 ~/.tabby/config.tomlcrates/tabby-common/src/config.rs:18
ServeArgsserve 子命令的参数(model / port / device / parallelism…)crates/tabby/src/serve.rs:84
serve::main服务装配总指挥:合并配置→造服务→装路由→起监听crates/tabby/src/serve.rs:117
api_router把各服务 with_state 到具体 HTTP 路由,合并成一棵 api 树crates/tabby/src/serve.rs:251
run_app收尾层:CORS、Prometheus、/metrics,axum::serve 监听crates/tabby/src/routes/mod.rs:14
ApiDoc / SwaggerUiutoipa 生成 OpenAPI JSON,Swagger UI 挂在 /swagger-uicrates/tabby/src/serve.rs:40-81:205-207
Webserver::attach(EE)企业版把 GraphQL / hub / OAuth / 前端 UI 挂到 api、ui 上ee/tabby-webserver/src/webserver.rs:59

2.3 主线走一遍(高层,不进代码)

serve::main(serve.rs:117-233)是本章核心。它按这个顺序把服务逐个造出来、串起来—— 顺序不是随意的,后面的服务依赖前面的产物:

  1. 合并配置 merge_args:命令行 --model 覆盖进 Config(serve.rs:397)。
  2. 备模型 load_model:本地模型没下载就先下(serve.rs:235)。
  3. embedding:只有开了环境变量 TABBY_EMBEDDING_ENABLED=yes 才造(serve.rs:132)。 它是很多东西的地基——代码检索、文档检索都要嵌入向量。
  4. webserver(EE):若启用,Webserver::new 打开 SQLite、准备 logger(serve.rs:139)。
  5. logger:默认写本地事件日志;EE 下换成 webserver 的 composed logger(serve.rs:147-152)。
  6. index_reader_provider → docsearch / code:都建立在 embedding 之上,embedding 没有就全是 None(serve.rs:154-174)。
  7. completion + chat:create_completion_service_and_chat 造补全服务和对话流(serve.rs:177)。
  8. api_router:把上面这些 Option<服务> 挂成路由(serve.rs:192)。
  9. OpenAPI/UI + EE attach + run_app:拼文档、企业版加挂、起监听(serve.rs:202-232)。

关键心智:几乎每个服务都是 Option 没配对应模型 / 没开 embedding,就是 None, 对应路由要么不挂、要么挂一个直接返回 501 NOT_IMPLEMENTED 的桩。这是理解装配逻辑的主线索。

目标:看懂"大盘"——谁先造、谁依赖谁、谁可选。下面逐段拆。


3. 核心原理(逐段拆装配线)

3.1 CLI:两个子命令,一个 fatal 宏

要解决的小问题: 一个二进制既要能"起服务",又要能"单独下模型",还要能优雅退出。

结构。 clap 的派生宏定义了顶层 CliCommands 两个子命令:

子命令作用参数结构
Serve起 IDE/编辑器用的 API 服务ServeArgs(serve.rs:84)
Download只下载模型,不起服务DownloadArgs(download.rs)

main(main.rs:54-74)是全流程入口,做四件事:装 color_eyre 错误报告、初始化 tracing、 Config::load() 读配置、create_dir_all~/.tabby 根目录(Unix 下还 chmod 0700 锁权限), 最后 match 分发到 serve::maindownload::main

Device 枚举(main.rs:36-51)列出五种推理设备:Cpu / Cuda / Rocm / Metal / Vulkan, 用 strum::Display 把它们序列化成 "cpu" 这类小写字符串,后面拼推理参数时会用到。

fatal!(main.rs:76-91)是全项目统一的"打日志然后 exit(1)":

// crates/tabby/src/main.rs:77 —— 记一条 error 级日志后直接终止进程
macro_rules! fatal {
($msg:expr) => {{ tracing::error!($msg); std::process::exit(1); }};
// 还有一个带格式化参数的分支
}

它在 run_app 绑定/监听失败时兜底(routes/mod.rs:52),保证致命错误不被静默吞掉。

to_local_config(main.rs:93-107)把"命令行传的一个模型名"转成 ModelConfig::Local。 这里藏着两个环境变量约定:非 CPU 设备时读 LLAMA_CPP_N_GPU_LAYERS(默认 9999,即"能上 GPU 的层全上"), 以及 LLAMA_CPP_FAST_ATTENTION 开关。这段把 CLI 与推理后端的参数约定连了起来。

3.2 配置加载与合并:文件 + 命令行的两段式

要解决的小问题: 配置既能写在 config.toml 里(持久),又能被命令行临时覆盖,还要在解析失败时不崩死。

第一段——从文件加载。 Config::load(config.rs:42-89)的逻辑很实在:

  • 配置文件不存在 → 直接返回 Default::default(),即"零配置也能跑"。
  • 存在 → serdeconv::from_toml_file 反序列化;失败会 context 包一层可读错误。
  • 两道校验:validate_dirs(仓库目录不能重复)、validate_config(HTTP 模型的 model_name 必须在 supported_models 里)。目录校验失败回退默认配置并打印警告; 配置校验失败则直接 process::exit(1)——两种失败严重程度不同。

Config 的形状(config.rs:18-39)是理解整个服务的地图,每个字段喂给不同服务:

字段类型喂给谁
repositoriesVec<RepositoryConfig>索引/检索:要索引哪些 git 仓库
serverServerConfigHTTP 层:completion_timeout(默认 30s,config.rs:234)
modelModelConfigGroup三类模型:completion / chat(可选)+ embedding(有默认)
completionCompletionConfig补全服务:max_input_length、检索参数(见第 2 章)
embeddingEmbeddingConfig嵌入服务:max_input_length
answerAnswerConfigEE Answer Engine:系统提示、检索参数(见第 5 章)

ModelConfig 是个二选一枚举(config.rs:278-281):Http(远程 API,如 OpenAI 风格) 或 Local(本地 llama.cpp)。这个 Http vs Local 的分叉贯穿全项目——健康检查、模型加载、 补全服务都要对它 matchModelConfigGroup(config.rs:259)里 completion/chatOption (可以不配),但 embedding 有个默认值 Nomic-Embed-Text(config.rs:247)。

第二段——命令行覆盖。 merge_args(serve.rs:397-418)克隆一份 Config,若命令行给了 --model / --chat-model,就用 to_local_config 造出 ModelConfig::Local 覆盖进去。 若配置文件里本来就有该模型,会 warn! 提示"命令行覆盖行为可能让你意外":

// crates/tabby/src/serve.rs:399 —— 命令行 --model 覆盖 config.toml 里的补全模型
if let Some(model) = &args.model {
if config.model.completion.is_some() {
warn!("Overriding completion model from config.toml. ...");
}
config.model.completion = Some(to_local_config(model, args.parallelism, &args.device));
}

注意: 命令行注入的模型永远是 Local;要用远程 HTTP 模型,只能写在 config.toml 里。

备模型。 load_model(serve.rs:235-249)对 completion / chat / embedding 三处, 凡是 ModelConfig::Local 的就 download_model_if_needed。后者(services/model/mod.rs:83) 很简单:fs::metadata(model) 能打开就当本地路径直接用,否则去下载。embedding 那支还多一道 is_embedding_service_enabled() 门禁(config.rs:156,读环境变量 TABBY_EMBEDDING_ENABLED=yes)。

3.3 服务的依赖注入顺序(本章的精华)

要解决的小问题: 补全服务要用代码检索,代码检索要用 embedding……这些"谁依赖谁"必须按拓扑序造出来, 且每一环都可能因为"没配"而缺席。

看这张依赖图。怎么读:箭头指向"依赖谁";虚线框是可选,缺了下游就跟着变 None

[env] TABBY_EMBEDDING_ENABLED=yes

┌─────▼─────┐ 没开 → embedding = None,下面 code/docsearch 全 None
│ embedding │ serve.rs:132 (Option<Arc<dyn Embedding>>)
└─────┬─────┘
│ 有 embedding 才继续
┌───────┼─────────────────┐
▼ ▼ ▼
index_reader ... ┌──────────┐
_provider ──┬──────────► │ docsearch│ serve.rs:158 文档检索
│ └──────────┘

┌──────┐
│ code │ serve.rs:167 代码检索(CodeSearch)
└──┬───┘
│ code 作为入参

┌─────────────────────────────────┐
│ create_completion_service_and_chat│ serve.rs:177
│ → completion / completion_stream / chat
└──────────────┬──────────────────┘
│ chat 有值才有

┌──────────┐
│ ChatState│ serve.rs:186 (chat_completion + logger)
└──────────┘

逐环说明:

  • embedding(serve.rs:132-136):地基。没开就 None,后面代码/文档检索全塌成 None
  • logger(serve.rs:147):默认 create_event_logger 写本地事件日志;EE 下被 webserver 的 logger 顶替(serve.rs:150),这样事件既写本地又落库。
  • docsearch / code(serve.rs:158-174):都用 embedding.zip(index_reader_provider)—— Rust 的 Option::zip,两者都 Some 才产出,任一为 None 结果就是 None。这是"可选依赖"的惯用写法。
  • completion + chat(serve.rs:177-184):create_completion_service_and_chat (services/completion.rs:538)返回三元组:completion(HTTP 处理器要的服务)、 completion_stream(底层流,给 EE 用)、chat(对话流)。它内部调 load_code_generation_and_chat(services/model/mod.rs:13)真正把模型 load 起来。
  • ChatState(serve.rs:186-191,定义在 routes/chat.rs:38):把 chat 流和 logger 打包,作为 /v1/chat/completions 路由的 state。

一句话精华:整个 serve::main 就是一条"embedding 打底 → 检索 → 补全/对话 → 打包成 state"的 拓扑装配线,每一环都用 Option 表达"可缺席",缺席就在路由层降级成 501。

3.4 api_router:把服务 with_state 到路由,再 merge 成树

要解决的小问题: axum 里每条路由可以带自己的 state。怎么把上面造好的一堆 Option<服务> 挂成一棵路由树,还要处理"服务缺席"的情况?

思路: api_router(serve.rs:251-360)维护一个 routers: Vec<Router>, 每组相关路由建一个小 Routerpush 进去,最后一个循环 mergeroot(serve.rs:355-359)。

路由清单(全在 api_router 内):

路由方法处理器state / 层
/v1/eventsPOSTroutes::log_eventwith_state(logger)
/v1/healthPOST + GETroutes::healthwith_state(health_state)
/v1beta/modelsGETroutes::modelsstate 来自 config.clone().into()(serve.rs:286)
/v1/completionsPOSTroutes::completionswith_state(completion_state) + TimeoutLayer
/v1/chat/completionsPOSTroutes::chat_completionswith_state(chat_state)
/v1beta/chat/completionsPOST同上(前向兼容)同上
/v1beta/server_settingGETroutes::setting无 state(仅非 EE 或 --no-webserver 时挂)

"缺席即降级"的两处细节:

  • completion 缺席(serve.rs:304-311):没有 completion_state 时,路由直接绑 routing::post(StatusCode::NOT_IMPLEMENTED)——axum 允许把一个 StatusCode 当处理器,永远返回 501。
  • chat 缺席(serve.rs:328-342):同理,/v1/chat/completions/v1beta/... 都挂 501 桩。

两个关键的层(Layer):

// crates/tabby/src/serve.rs:290 —— 补全路由加超时层,并按部署形态决定是否注入 AllowedCodeRepository
let mut router = Router::new()
.route("/v1/completions", routing::post(routes::completions).with_state(...))
.layer(TimeoutLayer::new(Duration::from_secs(config.server.completion_timeout)));

if webserver.is_none() || webserver.is_some_and(|x| !x) {
router = router.layer(Extension(AllowedCodeRepository::new_from_config()));
}
  • TimeoutLayer(serve.rs:295):给补全请求套 completion_timeout 秒(默认 30s)的超时。
  • AllowedCodeRepository(serve.rs:299-301):这是"补全请求带的 git_url 能否匹配到已配置仓库" 的白名单。只有非 webserver 部署(社区版或 --no-webserver)才在此注入——EE 模式下由 webserver 自己的中间件提供。它定义在 crates/tabby-common/src/axum.rs:34,new_from_configConfig::load()repositories 构建(axum.rs:43),closest_match 用 git URL 的仓库名做模糊匹配。

3.5 HTTP 路由处理器:提取器与 OpenAPI 标注

要解决的小问题: 每个处理器怎么从请求里"提取"自己要的东西(state、header、body), 又怎么被收进 OpenAPI 文档?

统一模式——axum 提取器。 看最典型的补全处理器:

// crates/tabby/src/routes/completions.rs:26 —— 提取器按类型自动从请求里取值
pub async fn completions(
State(state): State<Arc<CompletionService>>, // 路由 state
Extension(allowed_code_repository): Extension<AllowedCodeRepository>, // 上面注入的层
TypedHeader(MaybeUser(user)): TypedHeader<MaybeUser>, // 自定义 user header
user_agent: Option<TypedHeader<headers::UserAgent>>,
Json(mut request): Json<CompletionRequest>, // 请求体
) -> Result<Json<CompletionResponse>, StatusCode> { ... }

MaybeUser(tabby-common/src/axum.rs:7)是个自定义 header 提取器,从 USER_HEADER_FIELD_NAME 读用户标识,取不到就是 MaybeUser(None)——匿名也能用。

各处理器一句话:

处理器文件干什么
log_eventroutes/events.rs:28记 view/select/dismiss 遥测事件,按 event_type 分派
healthroutes/health.rs:18返回 HealthState(模型/设备/CPU/版本),GET+POST 同一处理器
modelsroutes/models.rs:52返回 HTTP 模型的 supported_models 列表
completionsroutes/completions.rs:26见上,调 state.generate(...)(补全内部见第 2 章)
chat_completionsroutes/chat.rs:44SSE 流式对话,返回 text/event-stream
settingroutes/server_setting.rs:16返回 ServerSetting(社区版恒返回 telemetry 未禁用)

对话是流式的,不是普通 JSON。 chat_completions(routes/chat.rs:44-86)返回 Sse<impl Stream<...>>——服务器发送事件(SSE)。它用 async_stream::stream! 把底层 chat_stream 逐块转成 SSE Event,并特判 "Stream ended" 作为正常结束(chat.rs:70-73)。

OpenAPI 的一处小分裂。 /v1/chat/completions 的真实处理器是 chat_completions, 但它的返回类型(SSE 流)utoipa 描述不了,所以另写了个只为文档存在的桩 chat_completions_utoipa(routes/chat.rs:34,函数体是 unimplemented!()), 在 ApiDocpaths(...) 里登记的是这个桩(serve.rs:60)。真跑的和文档登记的是两个函数,别混。

3.6 OpenAPI / Swagger 与安全方案

要解决的小问题: 自动生成一份可交互的 API 文档,并声明"需要 Bearer token"。

ApiDoc(serve.rs:40-81)用 utoipa 的 #[derive(OpenApi)],在 paths(...) 列出要收录的处理器、 components(schemas(...)) 列出要收录的数据结构,modifiers(&SecurityAddon) 挂安全方案。

SecurityAddon(serve.rs:379-395)实现 Modify,往 OpenAPI 里加一个名为 "token" 的 HTTP Bearer 认证方案。各处理器的 #[utoipa::path(..., security(("token" = [])))] 引用它。

装配处(serve.rs:202-207):ApiDoc::openapi() 生成文档对象;EE 下 doc.merge(EEApiDoc::openapi()) 把企业版接口并进来;SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", doc) 把交互 UI 挂到 /swagger-ui,并加一个 fallback 把根路径 / 重定向过去(serve.rs:207)。

3.7 EE 加挂:ws.attach 怎么把额外 api/ui 挂上

要解决的小问题: 企业版有一大堆社区版没有的东西(GraphQL、后台索引任务、Web 前端、OAuth……), 怎么在不改社区版路由的前提下"叠加"上去?

思路:装饰器模式。 serve::main 已经拼好社区版的 apiui 两棵 Router; EE 版把它俩传进 Webserver::attach,attach 在上面接着挂新路由,返回加强版的 (api, ui)(serve.rs:209-225):

// crates/tabby/src/serve.rs:210 —— 把社区版 api/ui 交给 webserver 加挂,拿回加强版
if let Some(ws) = &ws {
let (new_api, new_ui) = ws.attach(
&config, api, ui,
code, chat, completion_stream, docsearch,
|x| Box::new(services::structured_doc::create_serper(x)), // Serper 搜索工厂
).await;
api = new_api;
ui = new_ui;
}

Webserver::attach(ee/tabby-webserver/src/webserver.rs:59-159)做两件事: 先造出一堆企业级服务(job / integration / repository / auth / license / answer……), 再把 api、ui 和这些服务交给 routes::create(ee/tabby-webserver/src/routes/mod.rs:35)去挂路由。

routes::create(ee/.../routes/mod.rs:35-122)在 api 上加挂的关键路由:

路由作用
/graphql(POST/GET)GraphQL 端点 + playground
/subscriptionsGraphQL 订阅(WebSocket)
/hubworker 节点接入的 WebSocket hub
/v1beta/ingestion内容摄取 API(注册 token 保护)
/repositories/oauth/avatar/{id}仓库浏览、OAuth 回调、头像
ui 的 fallback交给 ui::handler 渲染前端页面

两层认证中间件(ee/.../routes/mod.rs:124-159):require_login_middleware 校验 Bearer JWT; distributed_tabby_layer/v1/*/v1beta/* 的请求交给 worker 分发。企业版内部见 第 5 章,本章只需知道"attach 是叠加,不是替换"。

3.8 run_app:收尾层与开始监听

要解决的小问题: 路由树拼好了,还差全局中间件和真正的端口绑定。

run_app(routes/mod.rs:14-53)是最后一站,给整棵 api 加三样全局的东西,再把 ui merge 进来:

api(路由树)
│ .layer(CorsLayer::permissive()) 允许跨域(IDE 插件从各处来)
│ .layer(prometheus_layer) 请求指标采集
│ .route("/metrics", metrics) 暴露 Prometheus 指标
│ .merge(ui) 把 swagger/前端 UI 并进来

SocketAddr::from((host, port))
│ TcpListener::bind(address)

axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
└─ 失败 → fatal!("Error happens during serving: ...") routes/mod.rs:52

into_make_service_with_connect_info::<SocketAddr>()(routes/mod.rs:49)让处理器能拿到客户端地址。 绑定/监听出错时用 3.1 的 fatal! 宏打日志并退出。至此进程进入监听态,骨架装配完成。

旁路:心跳。 run_app 之前 start_heartbeat(serve.rs:362-377)会 spawn 一个后台任务, 每 3000 秒用 usage::capture("ServeHealth", &state) 上报一次匿名健康遥测——与请求处理并行,不在主路由树里。


4. 巧妙之处(可借鉴的技术)

  • Option + Option::zip 表达"可选依赖链"。 embedding 缺席则 code/docsearch 自动 None (serve.rs:158-174),路由层再降级成 501 桩(serve.rs:304-342)。整条链无需 if/else 满天飞, 类型系统替你保证"上游没有,下游就没有"。

  • StatusCode 直接当处理器。 routing::post(StatusCode::NOT_IMPLEMENTED)(serve.rs:308) ——服务未配置时一行代码挂一个恒定 501,比写个空 handler 干净。

  • "文档桩函数"绕开类型描述难题。 SSE 流无法被 utoipa 描述,于是真处理器与文档处理器分离: 真跑 chat_completions,文档登记 chat_completions_utoipa(routes/chat.rs:34)。

  • attach 装饰器让社区版/企业版共用一套装配。 社区版拼好 api/ui,EE 版 ws.attach 在其上叠加(serve.rs:210),两条编译路径(#[cfg(feature = "ee")])共享前半段主线。

  • 命令行覆盖配置时主动 warn! merge_args(serve.rs:401)在覆盖 config.toml 已有模型时 提醒用户"这行为可能让你意外"——把隐式覆盖变显式。


5. 边界与局限(本章范围内)

  • 命令行只能注入 Local 模型。 to_local_config(main.rs:106)恒产出 ModelConfig::Local; 远程 HTTP 模型必须写在 config.toml

  • embedding 默认关闭。 不设 TABBY_EMBEDDING_ENABLED=yes,代码/文档检索整条链都是 None (config.rs:156serve.rs:132),补全就退化为"无仓库上下文"。

  • CORS 全开。 CorsLayer::permissive()(routes/mod.rs:17)对所有来源放行——面向"自托管、 内网 IDE 插件"的假设;暴露到公网需自行加固。

  • 配置校验失败的两种结局不一致。 目录重复 → 回退默认并继续;模型名不在白名单 → 直接 exit(1)(config.rs:71-86)。行为差异是有意为之,但对使用者不算显然。

  • 本章不覆盖: 补全/FIM 提示组装(第 2 章)、tree-sitter 切片与 Tantivy 检索(第 3 章)、 推理后端与停止条件(第 4 章)、EE 服务内部与 GraphQL schema(第 5 章)。本章只到"路由边界"为止。


6. 代码地图(导航索引)

主题文件路径符号名
CLI 顶层与子命令crates/tabby/src/main.rsCli / Commands / Device
进程入口crates/tabby/src/main.rsmain
致命错误宏crates/tabby/src/main.rsfatal!
CLI→本地模型配置crates/tabby/src/main.rsto_local_config
serve 子命令参数crates/tabby/src/serve.rsServeArgs
服务装配总指挥crates/tabby/src/serve.rsmain(serve)
配置合并crates/tabby/src/serve.rsmerge_args
模型预备/下载crates/tabby/src/serve.rsload_model
路由装配crates/tabby/src/serve.rsapi_router
OpenAPI 文档定义crates/tabby/src/serve.rsApiDoc / SecurityAddon
后台心跳遥测crates/tabby/src/serve.rsstart_heartbeat / try_run_spinner
收尾层 + 监听crates/tabby/src/routes/mod.rsrun_app
配置模型与加载crates/tabby-common/src/config.rsConfig / Config::load / ModelConfig / ModelConfigGroup / ServerConfig
完成/嵌入/回答配置crates/tabby-common/src/config.rsCompletionConfig / EmbeddingConfig / AnswerConfig / is_embedding_service_enabled
仓库白名单 & user headercrates/tabby-common/src/axum.rsAllowedCodeRepository / new_from_config / MaybeUser
补全路由处理器crates/tabby/src/routes/completions.rscompletions
对话路由(SSE)+ 文档桩crates/tabby/src/routes/chat.rschat_completions / chat_completions_utoipa / ChatState
事件遥测路由crates/tabby/src/routes/events.rslog_event
健康检查路由 + 状态crates/tabby/src/routes/health.rsservices/health.rshealth / HealthState / Version
模型列表路由crates/tabby/src/routes/models.rsmodels / ModelInfo
补全服务工厂crates/tabby/src/services/completion.rscreate_completion_service_and_chat
模型加载分发crates/tabby/src/services/model/mod.rsload_code_generation_and_chat / download_model_if_needed
EE 加挂入口ee/tabby-webserver/src/webserver.rsWebserver::new / Webserver::attach
EE 路由装配ee/tabby-webserver/src/routes/mod.rscreate / require_login_middleware / distributed_tabby_layer