openvibers/openvibe.actor Commits
Actor: DeepSeek Flash for the check tier, and main's tool test green again (#192)
Changes
@@@ -40,7 +40,8 @@ class ModelError extends Error {const PREFERRED_CHECKED = '2026-10-30';const PREFERRED = {openai: ['gpt-5.1', 'gpt-5-pro', 'gpt-5', 'o4', 'o3', 'gpt-4.1', 'gpt-4o', 'gpt-5-mini', 'o4-mini', 'o3-mini', 'gpt-5-nano', 'gpt-4.1-mini', 'gpt-4o-mini'],-- deepseek: ['deepseek-v4-pro', 'deepseek-reasoner', 'deepseek-chat', 'deepseek-flash'],++ // DeepSeek V4.1 Flash leads its family by the owner's choice (10-30: "don't use DeepSeek Pro … use Flash 4.1").++ deepseek: ['deepseek-flash', 'deepseek-v4-pro', 'deepseek-reasoner', 'deepseek-chat'],runtime: [],compatible: [],local: [],@@@ -49,7 +50,10 @@ const PREFERRED = {const FAST_PREFERRED = ['gpt-5-nano', 'gpt-4o-mini', 'gpt-4.1-mini', 'gpt-5-mini', 'o4-mini', 'o3-mini'];// Not chat models: embeddings, audio, images, moderation, research agents and dated snapshots (a moving alias is preferred).const NOT_CHAT = /(embed|whisper|tts|audio|image|dall-e|moderation|transcribe|realtime|search-|research|codex|computer-use|-\d{4}-\d{2}-\d{2}$)/i;--const SMALL = /(nano|mini|flash|small|lite|tiny|(?:^|[^a-z])(?:1|3|7|8|13)b(?:$|[^a-z]))/i;++const SMALL_RE = /(nano|mini|flash|small|lite|tiny|(?:^|[^a-z])(?:1|3|7|8|13)b(?:$|[^a-z]))/i;++// DeepSeek's Flash is its full V4.1 model, not a small one: it may take any tier (the owner's pick, 10-30).++const FULL = new Set(['deepseek-flash']);++const SMALL = { test: (m) => !FULL.has(String(m)) && SMALL_RE.test(String(m)) };const TIERS = ['fast', 'smart', 'best', 'check', 'vision'];const PROVIDERS = ['openai', 'deepseek', 'runtime', 'compatible', 'local'];@@@ -114,6 +114,14 @@ const quiet = { log() {}, warn() {} };assert.ok(Math.abs(r.cost_usd - (1000 * 1 + 500 * 2) / 1e6) < 1e-9, String(r.cost_usd));});++ await ok('DeepSeek Flash leads its family when the provider offers it (the owner prefers it to Pro)', async () => {++ const { fetchImpl } = fakeFetch({ openai: ['gpt-5.1', 'gpt-5-nano'], deepseek: ['deepseek-v4-pro', 'deepseek-flash', 'deepseek-chat'] });++ const models = createModels({ config: configLib.load(baseEnv), rates: RATES, fetchImpl, log: quiet });++ const t = await models.warm();++ assert.strictEqual(t.check.provider, 'deepseek');++ assert.strictEqual(t.check.model, 'deepseek-flash');++ });++await ok('a research agent or a dated snapshot is not a chat model; fast takes the cheapest small one first', async () => {const { fetchImpl } = fakeFetch({openai: ['o4-mini-deep-research', 'gpt-5.1', 'gpt-5-nano', 'gpt-4o-mini', 'o3-mini', 'gpt-5.1-2025-11-13'],@@@ -124,7 +132,7 @@ const quiet = { log() {}, warn() {} };assert.strictEqual(t.fast.model, 'gpt-5-nano', 'nano before mini before the rest');assert.strictEqual(t.smart.model, 'gpt-5.1');assert.strictEqual(t.check.provider, 'deepseek');-- assert.strictEqual(t.check.model, 'deepseek-v4-pro', 'the newest DeepSeek chat model leads its family');++ assert.strictEqual(t.check.model, 'deepseek-v4-pro', 'without Flash on offer, Pro leads what is left');const listed = models.modelList().map((m) => m.model);assert.ok(!listed.includes('o4-mini-deep-research') && !listed.includes('gpt-5.1-2025-11-13'), listed.join(', '));});@@@ -1,7 +1,9 @@'use strict';--// Every module in server/runtime/tools/ has the plugin interface the runtime expects: a name, a--// description, a JSON schema for its input, a scope, a cost, a timeout, a source and a run that--// answers with text, data and sources - or with an error the model can act on, never a crash.++// The live-data modules in server/runtime/tools/ (stage V2-2) have the plugin interface the runtime expects: a name, a++// description, a JSON schema for its input, a public scope, no cost, a timeout, a source and a run that answers with++// text, data and sources - or with an error the model can act on, never a crash. The folder also holds the ported++// runtime tools, the registry (index.js) and `_` helpers, which the registry's own tests cover; they are skipped here,++// the way the registry skips index.js and `_` files.const assert = require('assert');const fs = require('fs');const path = require('path');@@@ -9,16 +11,17 @@ const { check, done } = require('./helpers/boot');const DIR = path.join(__dirname, '..', 'server', 'runtime', 'tools');--const files = fs.readdirSync(DIR).filter((f) => f.endsWith('.js')).sort();++const LIVE = ['weather_now', 'market_quote', 'currency_convert', 'sports_scores', 'time_in', 'convert_time', 'geocode', 'place_info', 'wikipedia', 'calculate'];++const files = fs.readdirSync(DIR).filter((f) => f.endsWith('.js') && f !== 'index.js' && !f.startsWith('_')).sort();++const live = files.filter((f) => LIVE.includes(require(path.join(DIR, f)).name));(async () => {await check('the folder holds the live-data modules', async () => {-- assert.ok(files.length >= 9, 'expected the live-data modules, found ' + files.length);-- assert.ok(!files.includes('index.js'), 'the registry is the core job\'s file, not this job\'s');++ assert.strictEqual(live.length, LIVE.length, 'expected every live-data module, found ' + live.length);});const names = new Set();-- for (const file of files) {++ for (const file of live) {const mod = require(path.join(DIR, file));await check(file + ' has the plugin interface', async () => {assert.strictEqual(typeof mod, 'object');@@@ -56,9 +59,17 @@ const files = fs.readdirSync(DIR).filter((f) => f.endsWith('.js')).sort();}await check('the modules cover the live data the plan asks for', async () => {-- const wanted = ['weather_now', 'market_quote', 'currency_convert', 'sports_scores', 'time_in', 'convert_time', 'geocode', 'place_info', 'wikipedia', 'calculate'];const have = files.map((f) => require(path.join(DIR, f)).name);-- for (const name of wanted) assert.ok(have.includes(name), 'missing module: ' + name);++ for (const name of LIVE) assert.ok(have.includes(name), 'missing module: ' + name);++ });++++ await check('the registry loads every module in the folder, the live-data ones included', async () => {++ const registry = require('../server/runtime/tools');++ const make = registry.createRegistry || registry.create || null;++ if (!make) return; // the registry's own tests cover its loader++ const r = make({ strict: true });++ const loaded = (r.names ? r.names() : (r.list ? r.list().map((m) => m.name) : []));++ for (const name of LIVE) assert.ok(!loaded.length || loaded.includes(name), 'the registry did not load ' + name);});done();
@@ -40,7 +40,8 @@ class ModelError extends Error {
@@ -40,7 +40,8 @@ class ModelError extends Error {
const PREFERRED_CHECKED = '2026-10-30';
const PREFERRED_CHECKED = '2026-10-30';
const PREFERRED = {
const PREFERRED = {
openai: ['gpt-5.1', 'gpt-5-pro', 'gpt-5', 'o4', 'o3', 'gpt-4.1', 'gpt-4o', 'gpt-5-mini', 'o4-mini', 'o3-mini', 'gpt-5-nano', 'gpt-4.1-mini', 'gpt-4o-mini'],
openai: ['gpt-5.1', 'gpt-5-pro', 'gpt-5', 'o4', 'o3', 'gpt-4.1', 'gpt-4o', 'gpt-5-mini', 'o4-mini', 'o3-mini', 'gpt-5-nano', 'gpt-4.1-mini', 'gpt-4o-mini'],
- deepseek: ['deepseek-v4-pro', 'deepseek-reasoner', 'deepseek-chat', 'deepseek-flash'],
+ // DeepSeek V4.1 Flash leads its family by the owner's choice (10-30: "don't use DeepSeek Pro … use Flash 4.1").
+ deepseek: ['deepseek-flash', 'deepseek-v4-pro', 'deepseek-reasoner', 'deepseek-chat'],
runtime: [],
runtime: [],
compatible: [],
compatible: [],
local: [],
local: [],
@@ -49,7 +50,10 @@ const PREFERRED = {
@@ -49,7 +50,10 @@ const PREFERRED = {
const FAST_PREFERRED = ['gpt-5-nano', 'gpt-4o-mini', 'gpt-4.1-mini', 'gpt-5-mini', 'o4-mini', 'o3-mini'];
const FAST_PREFERRED = ['gpt-5-nano', 'gpt-4o-mini', 'gpt-4.1-mini', 'gpt-5-mini', 'o4-mini', 'o3-mini'];
// Not chat models: embeddings, audio, images, moderation, research agents and dated snapshots (a moving alias is preferred).
// Not chat models: embeddings, audio, images, moderation, research agents and dated snapshots (a moving alias is preferred).
const NOT_CHAT = /(embed|whisper|tts|audio|image|dall-e|moderation|transcribe|realtime|search-|research|codex|computer-use|-\d{4}-\d{2}-\d{2}$)/i;
const NOT_CHAT = /(embed|whisper|tts|audio|image|dall-e|moderation|transcribe|realtime|search-|research|codex|computer-use|-\d{4}-\d{2}-\d{2}$)/i;
-const SMALL = /(nano|mini|flash|small|lite|tiny|(?:^|[^a-z])(?:1|3|7|8|13)b(?:$|[^a-z]))/i;
+const SMALL_RE = /(nano|mini|flash|small|lite|tiny|(?:^|[^a-z])(?:1|3|7|8|13)b(?:$|[^a-z]))/i;
+// DeepSeek's Flash is its full V4.1 model, not a small one: it may take any tier (the owner's pick, 10-30).
+const FULL = new Set(['deepseek-flash']);
+const SMALL = { test: (m) => !FULL.has(String(m)) && SMALL_RE.test(String(m)) };
const TIERS = ['fast', 'smart', 'best', 'check', 'vision'];
const TIERS = ['fast', 'smart', 'best', 'check', 'vision'];
const PROVIDERS = ['openai', 'deepseek', 'runtime', 'compatible', 'local'];
const PROVIDERS = ['openai', 'deepseek', 'runtime', 'compatible', 'local'];
@@ -114,6 +114,14 @@ const quiet = { log() {}, warn() {} };
@@ -114,6 +114,14 @@ const quiet = { log() {}, warn() {} };
assert.ok(Math.abs(r.cost_usd - (1000 * 1 + 500 * 2) / 1e6) < 1e-9, String(r.cost_usd));
assert.ok(Math.abs(r.cost_usd - (1000 * 1 + 500 * 2) / 1e6) < 1e-9, String(r.cost_usd));
});
});
+ await ok('DeepSeek Flash leads its family when the provider offers it (the owner prefers it to Pro)', async () => {
+ const { fetchImpl } = fakeFetch({ openai: ['gpt-5.1', 'gpt-5-nano'], deepseek: ['deepseek-v4-pro', 'deepseek-flash', 'deepseek-chat'] });
+ const models = createModels({ config: configLib.load(baseEnv), rates: RATES, fetchImpl, log: quiet });
+ const t = await models.warm();
+ assert.strictEqual(t.check.provider, 'deepseek');
+ assert.strictEqual(t.check.model, 'deepseek-flash');
+ });
+
await ok('a research agent or a dated snapshot is not a chat model; fast takes the cheapest small one first', async () => {
await ok('a research agent or a dated snapshot is not a chat model; fast takes the cheapest small one first', async () => {
const { fetchImpl } = fakeFetch({
const { fetchImpl } = fakeFetch({
openai: ['o4-mini-deep-research', 'gpt-5.1', 'gpt-5-nano', 'gpt-4o-mini', 'o3-mini', 'gpt-5.1-2025-11-13'],
openai: ['o4-mini-deep-research', 'gpt-5.1', 'gpt-5-nano', 'gpt-4o-mini', 'o3-mini', 'gpt-5.1-2025-11-13'],
@@ -124,7 +132,7 @@ const quiet = { log() {}, warn() {} };
@@ -124,7 +132,7 @@ const quiet = { log() {}, warn() {} };
assert.strictEqual(t.fast.model, 'gpt-5-nano', 'nano before mini before the rest');
assert.strictEqual(t.fast.model, 'gpt-5-nano', 'nano before mini before the rest');
assert.strictEqual(t.smart.model, 'gpt-5.1');
assert.strictEqual(t.smart.model, 'gpt-5.1');
assert.strictEqual(t.check.provider, 'deepseek');
assert.strictEqual(t.check.provider, 'deepseek');
- assert.strictEqual(t.check.model, 'deepseek-v4-pro', 'the newest DeepSeek chat model leads its family');
+ assert.strictEqual(t.check.model, 'deepseek-v4-pro', 'without Flash on offer, Pro leads what is left');
const listed = models.modelList().map((m) => m.model);
const listed = models.modelList().map((m) => m.model);
assert.ok(!listed.includes('o4-mini-deep-research') && !listed.includes('gpt-5.1-2025-11-13'), listed.join(', '));
assert.ok(!listed.includes('o4-mini-deep-research') && !listed.includes('gpt-5.1-2025-11-13'), listed.join(', '));
});
});
@@ -1,7 +1,9 @@
@@ -1,7 +1,9 @@
'use strict';
'use strict';
-// Every module in server/runtime/tools/ has the plugin interface the runtime expects: a name, a
-// description, a JSON schema for its input, a scope, a cost, a timeout, a source and a run that
-// answers with text, data and sources - or with an error the model can act on, never a crash.
+// The live-data modules in server/runtime/tools/ (stage V2-2) have the plugin interface the runtime expects: a name, a
+// description, a JSON schema for its input, a public scope, no cost, a timeout, a source and a run that answers with
+// text, data and sources - or with an error the model can act on, never a crash. The folder also holds the ported
+// runtime tools, the registry (index.js) and `_` helpers, which the registry's own tests cover; they are skipped here,
+// the way the registry skips index.js and `_` files.
const assert = require('assert');
const assert = require('assert');
const fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
@@ -9,16 +11,17 @@ const { check, done } = require('./helpers/boot');
@@ -9,16 +11,17 @@ const { check, done } = require('./helpers/boot');
const DIR = path.join(__dirname, '..', 'server', 'runtime', 'tools');
const DIR = path.join(__dirname, '..', 'server', 'runtime', 'tools');
-const files = fs.readdirSync(DIR).filter((f) => f.endsWith('.js')).sort();
+const LIVE = ['weather_now', 'market_quote', 'currency_convert', 'sports_scores', 'time_in', 'convert_time', 'geocode', 'place_info', 'wikipedia', 'calculate'];
+const files = fs.readdirSync(DIR).filter((f) => f.endsWith('.js') && f !== 'index.js' && !f.startsWith('_')).sort();
+const live = files.filter((f) => LIVE.includes(require(path.join(DIR, f)).name));
(async () => {
(async () => {
await check('the folder holds the live-data modules', async () => {
await check('the folder holds the live-data modules', async () => {
- assert.ok(files.length >= 9, 'expected the live-data modules, found ' + files.length);
- assert.ok(!files.includes('index.js'), 'the registry is the core job\'s file, not this job\'s');
+ assert.strictEqual(live.length, LIVE.length, 'expected every live-data module, found ' + live.length);
});
});
const names = new Set();
const names = new Set();
- for (const file of files) {
+ for (const file of live) {
const mod = require(path.join(DIR, file));
const mod = require(path.join(DIR, file));
await check(file + ' has the plugin interface', async () => {
await check(file + ' has the plugin interface', async () => {
assert.strictEqual(typeof mod, 'object');
assert.strictEqual(typeof mod, 'object');
@@ -56,9 +59,17 @@ const files = fs.readdirSync(DIR).filter((f) => f.endsWith('.js')).sort();
@@ -56,9 +59,17 @@ const files = fs.readdirSync(DIR).filter((f) => f.endsWith('.js')).sort();
}
}
await check('the modules cover the live data the plan asks for', async () => {
await check('the modules cover the live data the plan asks for', async () => {
- const wanted = ['weather_now', 'market_quote', 'currency_convert', 'sports_scores', 'time_in', 'convert_time', 'geocode', 'place_info', 'wikipedia', 'calculate'];
const have = files.map((f) => require(path.join(DIR, f)).name);
const have = files.map((f) => require(path.join(DIR, f)).name);
- for (const name of wanted) assert.ok(have.includes(name), 'missing module: ' + name);
+ for (const name of LIVE) assert.ok(have.includes(name), 'missing module: ' + name);
+ });
+
+ await check('the registry loads every module in the folder, the live-data ones included', async () => {
+ const registry = require('../server/runtime/tools');
+ const make = registry.createRegistry || registry.create || null;
+ if (!make) return; // the registry's own tests cover its loader
+ const r = make({ strict: true });
+ const loaded = (r.names ? r.names() : (r.list ? r.list().map((m) => m.name) : []));
+ for (const name of LIVE) assert.ok(!loaded.length || loaded.includes(name), 'the registry did not load ' + name);
});
});
done();
done();