Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • 匿名访问和免费试用机制

匿名访问和免费试用机制

OpenCode 允许未登录用户通过 匿名访问 机制免费试用某些 AI 模型(如 Claude Opus)。这为用户提供了无需注册即可体验 OpenCode 功能的途径,同时通过 IP 限制来控制资源消耗。

目录

  • 核心原理
  • 为什么可以短暂使用 opus
  • 完整工作流程
  • 关键配置项
  • 客户端适配
  • 安全考虑
  • 常见问题

核心原理

设计理念

匿名访问机制基于以下设计理念:

  1. 零门槛试用 - 无需注册登录即可体验
  2. IP 级别限制 - 防止单个 IP 滥用
  3. 模型白名单 - 只有特定模型允许匿名访问
  4. 自动降级 - 超过限额后优雅降级到付费流程

架构概览

为什么可以短暂使用 opus

关键代码分析

1. 客户端自动使用 "public" API Key

当用户未配置 API Key 时,OpenCode 客户端会自动使用 "public" 作为 API Key:

// packages/opencode/src/provider/provider.ts:100-121
async opencode(input) {
  const hasKey = await (async () => {
    const env = Env.all()
    if (input.env.some((item) => env[item])) return true
    if (await Auth.get(input.id)) return true
    const config = await Config.get()
    if (config.provider?.["opencode"]?.options?.apiKey) return true
    return false  // 没有 Key
  })()

  if (!hasKey) {
    // 删除所有有成本的模型
    for (const [key, value] of Object.entries(input.models)) {
      if (value.cost.input === 0) continue
      delete input.models[key]
    }

    // 返回 autoload: true 和 apiKey: "public"
    return {
      autoload: Object.keys(input.models).length > 0,
      options: { apiKey: "public" },  // 关键!
    }
  }
}

为什么模型没有被全部删除?

如果模型配置了 allowAnonymous: true,即使没有 Key 也不会被删除,因为这是在服务端验证的,不是在客户端。

2. 服务端允许匿名访问

// packages/console/app/src/routes/zen/util/handler.ts:400-405
async function authenticate(modelInfo: ModelInfo) {
  const apiKey = parseApiKey(headers)

  if (!apiKey || apiKey === "public") {
    if (modelInfo.allowAnonymous) return // 允许通过
    throw new AuthError("Missing API key.")
  }

  // ... 正常认证流程
}

3. 计费源验证返回 "free"

// packages/console/app/src/routes/zen/util/handler.ts:486-490
function validateBilling(authInfo: AuthInfo, modelInfo: ModelInfo) {
  if (!authInfo) return "anonymous" // 匿名用户
  if (authInfo.provider?.credentials) return "free"
  if (authInfo.isFree) return "free"
  if (modelInfo.allowAnonymous) return "free"
  // ...
}

4. 匿名用户不扣费

// packages/console/app/src/routes/zen/util/handler.ts:653, 720-722
async function trackUsage(...) {
  // ...

  if (billingSource === "anonymous") return  // 直接返回,不扣费

  // ...

  await Database.use((db) =>
    db.update(BillingTable).set({
      balance: authInfo.isFree
        ? sql`${BillingTable.balance} - ${0}`  // 免费,不扣费
        : sql`${BillingTable.balance} - ${cost}`,  // 正常扣费
    })
  )
}

5. 试用 Provider 选择

// packages/console/app/src/routes/zen/util/handler.ts:355-357
if (isTrial) {
  return modelInfo.providers.find((provider) => provider.id === modelInfo.trial!.provider)
}

完整流程示例

场景: 用户首次使用 OpenCode,未配置 API Key,请求 claude-opus-4-5-20251101 模型

  1. 客户端初始化

    // 用户运行 opencode
    // ~/.opencode/auth.json 不存在或为空
    
    const providers = await Provider.list()
    // opencode provider 自动设置为 { apiKey: "public" }
    
  2. 发送请求到 OpenCode 服务

    POST /v1/chat/completions
    Authorization: public  ← 关键!
    Content-Type: application/json
    
    {
      "model": "claude-opus-4-5-20251101",
      "messages": [...]
    }
    
  3. 服务端认证

    // handler.ts:400-405
    if (!apiKey || apiKey === "public") {
      if (modelInfo.allowAnonymous) return // ✅ 允许
    }
    
  4. 检查试用限制

    // trialLimiter.ts:18-30
    const data = await Database.use(
      (tx) => tx.select({ usage: IpTable.usage }).from(IpTable).where(eq(IpTable.ip, "1.2.3.4")), // 用户 IP
    )
    
    _isTrial = (data?.usage ?? 0) < 50000 // 假设限额 50K
    return _isTrial // true(首次使用)
    
  5. 选择 Provider

    // handler.ts:355-357
    if (isTrial) {
      // 使用试用 Provider(如 Anthropic)
      return modelInfo.providers.find((p) => p.id === "anthropic")
    }
    
  6. 调用上游 API

    POST https://api.anthropic.com/v1/messages
    Authorization: sk-ant-...  // OpenCode 的内部 Key
    
  7. 追踪使用量

    // trialLimiter.ts:32-46
    await Database.use((tx) =>
      tx
        .insert(IpTable)
        .values({ ip: "1.2.3.4", usage: 12000 })
        .onDuplicateKeyUpdate({
          set: { usage: sql`${IpTable.usage} + 12000` },
        }),
    )
    
  8. 返回响应

    {
      "choices": [{ "message": { "content": "..." } }],
      "usage": { "input_tokens": 5000, "output_tokens": 7000 }
    }
    
  9. 用户再次请求(已使用 12K tokens)

    IP 当前使用量: 12K
    配置的限额: 50K
    isTrial = true  // 继续允许
    
  10. 达到限额后(已使用 52K tokens)

    IP 当前使用量: 52K
    配置的限额: 50K
    isTrial = false  // 拒绝访问
    

    返回错误:

    {
      "type": "error",
      "error": {
        "type": "RateLimitError",
        "message": "Subscription quota exceeded. Retry in 30min."
      }
    }
    

完整工作流程

关键配置项

模型配置

{
  "models": {
    "claude-opus-4-5-20251101": {
      "name": "Claude Opus 4.5",
      "allowAnonymous": true, // ← 关键:允许匿名访问
      "trial": {
        "provider": "anthropic", // 试用使用的 Provider
        "limits": [
          {
            "limit": 50000, // CLI 客户端:50K tokens
            "client": "cli"
          },
          {
            "limit": 30000, // Desktop 客户端:30K tokens
            "client": "desktop"
          },
          {
            "limit": 20000 // 默认:20K tokens
          }
        ]
      },
      "providers": [
        {
          "id": "anthropic", // 试用 Provider
          "model": "claude-3-5-opus-20250206",
          "weight": 1
        },
        {
          "id": "openai", // 备用 Provider
          "model": "gpt-4-turbo",
          "weight": 1
        }
      ]
    }
  }
}

环境变量

服务端需要的环境变量:

# Stripe(用于计费)
STRIPE_SECRET_KEY=sk_live_...

# 数据库连接(PlanetScale)
DATABASE_URL=...

# OpenCode 内部 API Keys(用于试用 Provider)
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GOOGLE_API_KEY=...

客户端适配

CLI 客户端

// packages/opencode/src/provider/provider.ts

// 1. 检测是否有 API Key
const hasKey = await (async () => {
  const env = Env.all()
  if (env["OPENCODE_API_KEY"]) return true
  if (await Auth.get("opencode")) return true
  const config = await Config.get()
  if (config.provider?.["opencode"]?.options?.apiKey) return true
  return false
})()

// 2. 如果没有 Key,使用 "public"
if (!hasKey) {
  return {
    autoload: true,
    options: { apiKey: "public" },
  }
}

// 3. 请求时自动附加客户端标识
const headers = {
  Authorization: `Bearer ${apiKey}`, // "Bearer public"
  "x-opencode-client": "cli", // 标识为 CLI 客户端
  "x-opencode-session": sessionId,
}

Desktop 客户端

// packages/desktop/src/api.ts

const headers = {
  Authorization: `Bearer ${apiKey}`,
  "x-opencode-client": "desktop", // 标识为 Desktop 客户端
  "x-opencode-session": sessionId,
}

安全考虑

1. IP 限制

  • 防止滥用: 单个 IP 最多只能使用配额内的 tokens
  • 防止滥用: 共享 IP 的所有用户共享配额
  • 注意: IP 地址从 x-real-ip 头获取,依赖代理(如 Cloudflare)

2. Token 计算

// 追踪所有类型的 tokens
const usage =
  usageInfo.inputTokens +
  usageInfo.outputTokens +
  (usageInfo.reasoningTokens ?? 0) +
  (usageInfo.cacheReadTokens ?? 0) +
  (usageInfo.cacheWrite5mTokens ?? 0) +
  (usageInfo.cacheWrite1hTokens ?? 0)

3. 模型白名单

只有明确配置 allowAnonymous: true 的模型才能被匿名访问,防止所有模型被滥用。

4. 成本过滤

客户端在有成本限制时会过滤模型,但最终由服务端验证:

// 客户端过滤(可绕过)
if (!hasKey) {
  for (const [key, value] of Object.entries(input.models)) {
    if (value.cost.input === 0) continue
    delete input.models[key]
  }
}

// 服务端验证(不可绕过)
if (apiKey === "public" && !modelInfo.allowAnonymous) {
  throw new AuthError("Missing API key.")
}

5. 速率限制

除了 IP token 限制,还有全局速率限制:

const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip)
await rateLimiter?.check() // 如果超过速率,抛出 RateLimitError

常见问题

Q1: 为什么我的配额这么快就用完了?

A: 可能的原因:

  1. IP 共享: 如果你在公司网络,同事也在用 OpenCode,你们共享配额
  2. 推理模型: 如 Opus、Sonnet 等,每个请求消耗大量 tokens
  3. 缓存未计算: 实际上所有 tokens 都被计入,包括缓存读取

解决方法:

# 查看你的公网 IP
curl https://api.ipify.org

# 联系 OpenCode 团队了解是否可以增加配额

Q2: 如何重置我的试用配额?

A: 目前没有自动重置机制。可能的解决方法:

  1. 更换网络: 使用不同的 IP 地址(如移动热点)
  2. **注册账户: 创建正式账户使用付费额度

Q3: 匿名访问的请求会被记录吗?

A: 是的,但是:

  • 不记录到 UsageTable(billingSource === "anonymous" 时直接返回)
  • 只记录到 IpTable 用于限制追踪
  • 日志中仍然有请求记录(用于监控和调试)

Q4: 为什么有些模型不能匿名访问?

A: 只有配置了 allowAnonymous: true 的模型才能匿名访问。这是出于成本控制的考虑:

  • 高成本模型(如 Opus)通常有较小的试用配额
  • 实验性模型可能不提供试用
  • 定制模型完全禁用匿名访问

Q5: 可以在多个设备上同时使用同一个 IP 的配额吗?

A: 可以,但共享配额:

Q6: "账户到限额了" 错误是什么意思?

A: 这个错误可能来源于:

  1. 匿名试用: IP token 限制
  2. 订阅用户: 订阅配额已耗尽
  3. 按量付费: 余额不足

区分方法:

相关文档

  • 计费和认证系统 - 完整的计费和认证架构
  • Provider/Model - 模型配置和提供商管理
  • Config - 配置系统

代码位置

功能文件路径行号
匿名访问处理packages/console/app/src/routes/zen/util/handler.ts400-405
试用限制检查packages/console/app/src/routes/zen/util/trialLimiter.ts6-49
Provider 选择packages/console/app/src/routes/zen/util/handler.ts340-398
计费源验证packages/console/app/src/routes/zen/util/handler.ts486-580
使用量追踪packages/console/app/src/routes/zen/util/handler.ts592-749
客户端 API Key 处理packages/opencode/src/provider/provider.ts100-121
免费工作区配置packages/console/app/src/routes/zen/util/handler.ts56-59