# @jsonstudio/llms（RouteCodex LLM Switch 核心）

> 自 0.4.3 起，原 `rcc-llmswitch-core` npm 包已迁移并更名为 `@jsonstudio/llms`。开发 / 发布流程保持不变，仅需更新 package 依赖与文档引用。

本包是 RouteCodex V2 的“工具治理唯一入口 + 协议转换核心”，实现严格的“前半段/后半段”双向流水线，并通过 `defaultSseCodecRegistry` 统一治理所有 SSE ↔ JSON 转换。核心事实：

1. **唯一入口**：所有 HTTP 请求（Chat/Responses/Anthropic/Gemini）必须先进入前半段 Conversion，SSE 输入一律由 `SSEInputNode` → `defaultSseCodecRegistry` → 对应协议 JSON，再映射为标准 Chat 请求。
2. **唯一治理点**：后半段 Chat Pipeline 是唯一可修改工具行为的位置；其他节点仅做形状转换或流式编解码。
3. **唯一出站规则**：响应阶段的 output 格式 (JSON/SSE) 只看原入口端点与入站 streaming 标记；providerType 只能决定 inbound converter 与 provider 调用，不得改写 outbound 协议。

- 前半段（Front-Half，Conversion）：按端点最小映射为 OpenAI Chat 标准形状（不做工具治理、不兜底）。
- 后半段（Back-Half，Chat Pipeline）：统一工具治理、参数修复、MCP 两步暴露、流/非流一致化与最终响应组装。

遵循 AGENTS.md 的 9 大架构原则：职责单一、最小兼容、Fail Fast、无兜底、配置驱动、统一出口/入口。

版本提示：
- 从 0.2.95 起，Responses→Chat 的映射严格“只做形状转换”，不再注入兜底文本或工具；所有治理只在后半段进行。
- 从 0.3.20 起，Anthropic/Chat/Responses 三端在 V2 前后半段上实现双向闭环：请求侧统一 Anth/Chat/Responses→Chat→providerProtocol，响应侧统一 providerProtocol→Chat→入口协议，所有工具调用/结果在 Anth↔Chat↔Anth 与 Responses↔Chat↔Responses 流程中保持字段/分组不变。
- 自 0.4.2 起，Responses ↔ Anthropic 走完整的 Chat 桥后可直接互通：Responses upstream SSE → Chat canonical → Anthropic outbound，以及 Anthropic inbound → Chat canonical → Responses outbound，工具调用/函数输出在双向往返中保持 shape，不再需要任何协议猜测或字段兜底。
- 规划中：Gemini/Chat 双向闭环（`gemini-messages` 协议）将按与 Anthropic 完全平行的方式接入，仅在前半段/后半段增加独立 codec，不影响现有 Chat/Responses/Anthropic 行为。

👉 **Hub Pipeline 是当前唯一入口。** 如果 Host 需要直接编排 Virtual Router，请参见 `docs/HUB_PIPELINE_USAGE.md`，了解如何通过 `bootstrapVirtualRouterConfig` + `HubPipeline` 完成初始化与热更新。

👉 **ServerTool 是所有 server-side 工具的唯一统一框架。** web_search / vision followup 等服务端工具均在 llmswitch-core 内部通过 ServerTool 执行，Host 只需提供 providerInvoker/reenterPipeline 即可。设计与接入方式详见 `docs/SERVERTOOL_DESIGN.md`。

👉 **Rust 逐步替换门禁与 Shadow 条件**：参见 `docs/rust-migration-gates.md`。

## 总览

```
入站请求（任意端点）
  ├─ Chat (/v1/chat)        ┐
  ├─ Responses (/v1/responses) ├─ 前半段 Conversion → 规范化为 Chat 标准 JSON（非流）
  └─ Anthropic (/v1/messages) ┘   - 仅做字段/形状映射；不做工具治理/文本收割
                                     - 统一关闭上游直通，前半段需要时将 SSE 合成为非流 JSON

                ▼
         Chat Back-Half（唯一治理入口）
           - 工具治理（canonicalize/repair/去重/ID配对）
           - MCP 两步暴露（列表→读取/模板）
           - reasoning/think 标准化（按端点策略保留或过滤）
           - finalize：确保 tool_calls / tool role、finish_reason、content 形状一致

                ▼
          Provider（HTTP 通信，仅转发）

                ▼
         出站响应（统一从 Chat 反向映射）
           - Chat：直接输出标准 OpenAI Chat 形状
           - Responses：从 Chat 还原 required_action / output / items
           - Anthropic：映射为 Anthropic 支持的形状
```

要点：
- “后半段唯一治理点”：三端（Chat/Responses/Anthropic）最终都走同一套 Chat 后半段。
- Responses↔Anthropic 互通：Responses provider 输出先回落到 Chat，再由入口端点（例如 `/v1/messages`）决定重新构建 Anthropic wire 形状；反向同理，Anthropic 输入先 canonical 化为 Chat 后再落地 Responses 请求，由 outbound streaming 配置决定最终 SSE/JSON 行为。
- “前半段最小映射”：只做协议字段/工具形状转换，不做文本工具收割/兜底/治理。
- “流式一致”：默认不上游直通（upstream SSE OFF），前半段将流合成为非流 JSON，再走后半段，保证一致。

## Conversion V3 节点架构与工具治理约束

- **唯一入口**：RouteCodex 主包只能通过 `src/modules/llmswitch/bridge.ts` → `dist/bridge/routecodex-adapter.js` 调用 conversion v3；禁止旁路 import。
- **配置驱动管线**：`config/llmswitch/pipeline-config.json`（或通过 `LLMSWITCH_PIPELINE_CONFIG` 指定）声明每条入/出站线路的节点序列：`SSE Input → Provider Input → Chat Process → Provider Output → SSE Output`，Responses/Anthropic 线路结构一致。
- **节点职责**
  - `nodes/sse/*`：入站 SSE 正规化、出站 SSE 序列化以及纯透传，占位但不改写业务数据。
  - `nodes/input/*`：OpenAI Chat / Responses / Anthropic 请求解析器，校验 `model/messages`，输出 canonical `standardizedRequest`。
  - `nodes/process/chat-process-node`：唯一的工具治理点，负责 tool_calls 修复、MCP 两步暴露、上下文与 streaming 策略、passthrough 判定等。
  - `nodes/output/*`：基于 `processedRequest` 生成 Provider 协议响应（choices、usage、content blocks）。
  - `nodes/response/*`：出站方向入口，把 Provider 响应重新映射为 canonical，再交由 output/sse 节点返回。
- **工具治理原则**
  - 除 Compatibility 层为了满足 Provider/OpenAI 形状所做的最小字段修剪外，任何模块都不得修改工具语义。
  - Input/Output/SSE/Response 节点只做格式转换或序列化；所有工具解析/修复/透传/执行判定必须发生在 `process` 链路。
  - 如需新增与工具相关的功能，必须在 `chat-process-node` 中实现或扩展新的 process 节点，并由 pipeline 配置显式启用。

## 流水线节点与职能（入站→出站）

1) HTTP Server 入站（端点路由）
- 接收 /v1/chat、/v1/responses、/v1/messages 的原始请求体
- 写入 http-request 快照（可选：.parsed 摘要）

2) 前半段 Conversion（本包 v2/conversion/...）
- Chat：校验/轻量规范（保持 OpenAI Chat 标准）
- Responses：instructions + input 映射为 Chat.messages；function_call/tool_result → assistant.tool_calls / role='tool'（只形状，不治理）
- Anthropic：Claude 消息/工具映射为 OpenAI Chat（仅形状）
- SSE：如入站为 SSE，先在前半段合成为非流 JSON（默认），确保后续统一路径

3) 后半段 Chat Pipeline（本包 v2/conversion/openai-chat/...）
- request-shape：
  - 统一 messages.content 为 string
  - 删除不支持字段（如 stream）
- request-tools-stage：
  - canonicalizeChatResponseTools：不变式（content=null，finish_reason=tool_calls）
  - JSON/JSON5 风格参数修复，失败回退 "{}"
  - 工具 ID 生成/去重
  - MCP 两步暴露（仅在后半段）
- provider 调用（Provider 层）：HTTP 转发 + 快照；不做工具处理
- response-shape：
  - 统一 Chat 响应形状、finish_reason、content
  - reasoning/think 清理或保留（按端点策略）
- response-tools-stage：
  - 工具结果配对（role='tool' 与 tool_calls.id 对齐）
  - Responses 反向桥接（仅映射，不治理）：required_action + items/output 还原

4) 出站响应
- Chat：OpenAI Chat
- Responses：OpenAI Responses
- Anthropic：Anthropic Messages

## 三端前半段：具体滤波器与映射（伪代码）

### Chat 前半段
```
function chatFrontHalf(payload):
  assert(Array.isArray(payload.messages))
  drop(payload.stream)          // 统一非流
  ensureOpenAIChatShape(payload)
  return payload
```

### Responses 前半段（严格不兜底）
```
function responsesFrontHalf(payload):
  ctx = captureResponsesContext(payload)
  tools = normalizeTools(payload.tools)
  msgs = []
  if ctx.instructions: msgs.push({role:'system', content:trim(ctx.instructions)})

  for entry in payload.input:
    switch(entry.type):
      case 'function_call'|'tool_call':
        name = entry.name || entry.function?.name
        args = parseArguments(entry.arguments || entry.function?.arguments)
        msgs.push({role:'assistant', tool_calls:[{id:genId(entry), type:'function', function:{name, arguments:stringify(args)}}]})
      case 'function_call_output'|'tool_result'|'tool_message':
        id = entry.tool_call_id || entry.call_id || entry.tool_use_id || entry.id
        out = normalizeToolOutput(entry.output)
        msgs.push({role:'tool', tool_call_id:id, content:String(out ?? '')})
      default:
        // 优先 entry.message.content[]
        if entry.message?.content: text = collectText(entry.message.content)
        else if entry.content:      text = collectText(entry.content)
        else if entry.text:         text = entry.text
        if text: msgs.push({role:normalizeRole(entry.role), content:text})

  return { model, messages: msgs, tools: tools, tool_choice: payload.tool_choice }
```

注意：不注入“伪 user”，不做文本工具收割/治理，严格只做形状转换。

### Anthropic 前半段
```
function anthropicFrontHalf(payload):
  // Claude → OpenAI Chat 映射
  for m in payload.messages:
    map role/parts → Chat message
  map tools → OpenAI function tools
  drop(stream)
  return chatPayload
```

### Gemini 前半段（规划中）

Gemini REST 规范使用 `contents[].role` + `parts[]` + `tools.functionDeclarations` 的结构，与 Anthropic 的 Messages 协议类似，本包会以完全平行的方式接入一个 `gemini-messages` 协议线：

目标：
- 仅在前半段/后半段增加 `gemini-messages` 的 codec，既有 Chat/Responses/Anthropic 行为不受影响。
- 非 passthrough 流水线继续遵循统一约束：`entryProtocol → Chat → providerProtocol`（请求）与 `providerProtocol → Chat → entryProtocol`（响应），中段只认 Chat。

计划中的 main codec 文件：
- `src/conversion/codecs/gemini-openai-codec.ts`
  - `buildOpenAIChatFromGeminiRequest(gReq)`：Gemini 请求 → OpenAI Chat 请求
  - `buildGeminiRequestFromOpenAIChat(chatReq)`：OpenAI Chat 请求 → Gemini 请求
  - `buildOpenAIChatFromGeminiResponse(gResp)`：Gemini 响应 → OpenAI Chat completion
  - `buildGeminiFromOpenAIChat(chatResp)`：OpenAI Chat completion → Gemini 响应（用于“入口/出口都是 Gemini”的回环测试与 `/v1/gemini` 出口）

请求侧（Gemini → Chat）映射要点：
- `contents: Content[]`
  - `Content.role`：
    - `"user"`   → Chat `role: "user"`
    - `"model"`  → Chat `role: "assistant"`
    - `"system"` → Chat `role: "system"`（或汇总进顶层 systemInstruction，再统一收敛为 Chat system 消息）
    - `"tool"`   → Chat `role: "tool"`（对应 functionResponse）
  - `Content.parts: Part[]`：
    - `{ text }` → Chat message.content 文本（可继续使用 text-markup-normalizer 做轻量标准化，不做治理）
    - `{ functionCall: { name, args } }` →
      - 映射为 Chat `assistant.tool_calls[]`：
        - `type: "function"`
        - `function.name = name`
        - `function.arguments = JSON 字符串`（通过现有 jsonish.repairArgumentsToString 进行安全修复）
    - `{ functionResponse: { name, response } }` →
      - 映射为 Chat `role: "tool"` 消息：
        - `tool_call_id` 与前一轮 `tool_calls` 对齐（ID 生成与去重仍由后半段工具治理负责）
        - `content` 为字符串化后的结果（不丢失结构，可放入 metadata 保留原始 JSON）
    - 其它多模态部件（`inlineData/fileData/...`）：
      - 按“不丢失信息”原则，先透传到 Chat 消息的 `metadata.vendor.gemini.parts[]`，后续再根据需要扩展。
- `systemInstruction?: Content`：
  - 统一转换为 Chat 顶层 system 消息（或合并进 messages 中的 system 段），保持与 Anthropic 一致的“所有系统指令在 Chat 段收敛”策略。
- `tools.functionDeclarations`：
  - 映射为 Chat `tools[].function`，字段基本一一对应（`name/description/parameters`）。
- 采样/安全配置：
  - `generationConfig.temperature/topP/topK/maxOutputTokens/stopSequences` 映射为 Chat 顶层采样参数；
  - `safetySettings` 透传到 `metadata.vendor.gemini.safetySettings`，保证信息不丢。

响应侧（Gemini → Chat）映射要点：
- `candidates[0].content.parts[]`：
  - text 部分 → Chat `choices[0].message.content`（多段合并）
  - functionCall 部分 → Chat `choices[0].message.tool_calls[]`（与请求侧规则对应）
- `finishReason` → Chat `finish_reason`（STOP → "stop"、MAX_TOKENS → "length" 等）
- `usageMetadata` → Chat `usage`（prompt_tokens/output_tokens/total）

Chat → Gemini（请求与响应）：
- 与 Anthropic 完全平行，在 `buildGeminiRequestFromOpenAIChat` / `buildGeminiFromOpenAIChat` 内按 Chat canonical 重新投影回 Gemini wire 形状：
  - system → `systemInstruction` 或系统 Content；
  - assistant + tool_calls → `role: "model", parts:[{ functionCall... }]`；
  - `role: "tool"` → `role: "tool", parts:[{ functionResponse... }]`；
  - tools → `functionDeclarations`；
  - 采样/安全配置 → `generationConfig` / `safetySettings`。

与现有协议的隔离性：
- `gemini-messages` 只在 providerProtocol 显式配置为 `"gemini-messages"` 时参与路由，且 codec 文件和分支独立存在：
  - 不修改 `openai-chat`、`openai-responses`、`anthropic-messages` 的现有逻辑；
  - 中段 Chat 工具治理/最终器不依赖具体 provider 协议，只认 canonical Chat completion。
- RouteCodex 主包侧仅需：
  - `providerType: "gemini"` → `providerProtocol: "gemini-messages"`；
  - Provider 继续复用 OpenAIStandard，仅负责 HTTP 通信。

测试与回环（规划）：
- 类似 Anthropic/Responses，将新增：
  - `gemini-request-closed-loop.ts`：Gemini 请求 → Chat 请求 → Gemini 请求'，检查字段/工具/role 数量与顺序一致。
  - `gemini-in-out-closed-loop.ts`：通过 routecodex-adapter 的 `processIncoming` / `processOutgoing` 对真实 codex-samples 做闭环。
  - 如有需要，扩展 streaming 测试，验证 `streamGenerateContent` → Chat SSE delta → Responses/Anthropic SSE 的一致性。


## 后半段：主要处理点与职责

- request-shape（v2/conversion/openai-chat/request-shape.ts）
  - 统一 messages.content 为 string
  - 删除不支持字段（如 stream）
- request-tools-stage（v2/conversion/openai-chat/request-tools-stage.ts）
  - canonicalizeChatResponseTools：不变式（content=null，finish_reason=tool_calls）
  - jsonish.repairArgumentsToString：把任意形态 arguments 修复为安全 JSON 字符串
  - 工具 ID 生成/去重
  - MCP 两步暴露（仅在后半段）
- response-shape（v2/conversion/openai-chat/response-shape.ts）
  - Chat 响应标准化；think/推理文本按端点策略保留/清理
- response-tools-stage（v2/conversion/openai-chat/response-tools-stage.ts）
  - role='tool' 与 tool_calls.id 配对
  - Responses 反向桥接（仅映射，不治理）：required_action + items/output 还原

## 流式（SSE）策略
- 默认不上游直通（provider 配置未显式允许时）。
- 前半段把 SSE 合成为非流 JSON；后半段统一处理，再需要时用本包 streaming 模块合成 Responses SSE。

## 快照与排错
- 快照目录：`~/.routecodex/codex-samples/<endpoint>/`
  - `*_http-request.json` / `*_http-request.parsed.json`
  - `*_pipeline.llmswitch.request.post.json`（进入后半段前的 Chat 形状）
  - `*_pipeline.provider.request.pre.json`（上游请求体，顶层仅 Chat 字段）
- 常见 1214 根因：
  - 缺失用户消息（Responses 输入未包含 user 文本，且前半段不兜底）
  - 顶层出现 data/metadata/stream 等额外键（应移除“添加逻辑”，而非末端清理）

## 设计原则与边界
- 工具治理唯一在后半段；前半段绝不进行文本工具收割/参数修复
- 兼容层只做 provider 特定最小映射；Provider 只做 HTTP 通信
- Fail Fast：形状不合规直接报错，不做隐藏兜底

## 版本与构建
- 构建：`npm run build`
- 打包：`npm pack`
- 发布：`npm publish`
