import { BaseExecutor } from "./base.js"; import { PROVIDERS } from "../config/providers.js"; import { injectReasoningContent } from "../utils/reasoningContentInjector.js"; // Models that use /zen/v1/messages (claude format) const MESSAGES_MODELS = new Set(); // OpenCode free tier is limited per egress IP — a 429/403 with a limit-ish // body means the POOL's IP is exhausted, not the account. Declare it // pool-scoped so chatCore marks the pool unfit, retries via another pool, and // it shows up (clearable) on the Proxy Fitness page. const IP_LIMIT_BODY = /limit|rate|quota|exhausted|capacity|too many|retry/i; export class OpenCodeExecutor extends BaseExecutor { constructor() { super("opencode", PROVIDERS.opencode); } transformRequest(model, body) { return injectReasoningContent({ provider: this.provider, model, body }); } buildUrl(model) { const base = this.config.baseUrl; return MESSAGES_MODELS.has(model) ? `${base}/zen/v1/messages` : `${base}/zen/v1/chat/completions`; } buildHeaders() { return { "Content-Type": "application/json", "Authorization": "Bearer public", "x-opencode-client": "desktop", "Accept": "text/event-stream" }; } parseError(response, bodyText) { const status = response?.status || 0; const text = String(bodyText || ""); if ((status === 429 || status === 403) && IP_LIMIT_BODY.test(text)) { return { status, message: text.slice(0, 300) || `OpenCode free limit (${status})`, poolScoped: { reason: "ip-limit" }, }; } return null; // fall through to default parsing } }