openvibers/openvibe.ai Commits

Reasoning models get room to think in pass-through completions (max_tokens + 1024, low effort): a long tool result no longer ends a turn empty (#147)

OP OpenVibers committed 19 days ago · cb073c30ca · 2 files, +9 −4

Changes

ddiff --git a/server/providers/openai.js b/server/providers/openai.js
iindex ed190ca..218833a 100644
---- a/server/providers/openai.js
++++ b/server/providers/openai.js
@@@ -130,7 +130,8 @@ function createOpenAiProvider(record, { apiKey = '', fetchImpl = globalThis.fetc
/**
* complete (the OpenAI-compatible endpoint, /v1/chat/completions): the caller's Chat Completions request as they
* wrote it (messages with tool calls and tool results, tools, tool_choice, response_format, stop, seed), sent to
-- * this upstream with the routed model. A reasoning model takes max_completion_tokens and its default temperature.
++ * this upstream with the routed model. A reasoning model takes max_completion_tokens (with room to think), low
++ * reasoning effort and its default temperature.
* → { completion, text, model, usage }
*/
async function complete(req) {
@@@ -140,7 +141,11 @@ function createOpenAiProvider(record, { apiKey = '', fetchImpl = globalThis.fetc
if (o[k] !== undefined && o[k] !== null) body[k] = o[k];
}
if (isReasoningModel(req.model)) {
-- if (req.maxTokens) body.max_completion_tokens = req.maxTokens;
++ // max_tokens is the answer the caller wants; a reasoning model thinks first, out of the same allowance. Without
++ // room for that, and with its default (medium) effort, a long tool result could use it all up thinking and
++ // answer nothing: Chat's first production inbox action (2026-10-25) ended "finish_reason: length", empty.
++ if (req.maxTokens) body.max_completion_tokens = Math.max(req.maxTokens, 256) + 1024;
++ body.reasoning_effort = 'low';
for (const k of ['top_p', 'presence_penalty', 'frequency_penalty']) delete body[k];
} else {
if (req.maxTokens) body.max_tokens = req.maxTokens;
ddiff --git a/test/openai-compat.test.js b/test/openai-compat.test.js
iindex 25f9315..5e34127 100644
---- a/test/openai-compat.test.js
++++ b/test/openai-compat.test.js
@@@ -3,7 +3,7 @@
// from a run of ai.complete — text, tool calls and a tool-result round trip, emulated streaming as chunks then [DONE], an
// app token whose run is its project's, OpenAI-shaped errors (no token, bad body), GET /v1/models; and the OpenAI-format
// adapter's complete() passes tools, tool_choice and the tool messages through as written, mapping max_tokens to
--// max_completion_tokens (and dropping sampling knobs) for a reasoning model.
++// max_completion_tokens with room to think and low effort (and dropping sampling knobs) for a reasoning model.
const assert = require('assert');
const http = require('http');
const { boot, request, token, suite, ALL } = require('./helpers');
@@@ -135,7 +135,7 @@ t.test('the OpenAI-format adapter passes the request through as written', async
assert.deepStrictEqual(seen[0].body, { model: 'gpt-4.1-mini', messages, tools: [WEATHER], tool_choice: 'required', parallel_tool_calls: false, top_p: 0.9, max_tokens: 300, temperature: 0.2 });
assert.deepStrictEqual([out.model, out.usage.input, out.usage.output, out.completion.choices[0].finish_reason], ['up-1', 12, 7, 'tool_calls']);
await p.complete({ model: 'gpt-5-nano', maxTokens: 300, temperature: 0.2, openai: { messages, top_p: 0.9 } });
-- assert.deepStrictEqual(seen[1].body, { model: 'gpt-5-nano', messages, max_completion_tokens: 300 }, 'a reasoning model: max_completion_tokens, no sampling knobs');
++ assert.deepStrictEqual(seen[1].body, { model: 'gpt-5-nano', messages, max_completion_tokens: 300 + 1024, reasoning_effort: 'low' }, 'a reasoning model: max_completion_tokens with room to think, low effort, no sampling knobs');
const plain = createOpenAiProvider({ key: 'local', capabilities: ['chat'] }, { fetchImpl });
assert.strictEqual(plain.supports('tools'), false, 'tools are declared, never assumed');
});