抹平两层差异:kosong 抹平大模型,kaos 抹平执行环境
30 秒导读: 一个编码 agent 的上层逻辑(回合循环、工具、上下文管理)最怕两件事天天变:换大模型(Kimi/Claude/GPT/Gemini 的协议各不相同)和换执行环境(本地跑还是 SSH 到远程跑)。kimi-code 用两个"适配层"包把这两处差异关进盒子:
packages/kosong让上层只面对一套消息/工具/用量类型,由适配器落到各家协议;packages/kaos让所有文件/进程操作只面对一套Kaos接口,由实现落到本地或远程。上层代码对"用哪个模型、在哪台机器上跑"基本无感。
本章只讲这两个适配层。工具本身怎么实现看 03-tools.md;回合循环怎么转看 01-loop.md;Agent 主机层怎么拼装看 02-agent.md。
1. 这是什么(零基础也能懂)
先建立一个直觉:适配层就是翻译官 + 统一插座。
- kosong = 大模型翻译官。 上层想说"把这段对话发给模型,带上这些工具,流式收回复"。它只会说一种"普通话"(kosong 的
Message/Tool/TokenUsage类型)。kosong 里每个 provider 适配器负责把普通话翻译成某一家模型的"方言"(Kimi 的 OpenAI 兼容协议、Anthropic 的 Messages API、Google 的 GenAI……),再把方言的流式回复翻译回普通话。 - kaos = 执行环境统一插座。 上层想说"读这个文件""跑这条命令"。它只往一个插座
Kaos上插。插座背后接的是本地机器(LocalKaos)还是一条 SSH 隧道(SSHKaos),上层不关心。
两者的共同价值观是同一句话:
把"易变的外部世界"收敛成一个稳定接口,让上层只依赖接口,不依赖具体家数。
给谁用、解决什么问题: 给 Agent 主机层(agent-core)用。它想要"换个模型只改配置、不改逻辑""同一套工具既能在本地跑也能远程跑"。没有这两层,每加一家模型、每支持一种远程执行,上层就要改一遍——适配层就是拦住这种扩散的墙。
一个最小直觉(kosong 的调用长这样):
// 示意,非源码:上层只碰这一套类型
const provider = createProvider({ type: 'anthropic', model: 'claude-...', apiKey });
const result = await generate(
provider, // 哪家模型,由 type 决定
'你是一个编码助手', // system prompt
[readFileTool, bashTool], // 一套 Tool
[createUserMessage('帮我改 bug')], // 一套 Message
);
result.message.toolCalls; // 收回来还是同一套类型
换成 type: 'kimi' 或 type: 'google-genai',上面这段一个字不用改——这就是"抹平"。
2. 顶层全景(它大概怎么转)
两层各守一条边界。先看它们在整条链路里的位置:
agent-core(回合循环 / 工具 / 上下文)
│ │
想调模型 │ │ 想读文件/跑命令
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ kosong (LLM 抽象) │ │ kaos (执行抽象) │
│ 一套 Message/Tool │ │ 一套 Kaos 接口 │
│ 一套 generate 循环 │ │ 文件 + 进程 + 环境 │
└─────────┬───────────┘ └──────────┬──────────┘
│ 适配器翻译 │ 实现落地
┌─────────┼──────────┐ ┌─────────┴─────────┐
▼ ▼ ▼ ▼ ▼ ▼ ▼
Kimi Anthr OpenAI Google … LocalKaos SSHKaos
(各家 HTTP 协议) (本机进程/fs) (ssh2 隧道)
两层职责一句话:
| 层 | 包 | 抹平的是 | 上层看到的统一物 | 底层落到 |
|---|---|---|---|---|
| LLM 抽象 | packages/kosong | 各家大模型协议差异 | Message / Tool / TokenUsage / generate() | Kimi / Anthropic / OpenAI(两代)/ Google 的 HTTP 契约 |
| 执行抽象 | packages/kaos | 本地 vs 远程执行差异 | Kaos 接口(fs + 进程 + 环境探测) | LocalKaos(node fs / child_process)、SSHKaos(ssh2) |
两层之间互不知道对方存在:kosong 只管消息进出模型,kaos 只管文件与进程。把它们缝到一起的是 agent-core——见 §5 接缝。
主线走一遍(高层):
- agent-core 用
createProvider(config)造出一个ChatProvider,再用getModelCapability(wire, model)查这个模型支不支持图片/视频/thinking。 - 每一回合,agent-core 调 kosong 的
generate(provider, ...),拿回一条组装好的 assistant 消息(文本 + thinking + 工具调用)。 - 消息里若有工具调用,agent-core 去执行工具;工具里所有"读文件、跑命令"都打到
Kaos上,由当前绑定的实现(本地或远程)真正落地。 - 工具结果作为新的
Message回到第 2 步,进入下一回合。
3. kosong 抹平大模型(上半)
这一层的全部工作可以浓缩成一句:上层只面对一套类型,由适配器落到各家协议。 下面拆成"统一的类型 → 统一的循环 → 各家的翻译 → 能力位穿针"四步,由浅入深。
3.1 统一的语言:一套消息 / 工具 / 用量
kosong 先定义了所有 provider 都必须说的"普通话",这些类型在 packages/kosong/src/ 根目录:
| 类型 | 文件 | 是什么 |
|---|---|---|
Message | message.ts:92 | 一条消息:role + 内容块数组 + 工具调用数组 |
ContentPart | message.ts:38 | 内容块联合:text / think / image_url / audio_url / video_url |
Tool | tool.ts:8 | 工具定义:name + description + JSON Schema 参数 |
TokenUsage | usage.ts:7 | 四栏用量:普通输入 / 输出 / 缓存读 / 缓存写 |
StreamedMessagePart | message.ts:82 | 流式片段:一个 ContentPart、一个 ToolCall、或一段工具参数增量 |
关键设计:内容是"块数组"而不是一根字符串(Message.content: ContentPart[])。因为多模态、thinking、工具调用要并存,一根字符串装不下。think(模型的推理内容)是一等公民块,和普通 text 分开——各家适配器再决定怎么把它塞进自家协议(Kimi 落到 reasoning_content 字段,Anthropic 落到 thinking 块)。
用量为什么是四栏而不是"输入/输出两栏"?因为 prompt 缓存要单独记账:inputCacheRead(命中缓存的便宜 token)和 inputCacheCreation(写缓存)必须和 inputOther 分开,上层才能算准成本(usage.ts:7 TokenUsage,inputTotal() 把三种输入相加)。
3.2 统一的循环:generate() 与 ChatProvider 契约
上层只调一个函数——generate()(generate.ts:87),它是"发一次、收一条组装好的消息"的唯一入口。它对 provider 的全部要求写在一个接口里:ChatProvider(provider.ts:220)。
ChatProvider 的核心成员:
| 成员 | 签名要点 | 干什么 |
|---|---|---|
generate(...) | provider.ts:246 | 把 system+tools+history 发出去,返回一个 StreamedMessage(可 for await 的流) |
withThinking(effort) | provider.ts:253 | 返回一个"带某档思考强度"的浅拷贝 |
withMaxCompletionTokens(cap, opts) | provider.ts:268 | 返回一个"补全预算被夹到 cap"的浅拷贝(可选) |
uploadVideo(input, opts) | provider.ts:273 | 上传视频、返回一个可放进消息的 video_url 块(可选) |
readonly thinkingEffort / maxCompletionTokens | provider.ts:226/237 | 让 host 读到"这实例实际会往线上发什么" |
为什么 generate() 是自由函数,不是 provider 的方法? 因为流式片段的组装逻辑是所有 provider 共享的,不该每家抄一遍。provider 只负责把自家的流翻成 StreamedMessagePart 序列;generate() 统一负责把这些片段合并成最终消息。
这个"怎么读流"很关键,单独讲。generate() 的核心是一个 for await 循环(generate.ts:145),它做三件事:
每收到一个 StreamedMessagePart:
① 相邻同类片段就地合并(mergeInPlace)
text+text → 拼接; think+think → 拼接; ToolCall+参数增量 → 追加参数
② 合并不了(来了个新块)→ 把 pending 冲刷进 message,再开新的 pending
③ 并行工具调用的参数增量,按 index 路由到正确的那个调用(见下)
真源码:合并规则在 mergeInPlace(message.ts:171),冲刷在 flushPart(generate.ts:317)。"工具调用何时算完整"不是靠一个 done 事件,而是靠合并边界推断——来了个不能合并的片段,就说明上一个工具调用收全了(generate.ts 注释,onToolCall 特意推迟到流结束后按最终顺序触发,见 generate.ts:247-253)。
一个不显然的坑:并行工具调用的参数会交错。 OpenAI 系流式可能这样发:调用0的头 → 调用1的头 → 调用0的参数 → 调用1的参数。若只按"追加到最近一个"顺序合并,参数就会串到错误的调用上。kosong 的解法是给每个片段带一个 index,generate() 用一张 toolCallIndexMap 把增量路由回正确的调用(generate.ts:167-184)。这是"抹平"里最容易被忽视、却决定正确性的一处。
3.3 各家的翻译:六个适配器
createProvider(config)(providers/index.ts:25)按 config.type 分派到具体适配器。六个 wire 类型:
| type | 适配器类 | 底层 SDK / 协议 | 特色处理 |
|---|---|---|---|
kimi | KimiChatProvider(kimi.ts:394) | OpenAI 兼容 + Moonshot 私有扩展 | reasoning_content、messages[].tools 动态工具、$ 内建函数 |
anthropic | AnthropicChatProvider(anthropic.ts:875) | @anthropic-ai/sdk Messages API | 必填 max_tokens、thinking 块、interleaved-thinking beta |
openai | OpenAILegacyChatProvider(openai-legacy.ts:466) | OpenAI Chat Completions | 与 Kimi 共享 finish_reason / usage 解析 |
openai_responses | OpenAIResponsesChatProvider(openai-responses.ts:1014) | OpenAI Responses API | reasoning item、加密推理内容回传 |
google-genai | GoogleGenAIChatProvider(google-genai.ts:730) | Google GenAI | inlineData/fileData 多模态、thinkingBudget/thinkingLevel |
vertexai | 同上(复用 GenAI 类) | Vertex 变体 | 与 google-genai 同实现 |
它们的翻译工作可以归纳成"三进三出":
进(kosong → 各家): 出(各家 → kosong):
Message → 各家消息格式 各家流事件 → StreamedMessagePart
Tool → 各家工具/函数声明 各家 usage → TokenUsage
ContentPart(含 think/媒体)→ 各家块 各家 finish → FinishReason
几处共享的翻译零件,避免每家重抄,值得单独记:
- OpenAI 系共享层
openai-common.ts:toolToOpenAI(:80)、extractUsage(:160,含 Moonshot 私有cached_tokens)、normalizeOpenAIFinishReason(:206,把stop/length/content_filter映射到统一枚举)。Kimi 和 OpenAI-legacy 都用它。 - 流式工具调用解码
chat-completions-stream.ts:29convertChatCompletionStreamToolCall:把 OpenAI 风格的增量(可能参数先于函数名到)缓冲、补齐,再发成带index的片段,供 §3.2 的路由用。 - 工具调用 id 归一化
tool-call-id.ts:21normalizeToolCallIdsForProvider:各家对 id 的字符集/长度限制不同(Kimi 限 64 字符、仅[A-Za-z0-9_-],见kimi.ts:86的 policy)。这一步在发送前把历史里的 id 批量改写成合法且不重复的版本。 - 连续 user 回合合并
merge-user-messages.ts:40mergeConsecutiveUserMessages:Anthropic、Gemini 严格要求 user/assistant 交替,连着两个 user 会被拒(HTTP 400)。压实后(compaction)或"工具结果后紧跟用户插话"都会产生连续 user 回合,所以这两家在转换边界统一压平;宽松的 OpenAI/Kimi 保留原结构。把算法放一处,防止某家漏做(这正是当年 Gemini 回归 bug 的根因,见该文件注释)。
举一个"同一个概念、各家落点不同"的例子——thinking(思考强度):
| provider | withThinking('high') 落到哪 | 依据 |
|---|---|---|
| Kimi | extra_body.thinking = { type:'enabled', effort:'high' } | kimi.ts:567 |
| Anthropic | 按模型 profile 选 budget/adaptive 模式 | anthropic.ts:1185 + anthropic-profile.ts:113 |
| OpenAI Responses | reasoning_effort = 'high' | openai-responses.ts:1147 |
映射成 thinkingLevel(gemini-3)或 thinkingBudget 数值 | google-genai.ts:920 |
上层只说一句 withThinking('high'),四家各自翻译。Anthropic 尤其讲究:anthropic-profile.ts 维护一张"模型家族 → 支持哪些 effort 档、能不能关思考"的矩阵(matchKnownAnthropicModelProfile,:113),连 max_tokens 默认值都按模型版本夹到官方上限(resolveDefaultMaxTokens,anthropic.ts:256)—— 因为 Anthropic 的 max_tokens 是必填,发大了会被服务端拒。
3.4 能力位:视频输入如何一路穿到工具
不是每个模型都能吃图片/视频/音频。kosong 用 ModelCapability(capability.ts:11)这张能力位描述一个模型接受哪些模态:
interface ModelCapability {
image_in; video_in; audio_in; thinking; tool_use;
max_context_tokens; dynamically_loaded_tools?;
}
能力从两条路来,再合并:
- 静态表
getModelCapability(wire, model)(providers/index.ts:54)——纯查表,不建 provider。按模型名前缀匹配,比如getGoogleGenAIModelCapability(capability-registry.ts:192)判定 Gemini 系天然支持 image/video/audio。Kimi wire 返回UNKNOWN_CAPABILITY,因为它的能力来自 host 的目录配置而非模型名。 - 目录/配置
catalog.ts——catalogModelToCapability(:122)把 models.dev 风格的公开目录条目(modalities.input里有没有video)归一成同一个ModelCapability。
一条"视频输入"能力如何从这里一路穿到工具,是理解这层设计的最好案例:
① 静态表/目录 → ModelCapability{ video_in: true }
│ agent-core: resolveModelCapabilities 合并声明与探测
▼ (provider-manager.ts:228)
② KosongLLM.capability (kosong-llm.ts:72)
│
├─(a) 发送前:downgradeUnsupportedMedia 剔除模型吃不下的媒体
│ 把 image/video/audio 块换成占位文本 (kosong-llm.ts:300)
│
└─(b) 工具侧:capability.video_in 为真才挂"视频上传"能力
createVideoUploader(provider) 绑定 provider.uploadVideo
(agent/tool/index.ts:779)
│
▼
③ provider.uploadVideo → KimiFiles.uploadVideo 上传到 Moonshot 文件服务
返回 video_url 块(url = ms://<file-id>) (kimi-files.ts:75)
要点:能力位是"双向闸门"。发送方向(a),模型吃不下的媒体在 downgradeUnsupportedMedia 被降级成占位文字,避免请求打过去才 400。工具方向(b),只有 video_in 为真才给模型挂上"能读视频"的工具,且这个工具的落地是 provider 自己的 uploadVideo——目前只有 Kimi 实现了(KimiFiles,kimi-files.ts:40),它把视频传到 Moonshot 文件服务、换回一个 ms:// 引用块。其它 provider 的 uploadVideo 是 undefined,createVideoUploader 直接返回 undefined,工具就不挂——能力开关一路贯穿到底。
3.5 认证与克隆:短命凭证不进长命状态
一处工程细节值得学:per-request 认证。OAuth bearer token 是短命的,不能塞进长命的 SDK client。resolveAuthBackedClient(request-auth.ts:54)定义了统一优先级:有 clientFactory 就用它;没有 per-request auth 且有缓存 client 就复用;否则临时 build(auth) 一个新 client。走 OAuth 路径时每次请求现造 client,把短命凭证挡在共享状态之外(注释里坦白:牺牲了连接池复用,但对"一回合一次调用"的工作负载可接受)。
与之配套的是 provider 的浅拷贝语义:withThinking / withMaxCompletionTokens 都返回克隆(kimi.ts:_clone,:640),而克隆共享底层 _client——因为每回合都要按当前输入大小夹一次预算(withMaxCompletionTokens),克隆必须廉价。源码注释特意警告:永远别在克隆上替换 _client,否则原实例会拿到一个被关掉的 socket。
4. kaos 抹平执行环境(下半)
上半把"模型"关进盒子,下半把"在哪台机器上跑"关进盒子。名字是 KAOS = Kimi Agent Operating System——它就是给 agent 的一层"操作系统门面"。
4.1 一个接口收下所有文件与进程操作
核心是 Kaos 接口(kaos.ts:12)。它把 agent 需要的一切外部副作用收成四组方法:
| 组 | 代表方法 | 说明 |
|---|---|---|
| 路径(同步) | pathClass() / normpath() / gethome() / getcwd() | posix vs win32 由环境决定 |
| 目录(异步) | iterdir() / glob() / mkdir() / chdir() | 遍历、匹配、建目录 |
| 文件(异步) | readText() / readBytes() / writeText() / stat() | 读写、元数据,readText 的 errors 参数照搬 Python open(...) |
| 进程 | exec(...args) / execWithEnv(args, env) | 返回一个 KaosProcess |
| 派生 | withCwd(cwd) / withEnv(env) | 返回换了 cwd/环境的新 Kaos |
进程被抽象成 KaosProcess(process.ts:10):stdin/stdout/stderr 三条流 + pid + wait() + kill() + dispose()。注释点明它"刻意最小",这样才能同时被本地子进程、SSH 会话、容器运行时实现。
所有文件/进程类工具最终都打到这个接口上。 比如 Bash 工具持有一个 Kaos,执行时走 this.kaos.execWithEnv(...)(tools/builtin/shell/bash.ts:297),而从不直接碰 node:child_process(该文件顶注:Execution goes through Kaos, never directly via node:child_process)。文件读工具同理,走 kaos.readBytes(tools/builtin/file/read.ts:109)。这就是"抹平"在下半的兑现:工具代码不知道自己在本地还是远程。
4.2 两个实现:本地 vs 远程
同一个接口,两套落地:
| 实现 | 文件 | 背后 | 造法 |
|---|---|---|---|
LocalKaos | local.ts:188 | node 的 fs / child_process | await LocalKaos.create()(:215) |
SSHKaos | ssh.ts:435 | ssh2 库的 SFTP + exec 通道 | await SSHKaos.create(options)(:483) |
对上层来说两者可互换:exec 在本地是 spawn 子进程(local.ts:727),在远程是开一条 ssh exec 通道(ssh.ts:855);readText 在本地读磁盘,在远程走 SFTP。withCwd/withEnv 在两边都返回一个共享连接、只换了 cwd/环境层的新实例(local.ts:224、ssh.ts:466)。想把整个 agent 从本地搬到远程,只需在启动时绑定一个 SSHKaos 而不是 LocalKaos——工具、循环、prompt 全都不用动。
边界诚实:
SSHKaos.osEnv目前会抛"远程环境探测尚未接线"(ssh.ts:448),即远程还没做完整的 OS/shell 探测。所以本地是完整实现,SSH 是骨架+大部分 fs/exec 已可用。
4.3 谁是"当前"环境:AsyncLocalStorage 绑定
工具不是每次都被显式塞一个 Kaos——很多模块函数直接调 getCurrentKaos()(current.ts:16)。"当前是哪个 Kaos"用 Node 的 AsyncLocalStorage 存(current.ts):
setCurrentKaos(kaos) 一次性绑定(进程启动) current.ts:33
runWithKaos(kaos, fn) 给某段异步子树绑定,互不串 current.ts:42
getCurrentKaos() 读当前;没绑定就抛清晰错误 current.ts:16
current.ts 还把接口方法都导出成模块级便捷函数(readText/exec/glob……,:48 起),内部都是 getCurrentKaos().xxx。好处:调用点写 import { readText } from '@moonshot-ai/kaos' 就行,不用到处传 Kaos 实例;runWithKaos 保证并发的不同 agent 上下文各绑各的、不互相污染。agent-core 在需要处 LocalKaos.create() 并绑定(如 rpc/core-impl.ts:1163、agent/replay/build.ts:13)。
4.4 环境探测:让同一套命令在 mac/Linux/Windows 都能跑
Environment(environment.ts:29)是对目标机 OS/shell 的一次探测:osKind / osArch / osVersion / shellName / shellPath。Kaos.osEnv 持有它,Bash 工具据此决定用哪个 shell、渲染哪套命令描述(bash.ts:231)。
两个探测函数都写成纯函数 + 注入依赖,好处是同一套测试能在任意 host OS 上跑:
detectEnvironment(deps)(environment.ts:75):POSIX 上在/bin/bash→/usr/bin/bash→…里找 bash,找不到退回/bin/sh;Windows 上定位 Git Bash(顺 着git.exe、git --exec-path、常见安装路径找,找不到抛KaosShellNotFoundError带安装提示)。生产用detectEnvironmentFromNode()(:250)喂 Node 默认值并记忆化(环境一辈子不变)。probeLoginShellPath(deps)(login-shell-path.ts:47):解决一个真实痛点——从 GUI 启动器起的 kimi-code,process.env.PATH可能缺/opt/homebrew/bin,导致 Bash 工具找不到gh等命令。它跑一次用户登录 shell($SHELL -l -c /usr/bin/env)抽出其 PATH,把当前 PATH 缺的绝对路径条目追加进去(mergeLoginShellPath,:92)。安全细节:只导入绝对路径条目(空/./相对项都是 cwd 相关的查找,追加会扩大搜索路径),且用绝对路径/usr/bin/env调用,避免仓库里植入的env二进制在启动时被执行。
5. 接缝:与 agent-core 怎么缝
两层自己不认识 agent-core;缝合发生在 agent-core 一侧。最主要的一条缝是 packages/agent-core/src/agent/turn/kosong-llm.ts。
KosongLLM(kosong-llm.ts:69)实现回合循环要的 LLM 契约,内部把它翻译成 kosong 的 generate()。它做三件事:
- 持有 provider 与 capability。 构造时收下
ChatProvider和ModelCapability(能力由provider-manager.ts:233的getModelCapability探测 + 用户声明合并而来,resolveModelCapabilities)。 - 每次 chat 现算预算、发一次。
chat()(:89)先用applyCompletionBudget在一个丢弃式浅拷贝上夹补全预算(:114,不回写this.provider,好让上层重试仍用同一个长命 client),再调generate()。发送前对消息跑downgradeUnsupportedMedia(§3.4a)剔除模型吃不下的媒体。 - 把 kosong 的流回调翻译成 loop 的 per-delta 回调。
buildKosongCallbacks(:204)把 kosong 的onMessagePart拆成onTextDelta/onThinkDelta/onToolCallDelta,并且在这里也做一遍并行工具调用的 index 路由(把交错的参数增量对到正确的 toolCallId,:263-288)——这样上层 UI 能边流边显示正确的工具参数。
第二条缝在工具侧:agent-core 造工具时用 provider.uploadVideo 拼出"视频上传器"(agent/tool/index.ts:779,见 §3.4b)。
kaos 一侧的缝更简单:agent-core 在启动/请求处 LocalKaos.create() 后 setCurrentKaos,工具构造时把 Kaos 注入进去,之后工具只认接口。
6. 巧妙之处(可借鉴)
- "何时算完整"用合并边界推断,而非 done 事件。 provider 只需把自家流翻成片段序列,
generate()统一判定工具调用收全没(generate.tsflushPart)。少一类每家都要正确实现的 done 事件,就少一处出错点。 - 并行工具调用双层 index 路由。 kosong 内一层(
generate.ts:167)保证组装正确,agent-core 回调再一层(kosong-llm.ts:263)保证流式 UI 正确。交错参数不串台。 - 共享翻译零件按"协议家族"而非"厂商"归组。 OpenAI-common 被 Kimi 和 OpenAI-legacy 共用;连续 user 合并被 Anthropic 和 Gemini 共用。归组维度是"谁的 wire 长得像",不是"谁家的品牌"。
- 能力位是双向闸门,一路贯穿到工具。 同一个
video_in既在发送侧降级媒体,又在工具侧决定挂不挂上传器——一个开关,两处生效,不会出现"挂了工具但模型其实不支持"的错配。 - 短命凭证永不进长命状态。
resolveAuthBackedClient让 OAuth 路径每请求现造 client,克隆共享_client保持廉价——两条约束都写进注释当护栏。 - 探测全是纯函数 + 注入依赖。
detectEnvironment/probeLoginShellPath把platform/env/execFileText都作为参数注入,同一套测试跨 OS 复用,生产入口再喂 Node 默认值并记忆化。
7. 边界与局限(诚实)
- SSH 是骨架。
SSHKaos.osEnv明确抛"未接线"(ssh.ts:448),远程环境探测未实现;远程用法目前偏向 fs/exec,完整 OS 探测还没做。 - 视频上传只 Kimi 有。 其余 provider 的
uploadVideo是undefined(provider.ts:273标可选);非 Kimi 模型即便声明video_in,也没有配套上传落地。 getModelCapability对 Kimi wire 返回 UNKNOWN(providers/index.ts:66):Kimi 的能力必须由 host 的目录/配置补(resolveModelCapabilities里合并),否则能力位是全 false 的UNKNOWN_CAPABILITY。- 能力位靠模型名前缀匹配(
capability-registry.ts):上游发了个新命名而前缀没覆盖,就落到UNKNOWN_CAPABILITY——保守失败(当作不支持),不会误报支持。 - login-shell PATH 探测只在 POSIX 跑(
login-shell-path.ts:48win32 直接返回),且失败静默(坏 profile / 无 shell → PATH 原样不动)。
8. 代码地图(导航索引)
用符号名 grep 比行号抗漂移。下表是读这两层该打开的关键锚点。
| 主题 | 文件 | 符号 |
|---|---|---|
| generate 循环 / 流片段合并 | packages/kosong/src/generate.ts | generate、flushPart、toolCallIndexMap |
| provider 契约 | packages/kosong/src/provider.ts | ChatProvider、ThinkingEffort、FinishReason、StreamedMessage |
| 统一消息/工具/用量类型 | packages/kosong/src/{message,tool,usage}.ts | Message、ContentPart、mergeInPlace、Tool、TokenUsage |
| provider 分派 / 静态能力表 | packages/kosong/src/providers/index.ts | createProvider、getModelCapability |
| 能力位定义 | packages/kosong/src/capability.ts | ModelCapability、UNKNOWN_CAPABILITY |
| 能力前缀匹配 | packages/kosong/src/providers/capability-registry.ts | getAnthropicModelCapability、getGoogleGenAIModelCapability |
| 目录归一(models.dev) | packages/kosong/src/catalog.ts | catalogModelToCapability、inferWireType、catalogBaseUrl |
| Kimi 适配器 | packages/kosong/src/providers/kimi.ts | KimiChatProvider、convertMessage、withThinking、_clone |
| Anthropic 适配器 + profile | packages/kosong/src/providers/{anthropic,anthropic-profile}.ts | AnthropicChatProvider、resolveDefaultMaxTokens、matchKnownAnthropicModelProfile |
| OpenAI 共享翻译零件 | packages/kosong/src/providers/openai-common.ts | toolToOpenAI、extractUsage、normalizeOpenAIFinishReason |
| 流式工具调用解码 | packages/kosong/src/providers/chat-completions-stream.ts | convertChatCompletionStreamToolCall |
| 工具调用 id 归一 | packages/kosong/src/providers/tool-call-id.ts | normalizeToolCallIdsForProvider、sanitizeToolCallId |
| 连续 user 回合合并 | packages/kosong/src/providers/merge-user-messages.ts | mergeConsecutiveUserMessages |
| per-request 认证 | packages/kosong/src/providers/request-auth.ts | resolveAuthBackedClient、requireProviderApiKey |
| 视频上传(Kimi) | packages/kosong/src/providers/kimi-files.ts | KimiFiles.uploadVideo |
| Kaos 接口 / 进程 | packages/kaos/src/{kaos,process}.ts | Kaos、KaosProcess |
| 本地 / 远程实现 | packages/kaos/src/{local,ssh}.ts | LocalKaos、SSHKaos |
| 当前 Kaos 绑定 | packages/kaos/src/current.ts | getCurrentKaos、runWithKaos、setCurrentKaos |
| 环境探测 | packages/kaos/src/environment.ts | detectEnvironment、detectEnvironmentFromNode |
| 登录 shell PATH 补全 | packages/kaos/src/login-shell-path.ts | probeLoginShellPath、mergeLoginShellPath |
| 与 agent-core 的缝(LLM) | packages/agent-core/src/agent/turn/kosong-llm.ts | KosongLLM、downgradeUnsupportedMedia、buildKosongCallbacks |
| 与 agent-core 的缝(能力/视频) | packages/agent-core/src/session/provider-manager.ts、agent/tool/index.ts | resolveModelCapabilities、createVideoUploader |