openvibers/openvibe.actor Commits

Actor: DeepSeek Flash for the check tier, and main's tool test green again (#192)

OP OpenVibers committed 14 days ago · 61e49a387b · 3 files, +35 −12

- The owner prefers DeepSeek V4.1 Flash to Pro (10-30). Flash leads DeepSeek's preference list, and the "small model"
  filter no longer counts it as small: that filter kept the check tier from ever choosing it. Without Flash on offer,
  Pro still leads.
- tool-modules-shape.test.js (#190) was written before the core (#191) landed. It asserted that index.js did not exist
  and checked every file in the folder, including `_` helpers and the ported runtime tools. Together they turned main
  red. It now checks the live-data modules, skips index.js and `_` files the way the registry does, and confirms the
  registry loads them.

Changes

ddiff --git a/server/runtime/models.js b/server/runtime/models.js
iindex 6bee521..e52e200 100644
---- a/server/runtime/models.js
++++ b/server/runtime/models.js
@@@ -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'];
ddiff --git a/test/runtime-models.test.js b/test/runtime-models.test.js
iindex c998eeb..9cea26f 100644
---- a/test/runtime-models.test.js
++++ b/test/runtime-models.test.js
@@@ -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(', '));
});
ddiff --git a/test/tool-modules-shape.test.js b/test/tool-modules-shape.test.js
iindex 5d3b501..791dd21 100644
---- a/test/tool-modules-shape.test.js
++++ b/test/tool-modules-shape.test.js
@@@ -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();