尧图精选

Resume Matcher 接入 OpenAI-Compatible 本地大模型:从 Settings 到 LiteLLM 路由的完整实现剖析

🕒 发布时间:2026/9/10 20:15:25 📁 来源:尧图网络
Resume Matcher 接入 OpenAI-Compatible 本地大模型从 Settings 到 LiteLLM 路由的完整实现剖析【免费下载链接】Resume-MatcherThe #1 AI Harness for Building Resumes, PDFs, Cover Letters more, locally with 100 LLMs support.项目地址: https://gitcode.com/GitHub_Trending/re/Resume-Matcher本文基于 openai-compatible-provider-design 设计文档issue #751展开围绕 Resume Matcher 如何为 llama.cpp、vLLM、LM Studio 等本地 OpenAI 兼容服务器提供一等公民的接入入口。读完本文你将掌握Settings 页面显式openai_compatible选项的设计动机、后端如何通过 LiteLLM 的openai/前缀完成模型路由、api_base的/v1保真归一化逻辑、无密钥场景下的sk-no-key哨兵机制以及密钥按 provider 隔离存储的完整链路。背景与问题为什么需要一个显式的 OpenAI-Compatible 入口在 #747 修复_normalize_api_base之后本地 OpenAI 兼容服务器llama.cpp、vLLM、LM Studio、Ollama 的 OpenAI endpoint 等其实已经可以工作用户选择provideropenai并把api_base指向本地 URL 即可。但这个路径只能通过阅读代码才能发现——Settings 界面暴露的六个 provideropenai、anthropic、openrouter、gemini、deepseek、ollama中没有一个标注为 OpenAI-compatible。设计文档指出了由此产生的真实痛点新用户想连接 llama.cpp看到选项里只有一个本地语义的 provider——Ollama于是选择了 Ollama填入错误的 base URL最终撞上 404。这个问题的本质是能力映射与产品可发现性之间的缺口底层能力早已具备OpenAI 兼容协议 LiteLLM 路由缺的只是 UI 上的显式入口与正确的默认值引导。从当前源码可以印证这一点llm.py 的_normalize_api_base的 docstring 明确写着 For theopenaiprovider, LiteLLM uses the upstream OpenAI client which handles/v1correctly — we MUST preserve whatever the user pasted so that OpenAI-compatible endpoints like llama.cpp (http://localhost:8080/v1) round-trip intact. See issue #751说明兼容能力是先行修复的而 UI 层入口是 #751 补全的另一半。设计目标与非目标设计文档对这次改动划定了清晰的边界目标在 Settings UI 中增加显式openai_compatibleprovider 选项并附上面向本地服务器的清晰标注通过 LiteLLM 的openai/前缀路由请求——这是 LiteLLM 官方文档给出的访问 OpenAI 兼容端点的标准方式保留现有provideropenai 自定义 api_base路径继续可用不做静默迁移、不强制切换openai_compatible的 API key 存放在独立命名空间与真实 OpenAI 的 key 互不泄漏。非目标不自动检测后端能力流式、工具调用等不做 Settings provider profile 抽象——一个 provider 一份配置不迁移已使用openai api_base的存量用户。这种纯增量的定位为回滚和安全评估提供了便利后文会看到它如何落实。后端实现五个关键改动点1. Provider 枚举为llm_provider增加合法值后端配置模型 config.py 的Settings.llm_provider使用Literal类型限定合法 provider 值llm_provider: Literal[ openai, openai_compatible, anthropic, openrouter, gemini, deepseek, groq, ollama, ] openai设计文档中的枚举为 7 个值当前仓库源码在此基础上还加入了groq说明该 Literal 仍在持续扩展。值得注意的是同文件中set_default_provider校验器config.py当读取到空字符串或未知值时会兜底回退到openai——这正是设计文档Rollback一节所依赖的容错机制一旦回滚移除该选项已配置openai_compatible的用户会在下次配置加载时看到unknown provider错误并被自动回退而他们存储的api_base仍然保留。2. Provider key map密钥按 provider 隔离存储llm.py 的_PROVIDER_KEY_MAP维护了 LLM provider 与密钥存储命名空间之间的映射_PROVIDER_KEY_MAP: dict[str, str] { openai: openai, openai_compatible: openai_compatible, anthropic: anthropic, gemini: google, openrouter: openrouter, deepseek: deepseek, groq: groq, ollama: ollama, }openai_compatible拥有自己独立的命名空间因此用户存在openai名下的密钥不会自动出现在openai_compatible的配置中反之亦然。这是刻意为之二者在逻辑上是不同的 provider。更值得关注的是密钥解析函数resolve_api_keyllm.py中的安全规则。它定义了一个特殊的集合_PROVIDERS_WITHOUT_ENV_KEY_FALLBACK: frozenset[str] frozenset( {openai_compatible, ollama} )对于openai_compatible和ollamaresolve_api_key不会回退到环境变量级默认值settings.llm_api_key。理由非常实际如果用户本地跑了一个不需要鉴权的服务器而环境里恰好配置了一把付费的 OpenAI keyLLM_API_KEY那么这把付费 key 就会在用户不知情的情况下被发送到本地端点——这是典型的安全泄漏。设计文档虽然没有显式列出这条规则但它与文档API keys 存储在独立命名空间、不互相泄漏的目标一脉相承并且有对应测试锁定行为见下文测试验证一节。密钥的存储链路也不容忽视config.py 的load_config_file会从加密的 SQLite 存储中注入api_keys字典而save_config_fileconfig.py在写盘前会把api_keys和遗留的api_key全部剥离——密钥只存在于加密存储中绝不落盘到config.json。3. 模型路由openai/前缀是访问兼容端点的关键LiteLLM 访问 OpenAI 兼容端点的官方姿势是modelopenai/model_nameapi_baseURL。在 llm.py 的get_model_name中provider_prefixes表为openai_compatible显式指定了前缀provider_prefixes { openai: , # OpenAI models dont need prefix openai_compatible: openai/, # explicit — users model names usually lack the prefix anthropic: anthropic/, openrouter: openrouter/, gemini: gemini/, deepseek: deepseek/, groq: groq/, ollama: ollama_chat/, # ollama_chat/ routes to /api/chat (supports messages array) }因此用户填写的模型名llama-3.1-8b会被转换成openai/llama-3.1-8b。同时already prefixed 检查的known_prefixes列表也扩展了openai/known_prefixes [ openrouter/, anthropic/, gemini/, deepseek/, groq/, ollama/, ollama_chat/, openai/, ]如果用户手动填写的模型名已经带有openai/前缀则会被尊重、不会被二次加前缀——这正是设计文档Error handling表中Model name includesopenai/already → Respected, not double-prefixed一行的实现。4. URL 归一化/v1原样保留_normalize_api_base是 #747 的核心修复点。它按 provider 采取不同的归一化策略openai / openai_compatible原样返回仅去除尾部斜杠因为 OpenAI 官方客户端能够正确解析/v1路径用户粘贴的http://localhost:8080/v1必须完整保真否则请求会 404anthropic / gemini / openrouterLiteLLM 内部会追加/v1/...因此若 base 已以/v1结尾则剥掉避免/v1/v1/messages这类重复路径ollama不适用/v1路径会依次剥离用户可能粘贴的/v1、/api/chat、/api/generate、/api后缀。if provider in (openai, openai_compatible): return base or None这个分支确保了openai_compatible与openai走同一套保真策略是本地服务器链路不 404 的基石。5. API key 需求sk-no-key哨兵真实 OpenAI 必须配置 key而 llama.cpp、LM Studio 这类本地服务器通常不需要鉴权。但 OpenAI 的 Python 客户端会校验 key 字符串非空空字符串会直接报错。设计文档给出的解法分两步健康检查门控放宽llm.py 的check_llm_healthif config.provider not in (ollama, openai_compatible) and not config.api_key: return { healthy: False, provider: config.provider, model: config.model, error_code: api_key_missing, }openai_compatible与ollama一样被排除在必须要有 key的检查之外。实际调用时的哨兵注入llm.py_OPENAI_COMPATIBLE_SENTINEL sk-no-key def _effective_api_key(provider: str, api_key: str) - str: if provider openai_compatible and not api_key: return _OPENAI_COMPATIBLE_SENTINEL return api_key当用户对openai_compatible留空 key 时后端传入sk-no-key字符串以满足 OpenAI 客户端的非空校验同时不泄漏任何真实凭据。这个哨兵最终会出现在Authorization头中本地不做鉴权的服务器会直接忽略它做鉴权的服务器则会拒绝——但这类用户本来就会配置真实 key因此不会构成回归。_effective_api_key在_build_routerllm.py和健康检查两条路径上都被调用保证一致性。前端实现Settings 页面的四个动作1. 类型与展示信息config.ts中的三处声明apps/frontend/lib/api/config.ts 中LLMProvider联合类型加入openai_compatibleexport type LLMProvider | openai | openai_compatible | anthropic | openrouter | gemini | deepseek | groq | ollama;PROVIDER_INFO字典config.ts则为该 provider 补充了面向用户的展示元数据这是设计文档第 7 点的落地当前仓库的最终形态为openai_compatible: { name: OpenAI-Compatible (Local), defaultModel: custom-model, requiresKey: false, },注意三点与设计文档的差异实际实现将显示名细化为OpenAI-Compatible (Local)以强化本地语义默认模型从custom-model开始用户需按本地服务器实际暴露的模型名填写如 llama.cpp 下的llama-3.1-8brequiresKey: false驱动 Settings 页面隐藏API key 必填的校验见 settings/page.tsx 的handleSave/settings/page.tsx#L403-L413)if (requiresApiKey !apiKey.trim() !hasStoredApiKey)才会报错。此外config.ts 还提供了llmProviderToKeyProviderconfig.ts映射前端 provider 与密钥存储命名空间gemini → google其余透传与后端_PROVIDER_KEY_MAP保持一致。2. 分段按钮provider 列表自动增长settings/page.tsx 的PROVIDERS数组/settings/page.tsx#L70-L79) 是分段按钮segmented button的数据源openai_compatible排在第二位const PROVIDERS: LLMProvider[] [ openai, openai_compatible, anthropic, openrouter, gemini, deepseek, groq, ollama, ];设计文档特别指出按钮行会随PROVIDERS增长而自动换行7 个 provider 在窄屏下会流动到两行属于可接受范围。当前仓库已增至 8 个 provider新增 groq该策略依然成立。3. api_base 预填零配置连接的最后一公里设计文档第 9 点是关键的可用性细节当用户选择openai_compatible且api_base字段为空时预填http://localhost:8080/v1llama.cpp 的默认端口。这在 settings/page.tsx 的handleProviderChange/settings/page.tsx#L383-L400) 中实现if (newProvider openai_compatible !apiBase.trim()) { // llama.cpp default; user can override for vLLM / LM Studio / etc. setApiBase(http://localhost:8080/v1); }同函数对 ollama 也有类似的预填逻辑http://localhost:11434。这个细节与后端的/v1保真归一化形成配合预填值含/v1后端不做剥除最终 LiteLLM 路由到http://localhost:8080/v1/chat/completions。4. 密钥独立存储切换 provider 不再互相覆盖Settings 页面的密钥保存逻辑handleSave/settings/page.tsx#L415-L434)将用户新输入的 key 通过PUT /config/api-keys写入按 provider 隔离的加密存储llmProviderToKeyProvider(provider)决定命名空间而非旧的共享api_key槽位非密钥配置provider / model / api_base / reasoning_effort才走PUT /config/llm-api-key。注释中明确记载了这次修复的动机旧实现把 key 写在共享 config 槽位上导致保存一个 provider 会抹掉另一个 provider 的 key。设计文档Saved keys are separate的验证点由此得到保证。API 层与端到端数据流配置相关的后端路由集中在 apps/backend/app/routers/config.pyGET /config/llm-api-key返回当前配置key 脱敏PUT /config/llm-api-key更新非密钥配置。注意 update_llm_config 明确不再写入request.api_key——密钥只存在于PUT /config/api-keys的加密存储中POST /config/llm-test使用请求体或已存配置做预保存联通性测试前端handleTestConnection在保存前调用GET/POST /config/api-keys、DELETE /config/api-keys/{provider}按 provider 管理密钥SUPPORTED_PROVIDERSconfig.py包含了openai_compatible。设计文档给出的数据流在当前实现中完整成立User picks OpenAI-Compatible in Settings └─ api_base pre-filled to http://localhost:8080/v1 └─ API key field optional (requiresKey: false) └─ PUT /api/v1/config/llm-api-key {provider: openai_compatible, model: llama-3.1-8b, api_base: ..., api_key: } └─ stored.api_keys[openai_compatible] (own namespace) └─ get_llm_config → LLMConfig(provideropenai_compatible, ...) └─ get_model_name → openai/llama-3.1-8b └─ _normalize_api_base → http://localhost:8080/v1 (preserved) └─ check_llm_health → passes api_keysk-no-key sentinel if empty └─ litellm.acompletion(modelopenai/llama-3.1-8b, api_base..., api_keysk-no-key) └─ LiteLLM routes via OpenAI client → http://localhost:8080/v1/chat/completions值得补充的是健康检查返回的响应模型名会通过response_model字段回传check_llm_healthSettings 页面据此展示模型输出和推理内容reasoning_content / thinking 回退为用户提供直观的联通确认。错误处理矩阵设计文档的 Error handling 表格在当前实现中逐条对应失败场景行为源码依据openai_compatible的api_key为空允许。哨兵传给 LiteLLM由服务器决定是否鉴权_effective_api_keyllm.py服务器在/v1/chat/completions返回 404健康检查表面not_found_404错误码check_llm_health 异常分支api_base含/v1/v1交给 LiteLLM 正常行为处理不做额外剥除_normalize_api_base的保真策略模型名已含openai/尊重原值不二次加前缀known_prefixes检查llm.py此外健康检查的异常分支还提供两类辅助错误码duplicate_v1_path404 且消息含/v1/v1/和html_response服务器返回 HTML 页面而非 JSON 响应常见于误指向了网页服务。所有上游异常消息在返回前端前会经过_scrub_secretsllm.py脱敏防止任何形如sk-...或AIza...的密钥片段被回显。环境变量与文档化不开应用也能配除了 Settings 界面openai_compatible也可以通过环境变量配置。apps/backend/.env.example 提供了完整的注释与示例# For llama.cpp / vLLM / LM Studio (OpenAI-compatible local servers) # LLM_PROVIDERopenai_compatible # LLM_MODELllama-3.1-8b # or whatever model your server exposes # LLM_API_BASEhttp://localhost:8080/v1 # llama.cpp default; adjust per server # LLM_API_KEY # leave blank if your server doesnt require authSETUP.md 的 AI Provider 配置表中也增加了对应行ProviderConfigurationGet API KeyOpenAI-CompatibleLLM_PROVIDERopenai_compatibleLLM_MODELllama-3.1-8bLLM_API_BASEhttp://localhost:8080/v1— (local)并附注OpenAI-Compatible targets any local server that exposes the OpenAI Chat Completions API — llama.cpp, vLLM, LM Studio, etc. API key is optional.关于 Docker 部署还有一个值得注意的细节同样出现在 .env.example 中宿主机上运行 Ollama/llama.cpp 时容器内应使用host.docker.internal而非localhostLinux 上需使用宿主机 IP 或--networkhost。对于本地大模型REQUEST_TIMEOUT_SECONDS 通常需要从默认的 240 秒调大上限 1800 秒并且必须与前端NEXT_PUBLIC_REQUEST_TIMEOUT_MS同步修改否则较短的某一层会先中断请求。测试验证从单元测试到真实服务器设计文档Verification一节给出的是手动验证步骤当前仓库实际上已经沉淀了自动化的测试保障设计文档no test infrastructure的状态已被后续演进超越。单元测试apps/backend/tests/unit/test_llm_providers.py 精确定位了本地 LLM 路由的四个关键行为前缀路由get_model_name(_cfg(openai_compatible, llama-3.1-8b)) openai/llama-3.1-8b/v1保真_normalize_api_base(openai_compatible, http://localhost:8080/v1) http://localhost:8080/v1仅剥离尾部斜杠密钥隔离resolve_api_key({}, openai_compatible) 即使环境里配置了sk-paid-secret也不会继承哨兵注入_effective_api_key(openai_compatible, ) sk-no-key而真实 key 原样透传。集成测试test_config_api.py覆盖了openai_compatible配置的保存与回读test_health_api.py的test_status_openai_compatible_is_configured_without_key验证了无 key 也能被识别为已配置test_llm_contract.py 则用 respx 模拟 OpenAI Chat Completions 响应验证openai_compatible的完整 HTTP 链路issue #751 的契约测试。手动验证步骤来自设计文档适用于本地实操在 8080 端口启动 llama.cpp并开启 OpenAI server 模式在 Settings 中选择 OpenAI-Compatible确认api_base自动预填为http://localhost:8080/v1保持 API key 为空选择模型llama-3.1-8b点击 Test Connection应显示 healthy 及模型输出切回openai并填入真实 key确认官方 OpenAI API 仍可正常连通检查已保存的 key 相互独立清空openai的 key 不会影响openai_compatible的设置。风险与回滚设计文档明确列出了三个风险点均已在实现中得到控制哨兵 keysk-no-key仅作为满足 OpenAI 客户端非空校验的字面字符串传入Authorization头。不做鉴权的本地服务器直接忽略做鉴权的服务器会拒绝——但这类用户本来就会配置真实 key因此不存在回归。密钥命名空间隔离的副作用已有openai密钥的用户切换到openai_compatible时不会看到该 key 自动填充需要手动粘贴或留空。这是刻意设计因为二者逻辑上是不同 provider。无新增失败路径完全复用现有的not_found_404/duplicate_v1_path/html_response错误启发式。回滚是纯增量的撤销改动只是让该选项从下拉框中消失已配置openai_compatible的用户会在下次配置加载时触发set_default_provider校验器config.py回退到openai其存储的api_base依然保留因此只需切回openai api_base...即可恢复原有可用路径。小结回顾整个设计openai_compatible的落地遵循了一条清晰的增量路径后端先行修复api_base归一化#747随后通过 provider 枚举、前缀路由、哨兵密钥与命名空间隔离完成能力层#751最后以 Settings UI 的显式选项、默认值预填与文档化收尾。对于用户而言连接 llama.cpp / vLLM / LM Studio 的成本从读源码猜配置降为选一个选项、留空密钥、测试连通对于开发者而言全部关键决策都有源码、测试与配置三重印证可作为后续接入其他 OpenAI 兼容生态推理服务、代理网关等的参考模板。关键文件索引设计文档docs/superpowers/specs/2026-04-17-openai-compatible-provider-design.md后端 provider 枚举apps/backend/app/config.py后端路由/密钥/哨兵apps/backend/app/llm.py、apps/backend/app/llm.py配置 API 路由apps/backend/app/routers/config.py前端 provider 定义apps/frontend/lib/api/config.ts前端 Settings 页面apps/frontend/app/(default)/settings/page.tsx/settings/page.tsx#L383-L400)单元测试apps/backend/tests/unit/test_llm_providers.py环境变量示例apps/backend/.env.example安装配置指南SETUP.md【免费下载链接】Resume-MatcherThe #1 AI Harness for Building Resumes, PDFs, Cover Letters more, locally with 100 LLMs support.项目地址: https://gitcode.com/GitHub_Trending/re/Resume-Matcher创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →