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)
Changes
@@@ -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.fetcif (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;@@@ -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', asyncassert.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');});
@@ -130,7 +130,8 @@ function createOpenAiProvider(record, { apiKey = '', fetchImpl = globalThis.fetc
@@ -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
* 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
* 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 }
* → { completion, text, model, usage }
*/
*/
async function complete(req) {
async function complete(req) {
@@ -140,7 +141,11 @@ function createOpenAiProvider(record, { apiKey = '', fetchImpl = globalThis.fetc
@@ -140,7 +141,11 @@ function createOpenAiProvider(record, { apiKey = '', fetchImpl = globalThis.fetc
if (o[k] !== undefined && o[k] !== null) body[k] = o[k];
if (o[k] !== undefined && o[k] !== null) body[k] = o[k];
}
}
if (isReasoningModel(req.model)) {
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];
for (const k of ['top_p', 'presence_penalty', 'frequency_penalty']) delete body[k];
} else {
} else {
if (req.maxTokens) body.max_tokens = req.maxTokens;
if (req.maxTokens) body.max_tokens = req.maxTokens;
@@ -3,7 +3,7 @@
@@ -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
// 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
// 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
// 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 assert = require('assert');
const http = require('http');
const http = require('http');
const { boot, request, token, suite, ALL } = require('./helpers');
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
@@ -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(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']);
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 } });
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 });
const plain = createOpenAiProvider({ key: 'local', capabilities: ['chat'] }, { fetchImpl });
assert.strictEqual(plain.supports('tools'), false, 'tools are declared, never assumed');
assert.strictEqual(plain.supports('tools'), false, 'tools are declared, never assumed');
});
});