openvibers/openvibe.ai Commits
Keep JSON off the local provider and retire stale seeded local…
Changes
@@@ -53,8 +53,8 @@ function providerRecords(config, env) {];if (config.localLlm && config.localLlm.url) {// A local model server (llama.cpp, Ollama) speaking the OpenAI API: no key, never paid (WS-O task 5).-- // JSON goes through the adapter's json_schema -> json_object -> prose step-down; embed only with an embedding model.-- const localCaps = ['chat', 'generate', 'summarize', 'classify', 'extract', 'json'];++ // No json: the small local model must not be offered the product JSON routes. Embed only with an embedding model.++ const localCaps = ['chat', 'generate', 'summarize', 'classify', 'extract'];if (config.localLlm.embedModel) localCaps.push('embed');out.push({ key: 'local', display_name: 'Local model (OpenAI-compatible server on this host)', kind: 'openai', status: 'active', base_url: config.localLlm.url, auth_mode: 'none', secret_ref: null, default_model: config.localLlm.model || 'local', capabilities: localCaps, timeout_ms: config.localLlm.timeoutMs, priority: 5, metadata: { configured_by: 'AI_LOCAL_LLM_URL', local: true, paid: false, billing_profile: 'free' } });}@@@ -84,7 +84,13 @@ async function seed({ registry, quotas, config, env = process.env, db }) {if (!await registry.getModel('shared', embeddingModel)) await registry.upsertModel({ provider_key: 'shared', model_key: embeddingModel, type: 'embedding' }, { actor: 'seed' });// The local embedding model joins default.embedding as its free member (vectors differ per upstream).const localEmbed = config.localLlm && config.localLlm.url && config.localLlm.embedModel;-- if (localEmbed && !await registry.getModel('local', localEmbed)) await registry.upsertModel({ provider_key: 'local', model_key: localEmbed, type: 'embedding' }, { actor: 'seed' });++ if (localEmbed && !await registry.getModel('local', localEmbed)) await registry.upsertModel({ provider_key: 'local', model_key: localEmbed, type: 'embedding', metadata: { seeded: true } }, { actor: 'seed' });++ // Never mix vectors: a seeded local embedding model that is no longer the configured one is retired.++ if (await registry.getProvider('local')) {++ for (const m of await registry.listModels({ provider: 'local' })) {++ if (m.type === 'embedding' && m.status === 'active' && m.metadata && m.metadata.seeded && m.model_key !== localEmbed) await registry.upsertModel({ provider_key: 'local', model_key: m.model_key, status: 'disabled' }, { actor: 'seed' });++ }++ }// Routes. A capability pool covers every provider that can serve the capability, ordered by// openvibe-sdk/placement (brief §4/§5): no static primary/fallbacks and no AI_MODEL_<ROLE> pins.@@@ -114,14 +114,14 @@ t.test('templates render values, JSON and sections; loose JSON repair works', ()assert.deepStrictEqual(parseJsonLoose('{“a”: 1,}'), { a: 1 });});--t.test('local provider: json always, embed only with AI_LOCAL_EMBED_MODEL', () => {++t.test('local provider: never json, embed only with AI_LOCAL_EMBED_MODEL', () => {const caps = (env) => {const p = providerRecords(load(env), env).find(x => x.key === 'local');return p && p.capabilities;};assert.strictEqual(caps({}), undefined, 'no local provider without AI_LOCAL_LLM_URL');const plain = caps({ AI_LOCAL_LLM_URL: 'http://127.0.0.1:8090/v1' });-- assert.deepStrictEqual(plain, ['chat', 'generate', 'summarize', 'classify', 'extract', 'json']);++ assert.deepStrictEqual(plain, ['chat', 'generate', 'summarize', 'classify', 'extract']);const withEmbed = caps({ AI_LOCAL_LLM_URL: 'http://127.0.0.1:8090/v1', AI_LOCAL_EMBED_MODEL: ' nomic-embed-text ' });assert.deepStrictEqual(withEmbed, [...plain, 'embed']);assert.strictEqual(load({ AI_LOCAL_EMBED_MODEL: ' nomic-embed-text ' }).localLlm.embedModel, 'nomic-embed-text');
@@ -53,8 +53,8 @@ function providerRecords(config, env) {
@@ -53,8 +53,8 @@ function providerRecords(config, env) {
];
];
if (config.localLlm && config.localLlm.url) {
if (config.localLlm && config.localLlm.url) {
// A local model server (llama.cpp, Ollama) speaking the OpenAI API: no key, never paid (WS-O task 5).
// A local model server (llama.cpp, Ollama) speaking the OpenAI API: no key, never paid (WS-O task 5).
- // JSON goes through the adapter's json_schema -> json_object -> prose step-down; embed only with an embedding model.
- const localCaps = ['chat', 'generate', 'summarize', 'classify', 'extract', 'json'];
+ // No json: the small local model must not be offered the product JSON routes. Embed only with an embedding model.
+ const localCaps = ['chat', 'generate', 'summarize', 'classify', 'extract'];
if (config.localLlm.embedModel) localCaps.push('embed');
if (config.localLlm.embedModel) localCaps.push('embed');
out.push({ key: 'local', display_name: 'Local model (OpenAI-compatible server on this host)', kind: 'openai', status: 'active', base_url: config.localLlm.url, auth_mode: 'none', secret_ref: null, default_model: config.localLlm.model || 'local', capabilities: localCaps, timeout_ms: config.localLlm.timeoutMs, priority: 5, metadata: { configured_by: 'AI_LOCAL_LLM_URL', local: true, paid: false, billing_profile: 'free' } });
out.push({ key: 'local', display_name: 'Local model (OpenAI-compatible server on this host)', kind: 'openai', status: 'active', base_url: config.localLlm.url, auth_mode: 'none', secret_ref: null, default_model: config.localLlm.model || 'local', capabilities: localCaps, timeout_ms: config.localLlm.timeoutMs, priority: 5, metadata: { configured_by: 'AI_LOCAL_LLM_URL', local: true, paid: false, billing_profile: 'free' } });
}
}
@@ -84,7 +84,13 @@ async function seed({ registry, quotas, config, env = process.env, db }) {
@@ -84,7 +84,13 @@ async function seed({ registry, quotas, config, env = process.env, db }) {
if (!await registry.getModel('shared', embeddingModel)) await registry.upsertModel({ provider_key: 'shared', model_key: embeddingModel, type: 'embedding' }, { actor: 'seed' });
if (!await registry.getModel('shared', embeddingModel)) await registry.upsertModel({ provider_key: 'shared', model_key: embeddingModel, type: 'embedding' }, { actor: 'seed' });
// The local embedding model joins default.embedding as its free member (vectors differ per upstream).
// The local embedding model joins default.embedding as its free member (vectors differ per upstream).
const localEmbed = config.localLlm && config.localLlm.url && config.localLlm.embedModel;
const localEmbed = config.localLlm && config.localLlm.url && config.localLlm.embedModel;
- if (localEmbed && !await registry.getModel('local', localEmbed)) await registry.upsertModel({ provider_key: 'local', model_key: localEmbed, type: 'embedding' }, { actor: 'seed' });
+ if (localEmbed && !await registry.getModel('local', localEmbed)) await registry.upsertModel({ provider_key: 'local', model_key: localEmbed, type: 'embedding', metadata: { seeded: true } }, { actor: 'seed' });
+ // Never mix vectors: a seeded local embedding model that is no longer the configured one is retired.
+ if (await registry.getProvider('local')) {
+ for (const m of await registry.listModels({ provider: 'local' })) {
+ if (m.type === 'embedding' && m.status === 'active' && m.metadata && m.metadata.seeded && m.model_key !== localEmbed) await registry.upsertModel({ provider_key: 'local', model_key: m.model_key, status: 'disabled' }, { actor: 'seed' });
+ }
+ }
// Routes. A capability pool covers every provider that can serve the capability, ordered by
// Routes. A capability pool covers every provider that can serve the capability, ordered by
// openvibe-sdk/placement (brief §4/§5): no static primary/fallbacks and no AI_MODEL_<ROLE> pins.
// openvibe-sdk/placement (brief §4/§5): no static primary/fallbacks and no AI_MODEL_<ROLE> pins.
@@ -114,14 +114,14 @@ t.test('templates render values, JSON and sections; loose JSON repair works', ()
@@ -114,14 +114,14 @@ t.test('templates render values, JSON and sections; loose JSON repair works', ()
assert.deepStrictEqual(parseJsonLoose('{“a”: 1,}'), { a: 1 });
assert.deepStrictEqual(parseJsonLoose('{“a”: 1,}'), { a: 1 });
});
});
-t.test('local provider: json always, embed only with AI_LOCAL_EMBED_MODEL', () => {
+t.test('local provider: never json, embed only with AI_LOCAL_EMBED_MODEL', () => {
const caps = (env) => {
const caps = (env) => {
const p = providerRecords(load(env), env).find(x => x.key === 'local');
const p = providerRecords(load(env), env).find(x => x.key === 'local');
return p && p.capabilities;
return p && p.capabilities;
};
};
assert.strictEqual(caps({}), undefined, 'no local provider without AI_LOCAL_LLM_URL');
assert.strictEqual(caps({}), undefined, 'no local provider without AI_LOCAL_LLM_URL');
const plain = caps({ AI_LOCAL_LLM_URL: 'http://127.0.0.1:8090/v1' });
const plain = caps({ AI_LOCAL_LLM_URL: 'http://127.0.0.1:8090/v1' });
- assert.deepStrictEqual(plain, ['chat', 'generate', 'summarize', 'classify', 'extract', 'json']);
+ assert.deepStrictEqual(plain, ['chat', 'generate', 'summarize', 'classify', 'extract']);
const withEmbed = caps({ AI_LOCAL_LLM_URL: 'http://127.0.0.1:8090/v1', AI_LOCAL_EMBED_MODEL: ' nomic-embed-text ' });
const withEmbed = caps({ AI_LOCAL_LLM_URL: 'http://127.0.0.1:8090/v1', AI_LOCAL_EMBED_MODEL: ' nomic-embed-text ' });
assert.deepStrictEqual(withEmbed, [...plain, 'embed']);
assert.deepStrictEqual(withEmbed, [...plain, 'embed']);
assert.strictEqual(load({ AI_LOCAL_EMBED_MODEL: ' nomic-embed-text ' }).localLlm.embedModel, 'nomic-embed-text');
assert.strictEqual(load({ AI_LOCAL_EMBED_MODEL: ' nomic-embed-text ' }).localLlm.embedModel, 'nomic-embed-text');