CodeCompanion Adapter 架构全解析:Handler 结构、向后兼容与 HTTP 客户端实现
2026/9/17 6:17:47 网站建设 项目流程

CodeCompanion Adapter 架构全解析:Handler 结构、向后兼容与 HTTP 客户端实现

【免费下载链接】codecompanion.nvim✨ AI Coding, Vim Style项目地址: https://gitcode.com/GitHub_Trending/co/codecompanion.nvim

导读:CodeCompanion 通过 Adapter 屏蔽不同 LLM/Agent 提供商的差异,使 ChatGPT、Anthropic、OpenAI、Ollama、Gemini 等数十种后端能以统一方式接入聊天缓冲区。本文以仓库文档 .codecompanion/adapters/adapters.md 为骨架,结合 lua/codecompanion/adapters 目录下的真实实现,系统讲解 Adapter 的嵌套 Handler 结构、规范工具结果格式、call_handler()的向后兼容机制、HTTP 客户端 http.lua 的驱动流程,并通过 OpenAI Responses 与 Anthropic 两个实例对比新旧两种 Handler 写法。读完你将能够理解现有内置 Adapter 的运行原理,并具备自定义、扩展和调试 Adapter 的完整知识。

一、Adapter 是什么:连接 LLM 与 Agent 的桥梁

在 CodeCompanion 中,Adapter 用于连接 LLM 或 Agent。HTTP 类型的 Adapter 包含两类核心内容:

  1. LLM 端点(endpoint)选项:请求 URL、请求头、环境变量、额外的 curl 参数等;
  2. schema 参数定义:对modeltemperaturetop_ktop_p等模型采样属性的声明式配置,用户可以在聊天缓冲区中直接调整这些参数。

同时,HTTP Adapter 还包含一组handler 函数,它们定义了发送给 LLM 的消息应该如何格式化(例如将聊天缓冲区中的消息转换为 Anthropic 的tool_use/tool_result块),以及 LLM 返回的输出应如何被接收并展示在聊天缓冲区中。

所有 Adapter 都定义在 lua/codecompanion/adapters 目录下,其中:

  • lua/codecompanion/adapters/http 存放 HTTP 类 Adapter(anthropic.luaopenai.luaopenai_responses.luagemini.luaollama.luadeepseek.luamistral.lua等);
  • lua/codecompanion/adapters/acp 存放 ACP(Agent Client Protocol)类 Adapter(claude_code.luacodex.luagemini_cli.luaopencode.lua等)。

从 adapters/init.lua 的adapter_type()函数可以看出,一个 Adapter 的类型判定遵循以下顺序:

  • 未指定时,取配置项config.interactions.chat.adapter的默认值;
  • 若传入的是带type字段的 table,则直接使用该类型;
  • 否则按名称在config.adapters.acpconfig.adapters.http两张表中查找,命中即返回对应类型;
  • 兜底类型为http

在 config.lua 中可以看到内置的完整注册表:HTTP 侧包括anthropicazure_openaicopilotdeepseekgeminigithubmodelshuggingfacekiminovitamistralollamaopenaiopenai_responsesopenrouterxaijinatavily;ACP 侧包括auggie_clicagentclaude_codecline_clicodexcursor_clicopilot_acpgemini_cligoosekimi_clikiromistral_vibeopencode。两者都支持extend表用于按 Adapter 做配置覆盖,以及opts表用于全局选项(如allow_insecurecache_models_forproxyshow_presetsshow_model_choices)。

一个解析后的 HTTP Adapter 是一个CodeCompanion.HTTPAdapter对象,其字段在 adapters/http/init.lua 的类注释中有完整定义:namevendortypeformatted_nameavailable_toolsroles(角色映射)、featuresurlenv/env_replaced(环境变量及替换结果)、bodyheadersparametersraw(额外 curl 参数)、handlersmeta(上下文窗口等模型元数据)、methodsmodeloptsschematemp(不随请求发送的临时存储)。

二、Handler 结构:按职责分组的嵌套设计

Adapter 使用嵌套的 handler 结构,按用途组织函数。完整结构如下(摘自文档,字段注释与 http/init.lua 中的类注释保持一致):

handlers = { -- Lifecycle hooks (side effects) lifecycle = { ---Called when adapter is resolved ---@param self CodeCompanion.HTTPAdapter ---@return boolean success setup = function(self) end, ---Called after request completes ---@param self CodeCompanion.HTTPAdapter ---@param data table ---@return nil on_exit = function(self, data) end, ---Called during adapter cleanup ---@param self CodeCompanion.HTTPAdapter ---@return nil teardown = function(self) end, }, -- Request builders (pure transforms) request = { ---Build request parameters ---@param self CodeCompanion.HTTPAdapter ---@param params table ---@param messages table ---@return table build_parameters = function(self, params, messages) end, ---Build message format for LLM ---@param self CodeCompanion.HTTPAdapter ---@param messages table ---@return table build_messages = function(self, messages) end, ---Build tools schema ---@param self CodeCompanion.HTTPAdapter ---@param tools table ---@return table|nil build_tools = function(self, tools) end, ---Build reasoning parameters (for models that support it) ---@param self CodeCompanion.HTTPAdapter ---@param messages table ---@return nil|{ content: string, _data: table } build_reasoning = function(self, messages) end, ---Set additional body parameters ---@param self CodeCompanion.HTTPAdapter ---@param data table ---@return table|nil build_body = function(self, data) end, }, -- Response parsers (pure transforms) response = { ---Parse chat response ---@param self CodeCompanion.HTTPAdapter ---@param data string|table ---@param tools? table ---@return { status: string, output: table }|nil parse_chat = function(self, data, tools) end, ---Parse inline response ---@param self CodeCompanion.HTTPAdapter ---@param data string|table ---@param context? table ---@return { status: string, output: string }|nil parse_inline = function(self, data, context) end, ---Extract token count ---@param self CodeCompanion.HTTPAdapter ---@param data table ---@return number|nil parse_tokens = function(self, data) end, }, -- Tool handlers (grouped functionality) tools = { ---Format tool calls for inclusion in request ---@param self CodeCompanion.HTTPAdapter ---@param tools table ---@return table format_calls = function(self, tools) end, ---Format tool response for LLM ---@param self CodeCompanion.HTTPAdapter ---@param tool_call table ---@param output string ---@return table format_response = function(self, tool_call, output) end, }, }

(旧版扁平结构中的form_structured_output/parse_message_meta等函数在新结构中同样有对应映射,详见下文“向后兼容”。)

这种结构实现了清晰的关注点分离:

  • lifecycle:副作用与初始化(setupteardown、请求完成后的清理on_exit);
  • request:构建请求的纯变换(参数、消息、工具、结构化输出、推理、body);
  • response:解析响应的纯变换(聊天输出、内联输出、token 数);
  • tools:工具相关的操作(格式化工具调用与工具结果)。

这样的划分让每个函数职责单一、便于单测,也降低了接入新提供商时的心智负担——你只需要实现对应类目下的函数,其余由框架调用。

三、规范的工具结果结构(Canonical Tool-Result Shape)

在插件内部,由format_response产生的工具结果消息会存储在chat.messages中,并且会被每一个Adapter 的build_messages/form_messages重新读取。为了让消息在不同 Adapter 之间可移植(例如把一段包含工具调用的对话从一个提供商切换到另一个),所有 Adapter 都必须写入同一种规范结构:

{ role = "tool", content = output, tools = { call_id = tool_call.id, -- required: matches the LLM's tool call name = tool_call["function"].name, -- required: function name (Gemini uses this) is_error = false, -- optional: Anthropic uses this }, opts = { visible = false }, }

字段说明:

  • role固定为"tool",是读取方判断工具结果的唯一依据;
  • content为工具执行后的输出文本;
  • tools.call_id是必填字段,必须与 LLM 发出的工具调用 ID 一一对应(Anthropic 用它映射tool_use_id,OpenAI Responses 用它映射function_call_outputcall_id);
  • tools.name是必填的函数名,Gemini 依赖它做匹配;
  • tools.is_error为可选字段,Anthropic 用它标记工具执行失败(会在tool_result中携带is_error);
  • opts.visible = false告诉聊天缓冲区这条工具结果不需要展示给用户。

Adapter 特有的扩展字段(例如 OpenAI Responses 的id)是允许的,但会被其他 Adapter 忽略。在回读这些消息时,Adapter 应当只依据role == "tool"来识别工具结果,绝不要以任何 Adapter 特有的字段作为判断条件。

以 openai_responses.lua 的format_response为例,它返回的正是上述规范形状(额外附带了tools.id);anthropic.lua 的output_response则额外携带tools.is_error = false,并在注释中说明role刻意设为"tool"是为了在form_messages中更易识别并与 user 消息合并。

四、调用 Handler:call_handler()与向后兼容机制

在整个插件中,handler 通过adapters.call_handler()函数调用,该函数负责向后兼容:

local adapters = require("codecompanion.adapters") -- Call a handler local result = adapters.call_handler(adapter, "parse_chat", data, tools) local tokens = adapters.call_handler(adapter, "parse_tokens", data) -- Handler automatically receives adapter as first argument local setup_ok = adapters.call_handler(adapter, "setup")

其实现位于 adapters/init.lua:先通过get_handler()解析出真正的 handler 函数,若存在则以handler(adapter, ...)形式调用——也就是说adapter 自身总是作为第一个参数自动传入,无需手动传递。

get_handler()的解析逻辑在 adapters/http/init.lua,核心分两步:

  1. 新格式:通过uses_new_handlers()检测(判断handlers.lifecyclehandlers.requesthandlers.response任一存在),然后在lifecyclerequestresponsetools四个类目中依次查找同名函数;
  2. 旧格式:按下表把新名字映射回旧名字后,在扁平的handlers表中查找:
新格式名称旧格式名称所属类目
setup/on_exit/teardownsetup/on_exit/teardownlifecycle
build_parametersform_parametersrequest
build_messagesform_messagesrequest
build_toolsform_toolsrequest
build_structured_outputform_structured_outputrequest
build_bodyset_bodyrequest
build_reasoningform_reasoningrequest
parse_chatchat_outputresponse
parse_inlineinline_outputresponse
parse_tokenstokensresponse
parse_metaparse_message_metaresponse
format_callsformat_tool_callstools
format_responseoutput_responsetools

旧格式示例(仍然受支持):

-- Old format (still supported) handlers = { setup = function(self) end, form_parameters = function(self, params, messages) end, form_messages = function(self, messages) end, chat_output = function(self, data, tools) end, tools = { format_tool_calls = function(self, tools) end, output_response = function(self, tool_call, output) end, } }

注意tools命名空间在新旧两种格式中都一直存在,因此不能仅凭tools是否存在来判断格式,检测必须看lifecyclerequestresponse这三个类目。

五、工厂方法:Adapter 的解析、扩展与安全序列化

adapters/init.lua 对外暴露一组工厂方法,统一分发到 http 或 acp 实现:

方法作用
resolve(adapter, opts)将字符串名、table 或函数解析为CodeCompanion.HTTPAdapter/CodeCompanion.ACPAdapter对象
resolved(adapter)判断 Adapter 是否已经完成解析(检查 metatable 是否为 Adapter 类,见 http/init.lua)
extend(adapter, opts)在既有 Adapter 配置上深合并用户自定义选项后生成新 Adapter
make_safe(adapter)生成适合序列化的精简副本(过滤掉schema.model,避免递归问题,见 http/init.lua)
set_model(args)便捷方法,将 schema 中的模型默认值/选择表写入adapter.model
call_handler(adapter, name, ...)向后兼容的 handler 调用入口

resolve的完整流程(http/init.lua)值得细读:

  1. 未传 Adapter 时使用config.interactions.chat.adapter默认值;
  2. 若传入的是已解析的 table(带nameschemaresolved()为真),直接复用并调用set_model
  3. 若传入的是{ name = "...", model = "..." }形式的 table,则递归解析name并附带指定model
  4. 若传入的是字符串,先尝试require("codecompanion.adapters.http." .. name),失败则回退到config.adapters.http[name],再与opts做深合并(vim.tbl_deep_extend("force", ...));
  5. 若传入的是函数,直接执行函数获取配置表;
  6. 最后补全type = "http"、执行旧的handlers.resolve(若存在),并通过 shared.apply_extend 将用户config.adapters.http.extend中的按 Adapter 配置覆盖(keyed by config key)深合并进去。

shared.lua 中还有几个 http/acp 共用的工具函数值得了解:

  • map_roles(adapter, messages):按 Adapter 定义的roles表替换消息中的角色名;
  • apply_extend(adapter, opts):将用户的 extend 配置深度合入已解析的 Adapter(嵌套 table 用vim.tbl_deep_extend("force", ...),普通值直接覆盖);
  • context_window(adapter):解析当前模型的上下文窗口大小,优先取adapter.model.meta.context_window,否则回退到schema.model.default/schema.model.choices(函数形式会在pcall中安全求值);
  • manages_own_context(adapter):判断当前提供商是否在服务端自行管理上下文压缩(依赖model.opts.can_manage_context,并且会通过pcall刷新可能过期的模型缓存)。

六、实例剖析:新结构与旧结构的对比

文档特意给出了两个代表性示例,分别演示新旧两种 handler 结构。

6.1 OpenAI Responses:完整使用新嵌套结构

openai_responses.lua 是使用新结构的范例,其顶层定义包括:name = "openai_responses"vendor = "openai"url = "https://api.openai.com/v1/responses"env = { api_key = "OPENAI_API_KEY" }roles(llm/user/tool 分别映射到assistant/user/tool)。

lifecycle.setup中会根据所选模型动态修正能力开关:例如从model_opts.opts中深合并has_visioncan_use_toolscan_manage_context等,并开启流式参数self.parameters.stream = true(openai_responses.lua)。

request.build_messages展示了 Responses API 的消息组织方式(openai_responses.lua):

  • system 消息被抽取为顶层instructions
  • 图片消息(带tags.IMAGE标记)被合并为input_image块,并与相邻的同角色文本消息合并;
  • PDF 文档(带tags.DOCUMENTfiletype == "pdf")被编码为input_file块;
  • 工具结果按role == "tool"转换为function_call_output
  • LLM 发出的工具调用被展开为多个function_call项;
  • 若模型支持服务端上下文管理(can_manage_context),还会附带context_management压缩策略。

response.parse_chat则同时处理流式(response.output_text.deltaresponse.reasoning_summary_text.deltaresponse.completed等事件)与非流式(json.output中的message/reasoning/function_call/compaction)两种返回,并负责把工具调用写入tools表、把压缩块写入meta.compaction

schema还给出了参数定义的完整写法(openai_responses.lua),每个字段都带ordermappingtypeoptionaldefaultdesc、可选的choicesvalidate

参数类型默认值约束/说明
modelenumgpt-5.6-lunachoices 中每个模型都带meta.context_windowopts(是否支持工具、视觉、推理、结构化输出、上下文管理)
reasoning.effortstringmedium可选xhigh/high/medium/low/none,仅推理模型启用
reasoning.summarystringauto可选auto/concise/detailed,推理摘要级别
temperaturenumber10~2,validate会校验
top_logprobsnumbernil0~20
top_pnumber10~1,默认enabled = false
max_output_tokensintegernil必须大于 0
verbositystringmedium可选low/medium/high,控制输出 token 数量

6.2 Anthropic:旧扁平结构仍在生产环境服役

anthropic.lua 是旧格式的活标本,所有函数都平铺在handlers顶层:

  • setup:开启流式、合并模型能力、按需注入anthropic-beta请求头(如compact-2026-01-12context-management-2025-06-27以启用服务端压缩);
  • form_messages:这是最复杂的部分(anthropic.lua),依次完成 system 消息抽取、图片/PDF 转 base64 块、filter_out_messages清理、空 user 提示占位、字符串 content 转{ { type = "text", text = ... } }数组、工具结果转tool_result块、LLM 工具调用转tool_use块、推理内容转thinking块(含signature)、压缩块回填、连续同角色消息合并,最后附带cache_control = { type = "ephemeral" }启用自动提示缓存;
  • form_parameters:针对扩展思维(extended thinking)处理thinking参数,并遵循 Anthropic 的兼容性约束(禁用top_k、将top_p收敛到 0.95~1);
  • form_tools/form_structured_output:工具 schema 经 tool_transformers 转换,结构化输出经 structured_outputs 转换;
  • chat_output/inline_output/tokens:分别解析聊天输出、内联输出与 token 用量(message_start累计输入,message_delta结算输出);
  • tools.format_tool_calls/tools.output_response:工具调用格式化为 OpenAI 风格,工具结果写入规范形状;
  • on_exit:请求完成后的错误日志捕获。

schema.modelchoices是一个动态函数,通过 adapters/utils/models/fetch.lua 请求https://api.anthropic.com/v1/models实时拉取模型列表;其他参数(extended_thinkingthinking_budget默认 16000、max_tokens默认 4096、top_ptop_kstop_sequences)也都带有各自的enabled/validate逻辑。

此外两个 Adapter 都展示了available_tools的用法:Anthropic 内置code_executionmemoryweb_fetchweb_search(各自注入对应的anthropic-beta头),OpenAI Responses 内置web_search。这些“适配器级工具”在build_tools/form_tools中通过schema._meta.adapter_tool标记被识别并回调。

七、HTTP 客户端:http.lua如何驱动 Adapter

文档明确指出 lua/codecompanion/http.lua 实现了一个与提供商无关的 HTTP 客户端,集中处理请求构造、流式传输、调度与可测试性,并且同样通过adapters.call_handler()以向后兼容的方式调用各 handler。

从源码看,其设计要点如下:

  1. 可测试的静态方法Client.static.methods(http.lua)把postgetencodescheduleschedule_wrap声明为可替换的默认实现,测试时可以整体 mock;
  2. 请求前准备prepare_adapter()vim.deepcopyAdapter,然后调用setuphandler,失败则返回错误,最后解析环境变量(http.lua);
  3. body 合并顺序Client.merge_body()(http.lua)按build_parametersbuild_messagesbuild_toolsbuild_structured_outputadapter.bodybuild_body的顺序用vim.tbl_deep_extend("keep", ...)合并出完整请求体;
  4. curl 参数build_curl_args()(http.lua)默认加入--retry 3--retry-delay 1--keepalive-time 60--connect-timeout 10,流式模式下追加--tcp-nodelay--no-buffer;请求头写入临时 headers 文件,通过--header @file传入,因此headers中类似Authorization = "Bearer ${api_key}"的占位符会在发送前由adapter_utils.set_env_vars替换为真实环境变量(底层依赖 plenary.curl 的env机制);
  5. 异步与同步Client:send()返回带cancel()/status()RequestHandle,流式数据通过on_chunk逐块回调,非流式在on_done中返回;Client:send_sync()用于内联补全等同步场景(会临时关闭流式、校验结构化输出支持)。

八、模型列表与动态获取

文档提到部分 Adapter 支持动态拉取模型列表,相关说明见 .codecompanion/adapters/models_list.md。典型实现:

  • OpenRouter:请求https://openrouter.ai/api/v1/models(openrouter.lua);
  • Copilot:请求 Copilot 模型端点(get_models.lua);
  • Anthropic:请求https://api.anthropic.com/v1/models(携带x-api-keyanthropic-version头);
  • Ollama:请求本地http://localhost:11434/api/tags

各端点的响应样例保存在 tests/adapters/http/stubs/model_list 目录下(anthropic.jsoncopilot.jsonollama.jsonopenrouter.json),可作为接入新提供商时格式参考。动态模型的缓存时长由配置config.adapters.http.opts.cache_models_for(默认 1800 秒)控制,环境变量解析类命令的超时由config.adapters.opts.cmd_timeout(默认 20000ms)控制。

九、测试与验证

文档给出了两组测试入口,它们是理解 handler 契约的最佳阅读材料:

  • tests/adapters/test_adapters.lua:定义了一组测试用 Adapter(test_adaptertest_adapter2)与chat_buffer_settings,覆盖 schema 映射、${...}环境变量占位符替换、mapping = "parameters.options"这类点分路径嵌套写入等行为,是校验map_schema_to_params(http/init.lua)的基准;
  • tests/adapters/http/test_openai_responses.lua:直接通过require("codecompanion.adapters").resolve("openai_responses")解析真实 Adapter,然后逐项断言build_reasoning对增量内容与encrypted_content的拼接、format_response产出的规范工具结果形状等。

仓库中还有 test_anthropic.lua、test_openai.lua、test_gemini.lua、test_ollama.lua 等覆盖各提供商的流式/非流式、工具调用、推理输出与压缩场景,其输入桩(stub)均位于 tests/adapters/http/stubs 目录,适合做回归验证时的对照样本。

十、如何开始自定义 Adapter

结合以上机制,自定义一个 Adapter 的推荐路径是:

  1. 复制一个最接近目标 API 的现有实现作为起点(新结构参考 openai_responses.lua,旧结构参考 anthropic.lua);
  2. 实现lifecycle(可选)、request(必选:至少build_parametersbuild_messages)、response(必选:至少parse_chat)、tools(若需工具调用);
  3. 遵守“规范工具结果结构”一节中的约定,确保工具结果可跨 Adapter 移植;
  4. 在 config.lua 的adapters.http(或adapters.acp)表中注册,或通过extend字段按 key 覆盖既有 Adapter 的envheadersschema等;
  5. 参考 tests/adapters/test_adapters.lua 的写法补充测试。

需要说明的是,本文所述结构(嵌套 handler、call_handler映射表、规范工具结果)均来自当前仓库 lua/codecompanion/adapters 与 lua/codecompanion/http.lua 的实际实现,接入新的 LLM 服务商时请以仓库内对应 Adapter 文件的最新代码为准。

【免费下载链接】codecompanion.nvim✨ AI Coding, Vim Style项目地址: https://gitcode.com/GitHub_Trending/co/codecompanion.nvim

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询