BigPizzaV3/CodexPlusPlus · error
API 未返回有效的 choices[0].message.content,请确认兼容 Chat Completions
Error message
API 未返回有效的 choices[0].message.content,请确认兼容 Chat Completions
What it means
completionText() extracts choices[0].message.content and requires a non-empty string after normalization (string content, or array of {type:'text'} parts joined). If the trimmed value is empty — or the response lacks the standard OpenAI choices[0].message.content shape entirely — this error is thrown, indicating the endpoint is not behaving like Chat Completions.
Solutions
- Verify the endpoint URL resolves to .../chat/completions (apiEndpoint appends it automatically — don't pre-strip it)
- Use a provider/model that returns plain message.content text for chat requests
- Inspect the raw JSON body to confirm choices[0].message.content exists and is non-empty
- If using a reasoning model, request the final-answer content or configure the proxy to surface it in message.content
Example fix
// before
const body = await fetch(embeddingsUrl, ...); // wrong endpoint shape
const text = completionText(await body.json()); // throws
// after
const body = await fetch('https://api.example.com/v1/chat/completions', {method:'POST', body: JSON.stringify({model, messages})});
const text = completionText(await body.json()); Defensive patterns
Strategy: validation
Validate before calling
function hasChatContent(body) {
const c = body?.choices?.[0]?.message?.content;
const v = typeof c === 'string' ? c : Array.isArray(c) ? c.filter(p => p?.type === 'text').map(p => p.text || '').join('') : '';
return v.trim().length > 0;
}
if (!hasChatContent(body)) console.warn('endpoint not Chat-Completions compatible', JSON.stringify(body).slice(0, 500)); Type guard
function isChatCompletionBody(b) { return Array.isArray(b?.choices) && b.choices.length > 0 && b.choices[0]?.message != null; } Try / catch
try { text = completionText(body); }
catch (e) { if (String(e).includes('choices[0].message.content')) { dumpRawBodyForDebug(body); showError('Endpoint incompatible with Chat Completions'); } return; } Prevention
- Point baseUrl at the chat completions root; let apiEndpoint append /chat/completions
- Smoke-test the endpoint with a tiny 1-message request before large batches
- Avoid reasoning-only or tool-only models for batch organization
- Log the raw JSON on failure to see the actual response shape
When it happens
Trigger: Response with choices: [] or missing choices; message.content null/'' (e.g. only tool_calls or reasoning returned); content array of non-text parts; endpoints returning Responses-API or other non-Chat-Completions shapes.
Common situations: Pointing the base URL at a non-chat endpoint (e.g. /v1/embeddings or /v1/responses); providers that return reasoning-only output with empty content; proxies that strip message.content; model that only emitted tool calls.
Understand the failure class
Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.
Related errors
- API 输出达到长度上限,本批未提交;请使用输出容量更大的模型
- API 未能生成本批整理结果
- 历史消息接口返回格式无效
- 整理侧边对话尚未创建完成
- result?.message||'Codex++ 会话读取失败'
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/f06950249bb9ce6d.
Report an issue: GitHub.
Appendix: source
Thrown at tools/conversation-canvas/external-api.mjs:82
else buffer+=text;
if(sse){let end;while((end=buffer.indexOf('\n'))>=0){consume(buffer.slice(0,end).replace(/\r$/,''));buffer=buffer.slice(end+1);}}
progress({bytes:size,chars:content.length});if(done)break;
}
const tail=decoder.decode();if(sse){buffer+=tail;if(buffer.trim())consume(buffer.replace(/\r$/,''));
if(!done&&!finish)throw Object.assign(Error('API 流式连接提前结束,本批未提交'),{canvasApiLocal:true,retryable:true});
return {choices:[{finish_reason:finish,message:{content}}]};
}
try{return JSON.parse(raw+tail);}catch{throw Object.assign(Error('API 返回的不是 JSON,请检查 API 地址'),{canvasApiLocal:true});}
}finally{void reader.cancel().catch(()=>{});try{reader.releaseLock();}catch{}}
}
export function completionText(body){
const choice=body?.choices?.[0];
if(choice?.finish_reason==='length')throw Error('API 输出达到长度上限,本批未提交;请使用输出容量更大的模型');
if(choice?.finish_reason==='content_filter'||choice?.message?.refusal)throw Error('API 未能生成本批整理结果');
const content=choice?.message?.content;
const value=typeof content==='string'?content:Array.isArray(content)?content.filter(p=>p?.type==='text').map(p=>p.text||'').join(''):'';
if(!value.trim())throw Error('API 未返回有效的 choices[0].message.content,请确认兼容 Chat Completions');
return value;
}
export function waitForApi(promise,signal){
signal?.throwIfAborted();
if(!signal)return promise;
return new Promise((resolve,reject)=>{
const abort=()=>{signal.removeEventListener('abort',abort);reject(signal.reason||new DOMException('Aborted','AbortError'));};
signal.addEventListener('abort',abort,{once:true});
promise.then(value=>{signal.removeEventListener('abort',abort);resolve(value);},error=>{signal.removeEventListener('abort',abort);reject(error);});
});
}
export function createApiOrganizer({request,timeoutMs=300000,maxRetries=2,retryDelayMs=2000}){
const requests=new Map();
const closed=()=>new DOMException('整理连接已关闭','AbortError');
const dispose=()=>{for(const entry of requests.values())entry.controller.abort(closed());requests.clear();};
async function run(config,prompt,requestId,session,onSession,signal,progress=()=>{}){View on GitHub (pinned to b1ed92e5e4)