跳转至

Model Discovery Pipeline 全景

核心源码: proxy/src/models.ts (102L) 关联文件: proxy/src/types.ts (MonkeyCodeModel 类型), proxy/src/task-runner.ts (cli_name 选择) 覆盖: 模型发现 → 缓存 → 6 层解析 → 接口类型映射 → 全链路追踪


1. ModelManager 架构总览

类设计

// 摘自 proxy/src/models.ts — ModelManager 完整类
export class ModelManager {
  private auth: AuthManager      // 认证管理器
  private models: MonkeyCodeModel[] = []  // 模型缓存
  private lastFetch: number = 0  // 上次获取时间
  private cacheTTL: number = 5 * 60 * 1000  // 5 分钟缓存

  constructor(auth: AuthManager) {
    this.auth = auth
  }
}

数据流全链路

用户请求 (OpenAI SDK/curl)
  ▼ POST /v1/chat/completions { model: "gpt-4" }
  ▼ api-routes.ts: resolveModel("gpt-4")
  ┌─────────────────────────────────────────────────────┐
  │  ModelManager.resolveModel()                        │
  │                                                     │
  │  第 1 层: 精确匹配 monkeycode/provider/model 格式    │
  │  第 2 层: 匹配 provider/model 格式                   │
  │  第 3 层: 匹配 model 名称                             │
  │  第 4 层: 匹配 display_name                           │
  │  第 5 层: 返回 is_default 模型                        │
  │  第 6 层: 返回 models[0] (兜底)                      │
  └─────────────────────┬───────────────────────────────┘
                        ▼ MonkeyCodeModel { id, provider, model, interface_type, ... }
                        ▼ task-runner.ts: createTask()
                        ▼ POST /api/v1/users/tasks { model_id: model.id, cli_name: ... }

2. 模型发现 (fetchModels)

API 调用

// 摘自 proxy/src/models.ts:20-42 — 获取模型列表
async fetchModels(): Promise<MonkeyCodeModel[]> {
  // 缓存命中: 直接返回
  if (this.models.length > 0 && Date.now() - this.lastFetch < this.cacheTTL) {
    return this.models
  }

  const url = `${MONKEYCODE_BASE_URL}/api/v1/users/models`

  const response = await fetch(url, {
    headers: mkHeaders({
      Cookie: `${this.auth.getSessionCookieName()}=${this.auth.getSessionCookieSync()}`,
    }),
  })
  if (!response.ok) {
    throw new Error(`Failed to fetch models (${response.status}): ${await response.text()}`)
  }

  const result = await response.json()
  // 兼容两种响应格式:
  // 格式 A: { code: 0, data: { models: [...] } }
  // 格式 B: { models: [...] }
  const data = result.data || result
  this.models = data.models || []
  this.lastFetch = Date.now()

  console.log(`[Models] Fetched ${this.models.length} models`)
  return this.models
}

响应格式示例

// 格式 A (code + data 包装)
{
  "code": 0,
  "data": {
    "models": [
      {
        "id": "uuid-xxx",
        "provider": "openai",
        "model": "gpt-4o",
        "display_name": "GPT-4o",
        "interface_type": "openai_chat",
        "is_default": true,
        "is_free": false,
        "access_level": "pro",
        "thinking_enabled": false,
        "context_limit": 128000,
        "output_limit": 4096,
        "owner": "public",
        "name": "GPT-4o",
        "description": "OpenAI GPT-4o",
        "api_key": "sk-...",
        "base_url": "https://api.openai.com/v1",
        "temperature": 0.7
      }
    ]
  }
}

// 格式 B (直接 models 数组)
{
  "models": [...]
}

3. 6 层模型解析回退

核心解析方法

// 摘自 proxy/src/models.ts:64-88 — 6 层回退解析
async resolveModel(openaiModelId: string): Promise<MonkeyCodeModel | null> {
  const models = await this.fetchModels()

  // 第 1 层: 精确匹配 monkeycode/provider/model 格式
  // 示例: "monkeycode/openai/gpt-4o"
  const exact = models.find((m) => this.toOpenAIModelId(m) === openaiModelId)
  if (exact) return exact

  // 第 2 层: 匹配 provider/model 格式
  // 示例: "openai/gpt-4o"
  const byProviderModel = models.find(
    (m) => `${m.provider}/${m.model}` === openaiModelId
  )
  if (byProviderModel) return byProviderModel

  // 第 3 层: 模糊匹配 model 名称
  // 示例: "gpt-4o" (不区分 provider)
  const byModelName = models.find((m) => m.model === openaiModelId)
  if (byModelName) return byModelName

  // 第 4 层: 匹配 display_name
  // 示例: "GPT-4o" (用户友好的显示名称)
  const byDisplayName = models.find((m) => m.display_name === openaiModelId)
  if (byDisplayName) return byDisplayName

  // 第 5 层: 默认模型
  // 返回 is_default=true 的模型
  const defaultModel = models.find((m) => m.is_default)
  if (defaultModel) return defaultModel

  // 第 6 层: 兜底 — 返回第一个可用模型
  return models[0] || null
}

解析层对比

输入格式 匹配方式 用途 示例
1 monkeycode/{provider}/{model} 精确字符串 OpenAI SDK 标准格式 monkeycode/openai/gpt-4o
2 {provider}/{model} 精确字符串 简洁格式 openai/gpt-4o
3 {model} 精确字符串 不关心 provider gpt-4o
4 {display_name} 精确字符串 用户友好名称 GPT-4o
5 is_default 标志 默认模型 平台配置的默认值
6 数组第一个 兜底 任何可用模型

模型 ID 生成

// 摘自 proxy/src/models.ts:58-59 — OpenAI 兼容模型 ID 格式
toOpenAIModelId(m: MonkeyCodeModel): string {
  // 格式: monkeycode/{provider}/{model}
  // 示例: monkeycode/openai/gpt-4o
  //        monkeycode/anthropic/claude-3-opus-20240229
  //        monkeycode/deepseek/deepseek-coder
  return `monkeycode/${m.provider}/${m.model}`
}

4. 缓存策略

5 分钟缓存

// 摘自 proxy/src/models.ts — 缓存
private models: MonkeyCodeModel[] = []
private lastFetch: number = 0
private cacheTTL: number = 5 * 60 * 1000  // 5 分钟

async fetchModels(): Promise<MonkeyCodeModel[]> {
  // 缓存有效期内直接返回
  if (this.models.length > 0 && Date.now() - this.lastFetch < this.cacheTTL) {
    return this.models
  }
  // 缓存过期 → 重新获取
  // ...
}

// 清除缓存
clearCache(): void {
  this.models = []
  this.lastFetch = 0
}

缓存刷新方式

// 方式 1: 自动过期(5 分钟 TTL)
// 每次 fetchModels() 自动检查

// 方式 2: 手动刷新(管理端点)
// 摘自 proxy/src/server.ts:136-143
app.post("/admin/refresh-models", async (_req, res) => {
  try {
    modelManager.clearCache()       // 清除缓存
    const models = await modelManager.fetchModels()  // 重新获取
    res.json({ status: "ok", count: models.length })
  } catch (err: any) {
    res.status(500).json({ error: err.message })
  }
})

// 方式 3: OAuth 登录成功后自动刷新
// 摘自 proxy/src/server.ts:211-215
app.post("/admin/login/verify", async (req, res) => {
  // 登录成功后清除缓存 + 重新获取
  modelManager.clearCache()
  await modelManager.fetchModels()
})

5. 接口类型映射

interface_type → cli_name 映射

// 摘自 proxy/src/task-runner.ts:62-63 — cli_name 选择
cli_name: model.interface_type === "openai_responses" ? "codex"
  : model.interface_type === "anthropic" ? "claude"
  : "opencode",

接口类型 → SDK 映射

// Go 后端 SDK 选择逻辑(基于 interface_type)
interface InterfaceTypeSDKMapping = {
  "openai_chat": {
    sdk: "sashabaranov/go-openai",
    endpoint: "{baseURL}/chat/completions",
    format: "completion"  // 标准 ChatCompletion
  },
  "openai_responses": {
    sdk: "原生 HTTP",
    endpoint: "{baseURL}/responses",
    format: "streaming"  // Responses API 流式
  },
  "anthropic": {
    sdk: "anthropics/anthropic-sdk-go",
    endpoint: "{baseURL}/v1/messages",
    format: "messages"  // Anthropic Messages API
  }
}

代理接口类型获取

// 摘自 proxy/src/models.ts:93-94 — 获取接口类型
getInterfaceType(model: MonkeyCodeModel): InterfaceType {
  return model.interface_type
}

6. OpenAI 模型列表转换

toOpenAIModels 方法

// 摘自 proxy/src/models.ts:47-53 — 转换为 OpenAI 格式
async toOpenAIModels(): Promise<OpenAIModel[]> {
  const models = await this.fetchModels()
  return models.map((m) => ({
    id: this.toOpenAIModelId(m),      // monkeycode/{provider}/{model}
    object: "model" as const,
    created: Math.floor(Date.now() / 1000),
    owned_by: m.provider,             // 提供商名称
  }))
}

OpenAI 响应格式

// GET /v1/models 响应
{
  "object": "list",
  "data": [
    {
      "id": "monkeycode/openai/gpt-4o",
      "object": "model",
      "created": 1718245800,
      "owned_by": "openai"
    },
    {
      "id": "monkeycode/anthropic/claude-3-opus-20240229",
      "object": "model",
      "created": 1718245800,
      "owned_by": "anthropic"
    }
  ]
}

7. 全链路追踪

从请求到 LLM 调用

用户请求: POST /v1/chat/completions { model: "gpt-4o", messages: [...] }
① api-routes.ts: resolveModel("gpt-4o")
  ├── fetchModels() → 缓存检查 → 5 分钟有效
  ├── resolveModel("gpt-4o")
  │   ├── 第 1 层: "monkeycode/openai/gpt-4o" === "gpt-4o" → ❌
  │   ├── 第 2 层: "openai/gpt-4o" === "gpt-4o" → ❌
  │   ├── 第 3 层: "gpt-4o" === "gpt-4o" → ✅ 找到!
  │   └── 返回 MonkeyCodeModel { id: "uuid-xxx", provider: "openai", ... }
② task-runner.ts: createTask(model, prompt)
  ├── model.id → POST /api/v1/users/tasks { model_id: "uuid-xxx" }
  ├── model.interface_type → "openai_chat"
  └── cli_name → "opencode" (因为 interface_type 是 openai_chat)
③ Backend: TaskFlow VM 创建
  ├── image_id → Docker 镜像
  ├── model_id → 后端模型选择
  └── cli_name → Agent 包选择 (opencode NPM 包)
④ LLM Client: 根据 interface_type 选择 SDK
  ├── openai_chat → go-openai SDK → POST {baseURL}/chat/completions
  └── 最终调用 LLM 提供商

8. 配置项总览

环境变量

// 影响模型发现的配置
const MONKEYCODE_BASE_URL = process.env.MONKEYCODE_BASE_URL || "https://monkeycode-ai.com"
// 模型缓存 TTL 不可配置(硬编码 5 分钟)

// 影响任务创建的配置
const DEFAULT_HOST_ID = process.env.MONKEYCODE_HOST_ID || "public_host"
const DEFAULT_IMAGE_ID = process.env.MONKEYCODE_IMAGE_ID || ""

关键参数

参数 默认值 说明
API 端点 /api/v1/users/models 模型列表 API
缓存 TTL 5 分钟 硬编码,不支持配置
模型 ID 格式 monkeycode/{provider}/{model} OpenAI 兼容格式
默认 host_id public_host 可通过 MONKEYCODE_HOST_ID 配置
模型解析层级 6 层 从精确匹配到兜底

相关章节