匿名访问和免费试用机制
OpenCode 允许未登录用户通过 匿名访问 机制免费试用某些 AI 模型(如 Claude Opus)。这为用户提供了无需注册即可体验 OpenCode 功能的途径,同时通过 IP 限制来控制资源消耗。
目录
核心原理
设计理念
匿名访问机制基于以下设计理念:
- 零门槛试用 - 无需注册登录即可体验
- IP 级别限制 - 防止单个 IP 滥用
- 模型白名单 - 只有特定模型允许匿名访问
- 自动降级 - 超过限额后优雅降级到付费流程
架构概览
为什么可以短暂使用 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 模型
客户端初始化
// 用户运行 opencode // ~/.opencode/auth.json 不存在或为空 const providers = await Provider.list() // opencode provider 自动设置为 { apiKey: "public" }发送请求到 OpenCode 服务
POST /v1/chat/completions Authorization: public ← 关键! Content-Type: application/json { "model": "claude-opus-4-5-20251101", "messages": [...] }服务端认证
// handler.ts:400-405 if (!apiKey || apiKey === "public") { if (modelInfo.allowAnonymous) return // ✅ 允许 }检查试用限制
// 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(首次使用)选择 Provider
// handler.ts:355-357 if (isTrial) { // 使用试用 Provider(如 Anthropic) return modelInfo.providers.find((p) => p.id === "anthropic") }调用上游 API
POST https://api.anthropic.com/v1/messages Authorization: sk-ant-... // OpenCode 的内部 Key追踪使用量
// 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` }, }), )返回响应
{ "choices": [{ "message": { "content": "..." } }], "usage": { "input_tokens": 5000, "output_tokens": 7000 } }用户再次请求(已使用 12K tokens)
IP 当前使用量: 12K 配置的限额: 50K isTrial = true // 继续允许达到限额后(已使用 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: 可能的原因:
- IP 共享: 如果你在公司网络,同事也在用 OpenCode,你们共享配额
- 推理模型: 如 Opus、Sonnet 等,每个请求消耗大量 tokens
- 缓存未计算: 实际上所有 tokens 都被计入,包括缓存读取
解决方法:
# 查看你的公网 IP
curl https://api.ipify.org
# 联系 OpenCode 团队了解是否可以增加配额
Q2: 如何重置我的试用配额?
A: 目前没有自动重置机制。可能的解决方法:
- 更换网络: 使用不同的 IP 地址(如移动热点)
- **注册账户: 创建正式账户使用付费额度
Q3: 匿名访问的请求会被记录吗?
A: 是的,但是:
- 不记录到
UsageTable(billingSource === "anonymous" 时直接返回) - 只记录到
IpTable用于限制追踪 - 日志中仍然有请求记录(用于监控和调试)
Q4: 为什么有些模型不能匿名访问?
A: 只有配置了 allowAnonymous: true 的模型才能匿名访问。这是出于成本控制的考虑:
- 高成本模型(如 Opus)通常有较小的试用配额
- 实验性模型可能不提供试用
- 定制模型完全禁用匿名访问
Q5: 可以在多个设备上同时使用同一个 IP 的配额吗?
A: 可以,但共享配额:
Q6: "账户到限额了" 错误是什么意思?
A: 这个错误可能来源于:
- 匿名试用: IP token 限制
- 订阅用户: 订阅配额已耗尽
- 按量付费: 余额不足
区分方法:
相关文档
- 计费和认证系统 - 完整的计费和认证架构
- Provider/Model - 模型配置和提供商管理
- Config - 配置系统
代码位置
| 功能 | 文件路径 | 行号 |
|---|---|---|
| 匿名访问处理 | packages/console/app/src/routes/zen/util/handler.ts | 400-405 |
| 试用限制检查 | packages/console/app/src/routes/zen/util/trialLimiter.ts | 6-49 |
| Provider 选择 | packages/console/app/src/routes/zen/util/handler.ts | 340-398 |
| 计费源验证 | packages/console/app/src/routes/zen/util/handler.ts | 486-580 |
| 使用量追踪 | packages/console/app/src/routes/zen/util/handler.ts | 592-749 |
| 客户端 API Key 处理 | packages/opencode/src/provider/provider.ts | 100-121 |
| 免费工作区配置 | packages/console/app/src/routes/zen/util/handler.ts | 56-59 |