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 包含两类核心内容:
- LLM 端点(endpoint)选项:请求 URL、请求头、环境变量、额外的 curl 参数等;
- schema 参数定义:对
model、temperature、top_k、top_p等模型采样属性的声明式配置,用户可以在聊天缓冲区中直接调整这些参数。
同时,HTTP Adapter 还包含一组handler 函数,它们定义了发送给 LLM 的消息应该如何格式化(例如将聊天缓冲区中的消息转换为 Anthropic 的tool_use/tool_result块),以及 LLM 返回的输出应如何被接收并展示在聊天缓冲区中。
所有 Adapter 都定义在 lua/codecompanion/adapters 目录下,其中:
- lua/codecompanion/adapters/http 存放 HTTP 类 Adapter(
anthropic.lua、openai.lua、openai_responses.lua、gemini.lua、ollama.lua、deepseek.lua、mistral.lua等); - lua/codecompanion/adapters/acp 存放 ACP(Agent Client Protocol)类 Adapter(
claude_code.lua、codex.lua、gemini_cli.lua、opencode.lua等)。
从 adapters/init.lua 的adapter_type()函数可以看出,一个 Adapter 的类型判定遵循以下顺序:
- 未指定时,取配置项
config.interactions.chat.adapter的默认值; - 若传入的是带
type字段的 table,则直接使用该类型; - 否则按名称在
config.adapters.acp与config.adapters.http两张表中查找,命中即返回对应类型; - 兜底类型为
http。
在 config.lua 中可以看到内置的完整注册表:HTTP 侧包括anthropic、azure_openai、copilot、deepseek、gemini、githubmodels、huggingface、kimi、novita、mistral、ollama、openai、openai_responses、openrouter、xai、jina、tavily;ACP 侧包括auggie_cli、cagent、claude_code、cline_cli、codex、cursor_cli、copilot_acp、gemini_cli、goose、kimi_cli、kiro、mistral_vibe、opencode。两者都支持extend表用于按 Adapter 做配置覆盖,以及opts表用于全局选项(如allow_insecure、cache_models_for、proxy、show_presets、show_model_choices)。
一个解析后的 HTTP Adapter 是一个CodeCompanion.HTTPAdapter对象,其字段在 adapters/http/init.lua 的类注释中有完整定义:name、vendor、type、formatted_name、available_tools、roles(角色映射)、features、url、env/env_replaced(环境变量及替换结果)、body、headers、parameters、raw(额外 curl 参数)、handlers、meta(上下文窗口等模型元数据)、methods、model、opts、schema、temp(不随请求发送的临时存储)。
二、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:副作用与初始化(
setup、teardown、请求完成后的清理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_output的call_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,核心分两步:
- 新格式:通过
uses_new_handlers()检测(判断handlers.lifecycle、handlers.request、handlers.response任一存在),然后在lifecycle、request、response、tools四个类目中依次查找同名函数; - 旧格式:按下表把新名字映射回旧名字后,在扁平的
handlers表中查找:
| 新格式名称 | 旧格式名称 | 所属类目 |
|---|---|---|
setup/on_exit/teardown | setup/on_exit/teardown | lifecycle |
build_parameters | form_parameters | request |
build_messages | form_messages | request |
build_tools | form_tools | request |
build_structured_output | form_structured_output | request |
build_body | set_body | request |
build_reasoning | form_reasoning | request |
parse_chat | chat_output | response |
parse_inline | inline_output | response |
parse_tokens | tokens | response |
parse_meta | parse_message_meta | response |
format_calls | format_tool_calls | tools |
format_response | output_response | tools |
旧格式示例(仍然受支持):
-- 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是否存在来判断格式,检测必须看lifecycle、request或response这三个类目。
五、工厂方法: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)值得细读:
- 未传 Adapter 时使用
config.interactions.chat.adapter默认值; - 若传入的是已解析的 table(带
name、schema且resolved()为真),直接复用并调用set_model; - 若传入的是
{ name = "...", model = "..." }形式的 table,则递归解析name并附带指定model; - 若传入的是字符串,先尝试
require("codecompanion.adapters.http." .. name),失败则回退到config.adapters.http[name],再与opts做深合并(vim.tbl_deep_extend("force", ...)); - 若传入的是函数,直接执行函数获取配置表;
- 最后补全
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_vision、can_use_tools、can_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.DOCUMENT且filetype == "pdf")被编码为input_file块; - 工具结果按
role == "tool"转换为function_call_output; - LLM 发出的工具调用被展开为多个
function_call项; - 若模型支持服务端上下文管理(
can_manage_context),还会附带context_management压缩策略。
response.parse_chat则同时处理流式(response.output_text.delta、response.reasoning_summary_text.delta、response.completed等事件)与非流式(json.output中的message/reasoning/function_call/compaction)两种返回,并负责把工具调用写入tools表、把压缩块写入meta.compaction。
其schema还给出了参数定义的完整写法(openai_responses.lua),每个字段都带order、mapping、type、optional、default、desc、可选的choices与validate:
| 参数 | 类型 | 默认值 | 约束/说明 |
|---|---|---|---|
model | enum | gpt-5.6-luna | choices 中每个模型都带meta.context_window与opts(是否支持工具、视觉、推理、结构化输出、上下文管理) |
reasoning.effort | string | medium | 可选xhigh/high/medium/low/none,仅推理模型启用 |
reasoning.summary | string | auto | 可选auto/concise/detailed,推理摘要级别 |
temperature | number | 1 | 0~2,validate会校验 |
top_logprobs | number | nil | 0~20 |
top_p | number | 1 | 0~1,默认enabled = false |
max_output_tokens | integer | nil | 必须大于 0 |
verbosity | string | medium | 可选low/medium/high,控制输出 token 数量 |
6.2 Anthropic:旧扁平结构仍在生产环境服役
anthropic.lua 是旧格式的活标本,所有函数都平铺在handlers顶层:
setup:开启流式、合并模型能力、按需注入anthropic-beta请求头(如compact-2026-01-12、context-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.model的choices是一个动态函数,通过 adapters/utils/models/fetch.lua 请求https://api.anthropic.com/v1/models实时拉取模型列表;其他参数(extended_thinking、thinking_budget默认 16000、max_tokens默认 4096、top_p、top_k、stop_sequences)也都带有各自的enabled/validate逻辑。
此外两个 Adapter 都展示了available_tools的用法:Anthropic 内置code_execution、memory、web_fetch、web_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。
从源码看,其设计要点如下:
- 可测试的静态方法:
Client.static.methods(http.lua)把post、get、encode、schedule、schedule_wrap声明为可替换的默认实现,测试时可以整体 mock; - 请求前准备:
prepare_adapter()先vim.deepcopyAdapter,然后调用setuphandler,失败则返回错误,最后解析环境变量(http.lua); - body 合并顺序:
Client.merge_body()(http.lua)按build_parameters→build_messages→build_tools→build_structured_output→adapter.body→build_body的顺序用vim.tbl_deep_extend("keep", ...)合并出完整请求体; - 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机制); - 异步与同步:
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-key与anthropic-version头); - Ollama:请求本地
http://localhost:11434/api/tags。
各端点的响应样例保存在 tests/adapters/http/stubs/model_list 目录下(anthropic.json、copilot.json、ollama.json、openrouter.json),可作为接入新提供商时格式参考。动态模型的缓存时长由配置config.adapters.http.opts.cache_models_for(默认 1800 秒)控制,环境变量解析类命令的超时由config.adapters.opts.cmd_timeout(默认 20000ms)控制。
九、测试与验证
文档给出了两组测试入口,它们是理解 handler 契约的最佳阅读材料:
- tests/adapters/test_adapters.lua:定义了一组测试用 Adapter(
test_adapter、test_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 的推荐路径是:
- 复制一个最接近目标 API 的现有实现作为起点(新结构参考 openai_responses.lua,旧结构参考 anthropic.lua);
- 实现
lifecycle(可选)、request(必选:至少build_parameters与build_messages)、response(必选:至少parse_chat)、tools(若需工具调用); - 遵守“规范工具结果结构”一节中的约定,确保工具结果可跨 Adapter 移植;
- 在 config.lua 的
adapters.http(或adapters.acp)表中注册,或通过extend字段按 key 覆盖既有 Adapter 的env、headers、schema等; - 参考 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),仅供参考