* Call the planner model to get a lightweight numbered plan for a task. * Returns a plan string or null if planning should be skipped. * * @param {string} task - The user's task description * @param {object} config - SmallCode config (for API key etc.)
(task, config)
| 67 | * @param {object} config - SmallCode config (for API key etc.) |
| 68 | */ |
| 69 | async function callPlanner(task, config) { |
| 70 | const chain = getChainConfig(); |
| 71 | if (!chain.enabled || !chain.planner) return null; |
| 72 | |
| 73 | const baseUrl = chain.plannerUrl || chain.baseUrl; |
| 74 | const url = `${baseUrl}/chat/completions`; |
| 75 | const apiKey = process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || config?.model?.apiKey; |
| 76 | const headers = { 'Content-Type': 'application/json' }; |
| 77 | if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`; |
| 78 | |
| 79 | const body = { |
| 80 | model: chain.planner, |
| 81 | temperature: 0.1, |
| 82 | max_tokens: 512, |
| 83 | messages: [ |
| 84 | { |
| 85 | role: 'system', |
| 86 | content: `You are a task planner. Given a coding task, output ONLY a numbered list of 2-6 concrete steps. No explanations. Just the numbered plan.`, |
| 87 | }, |
| 88 | { |
| 89 | role: 'user', |
| 90 | content: `Task: ${task.slice(0, 800)}`, |
| 91 | }, |
| 92 | ], |
| 93 | }; |
| 94 | |
| 95 | try { |
| 96 | // Use native fetch (Node 18+) or fall back to node-fetch v2 (CommonJS). |
| 97 | // node-fetch v3 is ESM-only and won't work with require(). |
| 98 | let fetcher = globalThis.fetch; |
| 99 | if (!fetcher) { |
| 100 | try { fetcher = require('node-fetch'); } catch {} |
| 101 | } |
| 102 | if (!fetcher) return null; |
| 103 | |
| 104 | const resp = await Promise.race([ |
| 105 | fetcher(url, { method: 'POST', headers, body: JSON.stringify(body) }), |
| 106 | new Promise((_, rej) => setTimeout(() => rej(new Error('planner timeout')), 15000)), |
| 107 | ]); |
| 108 | if (!resp.ok) return null; |
| 109 | const data = await resp.json(); |
| 110 | const content = data?.choices?.[0]?.message?.content || ''; |
| 111 | if (!content || content.length < 10) return null; |
| 112 | |
| 113 | // Validate it looks like a plan (has at least one numbered line) |
| 114 | if (!/^\s*\d+[\.\)]/m.test(content)) return null; |
| 115 | |
| 116 | return content.trim(); |
| 117 | } catch { |
| 118 | return null; // planner unavailable — fall through to executor |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | /** |
| 123 | * Get the executor model name for a task, respecting chain config. |
no test coverage detected