openvibers/openvibe.ai Commits
The advisor honours the model named on the run, and an app can read its own receipt (#429)
Changes
@@@ -150,7 +150,9 @@ function exchangeRouter({ db, registry, pool, quotas = null, env = process.env,const ob = p.onBehalfOf || (p.claims && p.claims.on_behalf_of);const obId = ob && typeof ob === 'object' ? ob.id : ob;const owner = receipt.owner || {};++ const requester = receipt.requester || {};const allowed = auth.principalHas(req, CAPS.usageRead)++ || (requester.type === subject.type && requester.id === subject.id)|| (receipt.requester_type === subject.type && receipt.requester_id === subject.id)|| (owner.type === 'user' && obId && String(owner.id) === String(obId));if (!allowed) throw new AiError(404, 'ai.receipt_not_found', `no receipt ${req.params.id}`);@@@ -1232,7 +1232,8 @@ function createRuns({ choices = null, db, registry, engine, cache, quotas, conficonst freeOnly = appRun && (principal.env === 'sandbox' || !(tiers && tiers.enabled));const ctx = { requesterType: row.requester_type, requesterId: row.requester_id, ...(person ? { onBehalfOf: person } : {}), ...(opts.policy ? { contentPolicy: opts.policy } : {}), ...(appRun ? { appRun: true, freeOnly } : {}) };const offers = await pool.offers(route, 'chat', ctx);-- const asked = body.model == null ? null : String(body.model).trim();++ // The model may be named on the advice call or once on the run's advisor policy; the call wins.++ const asked = body.model == null ? (policy.model || null) : String(body.model).trim();if (asked != null && !/^[A-Za-z0-9][A-Za-z0-9._:-]*\/[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,199}$/.test(asked)) throw new AiError(422, 'input.invalid', 'model must be provider/model from GET /api/v1/routes');let chosen = null;let why = null;@@@ -38,10 +38,10 @@ t.test('one advice call makes a run, cuts the advice to the cap, and lands on thconst h = await boot();const tok = token('live', ALL);try {-- const run = await startRun(h, tok, { advisor: { max_uses: 3, max_words: 40, model: 'stub/stub-1', effort: 'low' }, effort: 'low' });++ const run = await startRun(h, tok, { advisor: { max_uses: 3, max_words: 40, effort: 'low' }, effort: 'low' });const row = await h.db.prepare('SELECT options FROM runs WHERE id = ?').get(run.id);const policy = JSON.parse(row.options).advisor_policy;-- assert.deepStrictEqual({ enabled: policy.enabled, max_uses: policy.max_uses, max_words: policy.max_words, model: policy.model }, { enabled: true, max_uses: 3, max_words: 40, model: 'stub/stub-1' });++ assert.deepStrictEqual({ enabled: policy.enabled, max_uses: policy.max_uses, max_words: policy.max_words }, { enabled: true, max_uses: 3, max_words: 40 });const res = await request(h.base, 'POST', `/api/v1/runs/${run.id}/advice`, { tok, body: { transcript: [{ role: 'system', content: 'You are a coding agent.' }, ...messages, { role: 'tool', content: 'exit 1: expected 2' }], question: 'What now?' } });assert.strictEqual(res.status, 201, res.text);assert.ok(res.body.advice.length > 0);
@@ -150,7 +150,9 @@ function exchangeRouter({ db, registry, pool, quotas = null, env = process.env,
@@ -150,7 +150,9 @@ function exchangeRouter({ db, registry, pool, quotas = null, env = process.env,
const ob = p.onBehalfOf || (p.claims && p.claims.on_behalf_of);
const ob = p.onBehalfOf || (p.claims && p.claims.on_behalf_of);
const obId = ob && typeof ob === 'object' ? ob.id : ob;
const obId = ob && typeof ob === 'object' ? ob.id : ob;
const owner = receipt.owner || {};
const owner = receipt.owner || {};
+ const requester = receipt.requester || {};
const allowed = auth.principalHas(req, CAPS.usageRead)
const allowed = auth.principalHas(req, CAPS.usageRead)
+ || (requester.type === subject.type && requester.id === subject.id)
|| (receipt.requester_type === subject.type && receipt.requester_id === subject.id)
|| (receipt.requester_type === subject.type && receipt.requester_id === subject.id)
|| (owner.type === 'user' && obId && String(owner.id) === String(obId));
|| (owner.type === 'user' && obId && String(owner.id) === String(obId));
if (!allowed) throw new AiError(404, 'ai.receipt_not_found', `no receipt ${req.params.id}`);
if (!allowed) throw new AiError(404, 'ai.receipt_not_found', `no receipt ${req.params.id}`);
@@ -1232,7 +1232,8 @@ function createRuns({ choices = null, db, registry, engine, cache, quotas, confi
@@ -1232,7 +1232,8 @@ function createRuns({ choices = null, db, registry, engine, cache, quotas, confi
const freeOnly = appRun && (principal.env === 'sandbox' || !(tiers && tiers.enabled));
const freeOnly = appRun && (principal.env === 'sandbox' || !(tiers && tiers.enabled));
const ctx = { requesterType: row.requester_type, requesterId: row.requester_id, ...(person ? { onBehalfOf: person } : {}), ...(opts.policy ? { contentPolicy: opts.policy } : {}), ...(appRun ? { appRun: true, freeOnly } : {}) };
const ctx = { requesterType: row.requester_type, requesterId: row.requester_id, ...(person ? { onBehalfOf: person } : {}), ...(opts.policy ? { contentPolicy: opts.policy } : {}), ...(appRun ? { appRun: true, freeOnly } : {}) };
const offers = await pool.offers(route, 'chat', ctx);
const offers = await pool.offers(route, 'chat', ctx);
- const asked = body.model == null ? null : String(body.model).trim();
+ // The model may be named on the advice call or once on the run's advisor policy; the call wins.
+ const asked = body.model == null ? (policy.model || null) : String(body.model).trim();
if (asked != null && !/^[A-Za-z0-9][A-Za-z0-9._:-]*\/[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,199}$/.test(asked)) throw new AiError(422, 'input.invalid', 'model must be provider/model from GET /api/v1/routes');
if (asked != null && !/^[A-Za-z0-9][A-Za-z0-9._:-]*\/[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,199}$/.test(asked)) throw new AiError(422, 'input.invalid', 'model must be provider/model from GET /api/v1/routes');
let chosen = null;
let chosen = null;
let why = null;
let why = null;
@@ -38,10 +38,10 @@ t.test('one advice call makes a run, cuts the advice to the cap, and lands on th
@@ -38,10 +38,10 @@ t.test('one advice call makes a run, cuts the advice to the cap, and lands on th
const h = await boot();
const h = await boot();
const tok = token('live', ALL);
const tok = token('live', ALL);
try {
try {
- const run = await startRun(h, tok, { advisor: { max_uses: 3, max_words: 40, model: 'stub/stub-1', effort: 'low' }, effort: 'low' });
+ const run = await startRun(h, tok, { advisor: { max_uses: 3, max_words: 40, effort: 'low' }, effort: 'low' });
const row = await h.db.prepare('SELECT options FROM runs WHERE id = ?').get(run.id);
const row = await h.db.prepare('SELECT options FROM runs WHERE id = ?').get(run.id);
const policy = JSON.parse(row.options).advisor_policy;
const policy = JSON.parse(row.options).advisor_policy;
- assert.deepStrictEqual({ enabled: policy.enabled, max_uses: policy.max_uses, max_words: policy.max_words, model: policy.model }, { enabled: true, max_uses: 3, max_words: 40, model: 'stub/stub-1' });
+ assert.deepStrictEqual({ enabled: policy.enabled, max_uses: policy.max_uses, max_words: policy.max_words }, { enabled: true, max_uses: 3, max_words: 40 });
const res = await request(h.base, 'POST', `/api/v1/runs/${run.id}/advice`, { tok, body: { transcript: [{ role: 'system', content: 'You are a coding agent.' }, ...messages, { role: 'tool', content: 'exit 1: expected 2' }], question: 'What now?' } });
const res = await request(h.base, 'POST', `/api/v1/runs/${run.id}/advice`, { tok, body: { transcript: [{ role: 'system', content: 'You are a coding agent.' }, ...messages, { role: 'tool', content: 'exit 1: expected 2' }], question: 'What now?' } });
assert.strictEqual(res.status, 201, res.text);
assert.strictEqual(res.status, 201, res.text);
assert.ok(res.body.advice.length > 0);
assert.ok(res.body.advice.length > 0);