跳转至

LLM 接口类型详解(源码增强版)

核心发现: 3 种接口决定 3 种 SDK、3 种 API 格式、3 种 Agent 类型 代理层关键代码: api-routes.ts Chat + Responses 双模式、task-runner.ts cli_name 映射

1. 接口类型全景

特性 openai_chat openai_responses anthropic
底层 SDK @ai-sdk/openai-compatible @ai-sdk/openai @ai-sdk/anthropic
API 格式 Chat Completions Responses API Messages API
端点路径 {baseURL}/chat/completions {baseURL}/responses {baseURL}/v1/messages
Go SDK sashabaranov/go-openai 原生 HTTP anthropics/anthropic-sdk-go
流式事件 chat.completion.chunk response.* events content_block_delta
工具调用 tool_calls function_call tool_use
Agent 类型 opencode codex claude
代理端点 POST /v1/chat/completions POST /v1/responses 同 Chat(代理转换)

2. Go 后端 SDK 选择

// backend/pkg/taskflow/vm.go — 接口类型决定容器内 NPM 包
func getNpmPackage(interfaceType InterfaceType) string {
    switch interfaceType {
    case InterfaceOpenAIChat:
        return "@ai-sdk/openai-compatible"   // 通用 OpenAI 兼容
    case InterfaceOpenAIResponses:
        return "@ai-sdk/openai"              // OpenAI 官方 SDK
    case InterfaceAnthropic:
        return "@ai-sdk/anthropic"           // Anthropic 官方 SDK
    }
    return "@ai-sdk/openai-compatible"       // 默认回退
}

Go 端统一请求结构体

// pkg/llm/client.go — 三种接口共享的请求/响应格式
type ChatRequest struct {
    Messages      []Message     `json:"messages"`
    Model         string        `json:"model,omitempty"`
    MaxTokens     int           `json:"max_tokens,omitempty"`
    Temperature   float32       `json:"temperature,omitempty"`
    System        string        `json:"system,omitempty"`
    InterfaceType InterfaceType `json:"interface_type,omitempty"`
}

type ChatResponse struct {
    Content string `json:"content"`
    Usage   Usage  `json:"usage"`
}

// LLMConfig 注入到容器的 LLM 配置
type LLMConfig struct {
    APIKey  string `json:"api_key"`
    BaseURL string `json:"base_url"`
    Model   string `json:"model"`
    APIType string `json:"api_type"`    // "anthropic" | "openai"
}

3. 代理层 Chat 模式实现

3.1 Chat Completions 路由

// proxy/src/api-routes.ts — POST /v1/chat/completions
router.post("/v1/chat/completions", async (req, res) => {
  try {
    const body: OpenAIChatCompletionRequest = req.body

    // 验证请求
    if (!body.messages || body.messages.length === 0) {
      res.status(400).json({ error: { message: "messages is required" } })
      return
    }

    // 解析模型
    const model = await modelManager.resolveModel(body.model || "")
    if (!model) {
      res.status(404).json({ error: { message: `Model '${body.model}' not found` } })
      return
    }

    // 检查对话复用
    const conversation = body.conversation_id
      ? conversationManager?.get(body.conversation_id) : undefined

    if (conversation) {
      // 复用已有对话 → 发送最后一条消息
      conversation.sendUserInput(body.messages[body.messages.length - 1].content)
      // 流式/非流式响应
      if (body.stream) {
        await handleConversationStreamResponse(res, conversationManager!, conversation)
      } else {
        await handleConversationNonStreamResponse(res, conversationManager!, conversation)
      }
    } else {
      // 创建新任务
      const accountAuth = accountPool?.acquireWs() || accountPool?.acquireHttp() || null

      // 提取 system prompt 和用户消息
      const systemMsg = body.messages.find(m => m.role === "system")
      const prompt = body.messages.filter(m => m.role !== "system")
        .map(m => `[${m.role === "user" ? "User" : "Assistant"}]\n${m.content}`).join("\n\n")

      // 创建 MonkeyCode 任务
      const taskId = await taskRunner.createTask(model, prompt, {
        authOverride: accountAuth || undefined,
        systemPrompt: systemMsg?.content,
      })

      // 创建对话管理
      if (conversationManager) {
        conversation = conversationManager.create(taskId, model, accountAuth, body.messages)
      }

      // 流式/非流式响应
      if (body.stream) {
        await handleStreamResponse(res, taskRunner, taskId, model, prompt, accountPool, accountAuth)
      } else {
        await handleNonStreamResponse(res, taskRunner, taskId, model, prompt, accountPool, accountAuth)
      }
    }
  } catch (err: any) {
    if (!res.headersSent) {
      res.status(500).json({ error: { message: err.message, type: "internal_error" } })
    }
  }
})

3.2 ACP → Chat Chunk 转换

// proxy/src/task-runner.ts — ACP 事件转 OpenAI Chat Chunk
private handleACPEvent(acp, chatId, now, onChunk, usage) {
  switch (acp.type) {
    case "agent_message_chunk":
      onChunk({ id: chatId, object: "chat.completion.chunk",
        choices: [{ delta: { content: acp.text }, finish_reason: null }] })
      break
    case "agent_thought_chunk":
      onChunk({ id: chatId, object: "chat.completion.chunk",
        choices: [{ delta: { content: `[Thinking] ${acp.text}` }, finish_reason: null }] })
      break
    case "tool_call":
      onChunk({ id: chatId, object: "chat.completion.chunk",
        choices: [{ delta: { content: `[Tool: ${acp.tool_name}] ${acp.tool_input}` }, finish_reason: null }] })
      break
    case "task-ended":
      onChunk({ id: chatId, object: "chat.completion.chunk",
        choices: [{ delta: {}, finish_reason: "stop" }],
        usage: usage.total_tokens > 0 ? usage : undefined })
      break
  }
}

3.3 三种接口对应同一 SSE 格式

// 三种接口类型代理都输出此格式
// Chat: 直接 SSE 流
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"你好"},"finish_reason":null}]}

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":100,"completion_tokens":50,"total_tokens":150}}

data: [DONE]

4. 代理层 Responses 模式实现

4.1 Responses API 路由

// proxy/src/api-routes.ts — POST /v1/responses (Codex 原生模式)
router.post("/v1/responses", async (req, res) => {
  const { model: modelId, input, max_output_tokens } = req.body

  // 解析模型 + 创建任务(同上 Chat 模式)
  const model = await modelManager.resolveModel(modelId || "")

  // 归一化 input 为 prompt
  let prompt = ""
  if (typeof input === "string") prompt = input
  else if (Array.isArray(input)) {
    // 从 messages 格式提取 system + user 消息
    prompt = input.map(m => /* ... */).join("\n\n")
  }

  const accountAuth = accountPool?.acquireWs() || accountPool?.acquireHttp() || null
  const taskId = await taskRunner.createTask(model, prompt, { authOverride: accountAuth })

  // SSE: Responses 模式(多个事件类型)
  res.setHeader("Content-Type", "text/event-stream")

  // 发送 response.created
  sendEvent("response.created", { type: "response.created", response: { id: `resp-${taskId}`, status: "in_progress" } })

  // 接收 ACP 事件 → Responses 事件
  taskRunner.streamTaskRaw(taskId, prompt, (event) => {
    if (event.type === "acp") {
      const acp = event.data
      if (acp.type === "agent_message_chunk") {
        sendEvent("response.output_text.delta", { delta: { text: acp.text } })
      } else if (acp.type === "tool_call") {
        sendEvent("response.output_item.added", { item: { type: "function_call", name: acp.tool_name } })
      }
    }
  })
})

4.2 Responses 事件流对比

Chat 模式 SSE:                              Responses 模式 SSE:
─────────────────                           ─────────────────────
data: {"choices":[...]}                     event: response.created
                                            event: response.output_item.added
data: {"choices":[...]}                     event: response.output_text.delta
                                            event: response.function_call_arguments.delta
data: {"choices":[...],"usage":...}         event: response.function_call_arguments.done
                                            event: response.output_item.done
data: [DONE]                                event: response.completed

5. cli_name 映射链

// proxy/src/task-runner.ts — 动态 Agent 选择
cli_name: model.interface_type === "openai_responses" ? "codex"
  : model.interface_type === "anthropic" ? "claude"
  : "opencode",
interface_type cli_name Agent NPM 包
openai_chat opencode OpenCode @ai-sdk/openai-compatible
openai_responses codex Codex CLI @ai-sdk/openai
anthropic claude Claude Code @ai-sdk/anthropic

6. 三种接口的 HTTP 请求示例

openai_chat

POST {baseURL}/chat/completions
Authorization: Bearer {apiKey}
Content-Type: application/json

{
  "model": "gpt-4o",
  "messages": [{"role": "user", "content": "Hello"}],
  "temperature": 0.7,
  "stream": true
}

openai_responses

POST {baseURL}/responses
Authorization: Bearer {apiKey}

{
  "model": "gpt-4o",
  "input": "Hello",
  "stream": true
}

anthropic

POST {baseURL}/v1/messages
x-api-key: {apiKey}
anthropic-version: 2023-06-01

{
  "model": "claude-sonnet-4-20250514",
  "messages": [{"role": "user", "content": "Hello"}],
  "max_tokens": 1000,
  "stream": true
}

相关章节