openvibers/openvibe.codes Commits
Codes: an OpenVibe.AI agent runs the harness loop on AI's OpenAI-compatible endpoint with the app's Network token (#70)
Changes
@@@ -49,6 +49,7 @@ openvibe-codes resume <run id> "now add a test"| Command Code | `cmd --print --output-format json` | `cmd` on PATH || Aider | `aider --message` | `aider` on PATH || DeepSeek | the harness's own agent loop ([harness/adapters/api.js](harness/adapters/api.js)) | `DEEPSEEK_API_KEY` |++| OpenVibe.AI | the same loop against AI's `/v1/chat/completions`; its router picks the model and each answer reports its cost | `OPENVIBE_APP_ID` + `OPENVIBE_APP_SECRET` (an app with `ai.app.run`; tokens are fetched and refreshed), or `OPENVIBE_AI_TOKEN` || Your own model | the same loop against any OpenAI-compatible server | `OPENVIBE_CODES_BASE_URL`, `OPENVIBE_CODES_MODEL`, optional `OPENVIBE_CODES_API_KEY` |- **Events.** Every adapter turns its agent's output into one stream in Claude Code's `stream-json` shape (system/init, assistant text and tool_use, user tool_result, system warning/error/handoff, exactly one result), [harness/events.js](harness/events.js). Keys, tokens and private keys are redacted before an event is printed or kept.@@@ -141,7 +141,10 @@ function runCommand(command, cwd, signal) {}/**-- * apiAgent({ id, name, baseUrl, keyEnv, model, priceIn, priceOut }) → an adapter.++ * apiAgent({ id, name, baseUrl, keyEnv, model, priceIn, priceOut, token }) → an adapter.++ * token(env, fetch) → a bearer, asked before every request (OpenVibe.AI takes a five-minute Network token, refreshed by++ * the function); without it the key is keyEnv's value. When the API reports each answer's cost (OpenVibe.AI's++ * x_openvibe.cost_usd), the run's cost is the sum of those, not a price list.* ctx (from harness/index.js): { env, fetch, sessions } — sessions keeps the conversation for resume.*/function apiAgent(spec) {@@@ -169,15 +172,17 @@ function apiAgent(spec) {messages.push({ role: 'user', content: t.prompt });yield ev.init(sessionId, spec.id, model);const u = { input: 0, output: 0, cacheRead: 0 };++ const reported = { usd: 0, any: false };let lastText = '';let turns = 0;let failure = null;try {while (turns < maxTurns && !cancelled) {turns++;++ const bearer = spec.token ? await spec.token(env, doFetch) : key;const res = await doFetch(`${base}/chat/completions`, {method: 'POST',-- headers: { 'content-type': 'application/json', ...(key ? { authorization: `Bearer ${key}` } : {}) },++ headers: { 'content-type': 'application/json', ...(bearer ? { authorization: `Bearer ${bearer}` } : {}) },body: JSON.stringify({ model, messages, tools, tool_choice: 'auto' }),signal: controller.signal,});@@@ -188,6 +193,7 @@ function apiAgent(spec) {break;}const usage = body.usage || {};++ if (body.x_openvibe && Number.isFinite(+body.x_openvibe.cost_usd)) { reported.usd += +body.x_openvibe.cost_usd; reported.any = true; }u.input += +usage.prompt_tokens || 0; u.output += +usage.completion_tokens || 0;u.cacheRead += +(usage.prompt_cache_hit_tokens || (usage.prompt_tokens_details && usage.prompt_tokens_details.cached_tokens)) || 0;const msg = body.choices[0].message || {};@@@ -216,7 +222,7 @@ function apiAgent(spec) {yield ev.error(failure);}if (ctx.sessions) ctx.sessions.save(sessionId, { agent: spec.id, model, messages: messages.slice(-200) });-- const cost = (u.input * (spec.priceIn || 0) + u.output * (spec.priceOut || 0)) / 1000;++ const cost = reported.any ? reported.usd : (u.input * (spec.priceIn || 0) + u.output * (spec.priceOut || 0)) / 1000;yield ev.result({ ok: !failure && !cancelled, text: failure || lastText, sessionId, model, startedAt, turns, costUsd: cost,usage: ev.usage({ input: u.input - u.cacheRead, output: u.output, cacheRead: u.cacheRead }) });}());@@@ -26,6 +26,27 @@ const { apiAgent } = require('./adapters/api');const ev = require('./events');const { createStore } = require('./sessions');++/**++ * OpenVibe.AI's token: a five-minute Network token for your app (client credentials, audience openvibe.ai), fetched when++ * missing or within 30 s of expiring. OPENVIBE_AI_TOKEN, when set, is used as it is instead.++ */++function networkToken() {++ let cached = null;++ return async (env, doFetch) => {++ if (env.OPENVIBE_AI_TOKEN) return env.OPENVIBE_AI_TOKEN;++ if (cached && cached.until > Date.now()) return cached.token;++ const network = String(env.OPENVIBE_NETWORK_URL || 'https://openvibe.network').replace(/\/+$/, '');++ const res = await doFetch(`${network}/oauth/token`, {++ method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' },++ body: new URLSearchParams({ grant_type: 'client_credentials', client_id: env.OPENVIBE_APP_ID || '', client_secret: env.OPENVIBE_APP_SECRET || '', audience: 'openvibe.ai' }).toString(),++ });++ const body = await res.json().catch(() => null);++ if (!res.ok || !body || !body.access_token) throw new Error(`OpenVibe.Network refused the app's token (HTTP ${res.status}${body && body.error ? `: ${body.error}` : ''})`);++ cached = { token: body.access_token, until: Date.now() + Math.max(30, Number(body.expires_in) || 300) * 1000 - 30000 };++ return cached.token;++ };++}++/** The adapter for each catalog harness (the catalog row's id). */function adaptersFor(env) {return {@@@ -35,6 +56,8 @@ function adaptersFor(env) {'command-code': cli.commandCode,aider: cli.aider,deepseek: apiAgent({ id: 'deepseek', name: 'DeepSeek', baseUrl: env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com', keyEnv: 'DEEPSEEK_API_KEY', model: 'deepseek-chat', priceIn: 0.00027, priceOut: 0.0011 }),++ // OpenVibe.AI's OpenAI-compatible endpoint: its router picks the model, each answer reports its cost.++ 'openvibe-ai': apiAgent({ id: 'openvibe-ai', name: 'OpenVibe.AI', baseUrl: env.OPENVIBE_AI_URL || 'https://ai.openvibe.services/v1', token: networkToken(), model: 'openvibe' }),'openai-compatible': apiAgent({ id: 'openai-compatible', name: 'Your own model', baseUrl: env.OPENVIBE_CODES_BASE_URL || 'http://127.0.0.1:8080/v1', keyEnv: 'OPENVIBE_CODES_API_KEY', model: env.OPENVIBE_CODES_MODEL || 'local' }),};}@@@ -53,6 +76,7 @@ function availability(adapter, env) {const where = onPath(adapter.bin, env);return where ? { available: true, where } : { available: false, reason: `\`${adapter.bin}\` is not on PATH (install ${adapter.name})` };}++ if (adapter.id === 'openvibe-ai') return env.OPENVIBE_AI_TOKEN || (env.OPENVIBE_APP_ID && env.OPENVIBE_APP_SECRET) ? { available: true, where: adapter.baseUrl } : { available: false, reason: 'set OPENVIBE_APP_ID and OPENVIBE_APP_SECRET (an app of your project on openvibe.services with ai.app.run)' };if (adapter.id === 'openai-compatible') return env.OPENVIBE_CODES_BASE_URL ? { available: true, where: env.OPENVIBE_CODES_BASE_URL } : { available: false, reason: 'set OPENVIBE_CODES_BASE_URL (and OPENVIBE_CODES_MODEL) to your server' };return env[adapter.keyEnv] ? { available: true, where: adapter.baseUrl } : { available: false, reason: `set ${adapter.keyEnv}` };}@@@ -301,13 +301,62 @@"concurrency": 8,"price_per_1k_tokens": {"fresh_usd": 0.00027,-- "cached_usd": 0.00007,++ "cached_usd": 7e-05,"output_usd": 0.0011},"availability_windows": []}]},++ {++ "id": "openvibe-ai",++ "name": "OpenVibe.AI",++ "provider": "openvibe",++ "capabilities": {++ "host_access": true,++ "mcp": false,++ "long_autonomy": false,++ "resume": true,++ "edit": true,++ "review": true,++ "tools": true,++ "vision": false,++ "browser": false,++ "computer_use": false++ },++ "address": {++ "kind": "api",++ "target": "https://ai.openvibe.services/v1"++ },++ "price": {++ "amount_usd": 0,++ "unit": "hour"++ },++ "limits": {++ "max_duration_seconds": 3600,++ "max_concurrent_runs": 4,++ "max_context_tokens": 128000++ },++ "agents": [++ {++ "id": "openvibe-ai-router",++ "harness": "openvibe-ai",++ "provider": "openvibe",++ "model": "openvibe",++ "context_limits": {++ "input_tokens": 128000,++ "output_tokens": 16000++ },++ "concurrency": 4,++ "price_per_1k_tokens": {++ "fresh_usd": 0,++ "cached_usd": 0,++ "output_usd": 0++ },++ "availability_windows": []++ }++ ]++ },{"id": "openai-compatible","name": "Your own model",@@@ -176,6 +176,7 @@ ${table(['Agent', 'What it needs'], [[html`<a href="/harnesses#command-code">Command Code</a>`, html`<code>cmd</code> on PATH`],[html`<a href="/harnesses#aider">Aider</a>`, html`<code>aider</code> on PATH`],[html`<a href="/harnesses#deepseek">DeepSeek</a>`, html`<code>DEEPSEEK_API_KEY</code>; the harness runs its own agent loop against the API`],++ [html`<a href="/harnesses#openvibe-ai">OpenVibe.AI</a>`, html`<code>OPENVIBE_APP_ID</code> and <code>OPENVIBE_APP_SECRET</code> (an app of your project on openvibe.services with <code>ai.app.run</code>; AI's router picks the model and your project pays), or a Network token in <code>OPENVIBE_AI_TOKEN</code>`],[html`<a href="/harnesses#openai-compatible">Your own model</a>`, html`<code>OPENVIBE_CODES_BASE_URL</code> (an OpenAI-compatible server: llama.cpp, vLLM, Ollama, LM Studio…), <code>OPENVIBE_CODES_MODEL</code>, and <code>OPENVIBE_CODES_API_KEY</code> if it wants one`],])}<h2 id="use">Use it</h2>@@@ -28,7 +28,7 @@ async function mockApi() {if (!step) { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ choices: [{ message: { role: 'assistant', content: 'done' } }], usage: { prompt_tokens: 1, completion_tokens: 1 } })); return; }if (step.status) { res.writeHead(step.status, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: { message: step.error } })); return; }res.writeHead(200, { 'content-type': 'application/json' });-- res.end(JSON.stringify({ choices: [{ message: { role: 'assistant', content: step.content || '', ...(step.calls ? { tool_calls: step.calls.map((c, i) => ({ id: `call_${requests.length}_${i}`, type: 'function', function: { name: c[0], arguments: JSON.stringify(c[1]) } })) } : {}) } }], usage: { prompt_tokens: 1000, completion_tokens: 100, prompt_cache_hit_tokens: 400 } }));++ res.end(JSON.stringify({ choices: [{ message: { role: 'assistant', content: step.content || '', ...(step.calls ? { tool_calls: step.calls.map((c, i) => ({ id: `call_${requests.length}_${i}`, type: 'function', function: { name: c[0], arguments: JSON.stringify(c[1]) } })) } : {}) } }], usage: { prompt_tokens: 1000, completion_tokens: 100, prompt_cache_hit_tokens: 400 }, ...(step.x ? { x_openvibe: step.x } : {}) }));});});await new Promise((r) => server.listen(0, '127.0.0.1', r));@@@ -76,6 +76,37 @@ const toolResults = (events) => events.filter((e) => e.type === 'user').flatMap(assert.strictEqual(api.requests[2].body.messages.filter((m) => m.role === 'tool').length, 3, 'every tool result went back to the model');});++ await check('OpenVibe.AI: the app\'s Network token is fetched once and reused, and the cost is what AI reported', async () => {++ const tokenCalls = [];++ const network = http.createServer((req, res) => {++ let body = '';++ req.on('data', (d) => { body += d; });++ req.on('end', () => {++ tokenCalls.push({ path: req.url, body: Object.fromEntries(new URLSearchParams(body)) });++ res.writeHead(200, { 'content-type': 'application/json' });++ res.end(JSON.stringify({ access_token: 'ov-app-token', token_type: 'Bearer', expires_in: 300 }));++ });++ });++ await new Promise((r) => network.listen(0, '127.0.0.1', r));++ const aiEnv = { ...env, OPENVIBE_AI_URL: api.url, OPENVIBE_NETWORK_URL: `http://127.0.0.1:${network.address().port}`, OPENVIBE_APP_ID: 'app_01JAB2C3D4E5F6G7H8J9K0MNPQ', OPENVIBE_APP_SECRET: 's'.repeat(40) };++ const ai = createHarness({ env: aiEnv, store: createStore({ env: aiEnv }) });++ try {++ api.set([++ { calls: [['read_file', { path: 'src/a.js' }]], x: { run_id: 'run_1', cost_usd: 0.0021 } },++ { content: 'It exports name.', x: { run_id: 'run_2', cost_usd: 0.0009 } },++ ]);++ const events = await collect(ai.run({ prompt: 'what does a.js export?', agent: 'openvibe-ai', cwd: work, permission: 'read', handoff: false }));++ const r = events.find((e) => e.type === 'result');++ assert.strictEqual(r.is_error, false, r.result);++ assert.strictEqual(r.result, 'It exports name.');++ assert.deepStrictEqual(api.requests.map((q) => q.auth), ['Bearer ov-app-token', 'Bearer ov-app-token']);++ assert.strictEqual(api.requests[0].body.model, 'openvibe');++ assert.strictEqual(tokenCalls.length, 1, 'one token for both turns');++ assert.deepStrictEqual([tokenCalls[0].path, tokenCalls[0].body.grant_type, tokenCalls[0].body.audience, tokenCalls[0].body.client_id], ['/oauth/token', 'client_credentials', 'openvibe.ai', 'app_01JAB2C3D4E5F6G7H8J9K0MNPQ']);++ assert.ok(Math.abs(r.total_cost_usd - 0.003) < 1e-9, `cost ${r.total_cost_usd}`);++ } finally { await new Promise((r) => network.close(r)); }++ });++await check('read permission offers no writing tool, and a forced write is refused', async () => {api.set([{ calls: [['write_file', { path: 'src/new.js', content: 'x' }]] }, { content: 'ok' }]);const events = await collect(h.run({ prompt: 'look', agent: 'openai-compatible', cwd: work, permission: 'read', handoff: false }));@@@ -65,7 +65,7 @@ function priced(row, freshUsd) {await check('every seed offer validates as platform.resource-offer@1 of kind harness', async () => {const harnesses = createHarnesses({ catalogPath });const offers = harnesses.list().flatMap((harness) => harnesses.agents(harness.id).map((agent) => [harness, toOffer(harness, agent, { now })]));-- assert.strictEqual(offers.length, 7);++ assert.strictEqual(offers.length, 8);for (const [harness, offer] of offers) {const result = contracts.validate('platform.resource-offer@1', JSON.parse(JSON.stringify(offer)));assert.ok(result.valid, `${offer.offer_id}: ${JSON.stringify(result.errors)}`);@@@ -130,7 +130,7 @@ function priced(row, freshUsd) {const [harnessId] = r.selected.split(':');assert.ok(toOffer(harnesses.get(harnessId), harnesses.agents(harnessId)[0]).capabilities.includes('task:review'));assert.ok(r.reasons.length);-- assert.deepStrictEqual(r.candidates.map((c) => c.id), ['claude-code:claude-code-sonnet', 'codex:codex-default', 'command-code:command-code-default', 'opencode:opencode-default', 'aider:aider-default', 'deepseek:deepseek-chat', 'openai-compatible:openai-compatible-local']);++ assert.deepStrictEqual(r.candidates.map((c) => c.id), ['claude-code:claude-code-sonnet', 'codex:codex-default', 'command-code:command-code-default', 'opencode:opencode-default', 'aider:aider-default', 'deepseek:deepseek-chat', 'openvibe-ai:openvibe-ai-router', 'openai-compatible:openai-compatible-local']);});await check('an offer whose health is down is excluded with a reason', async () => {@@@ -13,7 +13,7 @@ const rows = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));(async () => {await check('every seed row matches the pinned harness and agent contracts', async () => {-- assert.strictEqual(rows.length, 7);++ assert.strictEqual(rows.length, 8);for (const { agents, ...offer } of rows) {const harnessCheck = contracts.validate('platform.harness-offer@1', offer);assert.strictEqual(harnessCheck.valid, true, JSON.stringify(harnessCheck.errors));
@@ -49,6 +49,7 @@ openvibe-codes resume <run id> "now add a test"
@@ -49,6 +49,7 @@ openvibe-codes resume <run id> "now add a test"
| Command Code | `cmd --print --output-format json` | `cmd` on PATH |
| Command Code | `cmd --print --output-format json` | `cmd` on PATH |
| Aider | `aider --message` | `aider` on PATH |
| Aider | `aider --message` | `aider` on PATH |
| DeepSeek | the harness's own agent loop ([harness/adapters/api.js](harness/adapters/api.js)) | `DEEPSEEK_API_KEY` |
| DeepSeek | the harness's own agent loop ([harness/adapters/api.js](harness/adapters/api.js)) | `DEEPSEEK_API_KEY` |
+| OpenVibe.AI | the same loop against AI's `/v1/chat/completions`; its router picks the model and each answer reports its cost | `OPENVIBE_APP_ID` + `OPENVIBE_APP_SECRET` (an app with `ai.app.run`; tokens are fetched and refreshed), or `OPENVIBE_AI_TOKEN` |
| Your own model | the same loop against any OpenAI-compatible server | `OPENVIBE_CODES_BASE_URL`, `OPENVIBE_CODES_MODEL`, optional `OPENVIBE_CODES_API_KEY` |
| Your own model | the same loop against any OpenAI-compatible server | `OPENVIBE_CODES_BASE_URL`, `OPENVIBE_CODES_MODEL`, optional `OPENVIBE_CODES_API_KEY` |
- **Events.** Every adapter turns its agent's output into one stream in Claude Code's `stream-json` shape (system/init, assistant text and tool_use, user tool_result, system warning/error/handoff, exactly one result), [harness/events.js](harness/events.js). Keys, tokens and private keys are redacted before an event is printed or kept.
- **Events.** Every adapter turns its agent's output into one stream in Claude Code's `stream-json` shape (system/init, assistant text and tool_use, user tool_result, system warning/error/handoff, exactly one result), [harness/events.js](harness/events.js). Keys, tokens and private keys are redacted before an event is printed or kept.
@@ -141,7 +141,10 @@ function runCommand(command, cwd, signal) {
@@ -141,7 +141,10 @@ function runCommand(command, cwd, signal) {
}
}
/**
/**
- * apiAgent({ id, name, baseUrl, keyEnv, model, priceIn, priceOut }) → an adapter.
+ * apiAgent({ id, name, baseUrl, keyEnv, model, priceIn, priceOut, token }) → an adapter.
+ * token(env, fetch) → a bearer, asked before every request (OpenVibe.AI takes a five-minute Network token, refreshed by
+ * the function); without it the key is keyEnv's value. When the API reports each answer's cost (OpenVibe.AI's
+ * x_openvibe.cost_usd), the run's cost is the sum of those, not a price list.
* ctx (from harness/index.js): { env, fetch, sessions } — sessions keeps the conversation for resume.
* ctx (from harness/index.js): { env, fetch, sessions } — sessions keeps the conversation for resume.
*/
*/
function apiAgent(spec) {
function apiAgent(spec) {
@@ -169,15 +172,17 @@ function apiAgent(spec) {
@@ -169,15 +172,17 @@ function apiAgent(spec) {
messages.push({ role: 'user', content: t.prompt });
messages.push({ role: 'user', content: t.prompt });
yield ev.init(sessionId, spec.id, model);
yield ev.init(sessionId, spec.id, model);
const u = { input: 0, output: 0, cacheRead: 0 };
const u = { input: 0, output: 0, cacheRead: 0 };
+ const reported = { usd: 0, any: false };
let lastText = '';
let lastText = '';
let turns = 0;
let turns = 0;
let failure = null;
let failure = null;
try {
try {
while (turns < maxTurns && !cancelled) {
while (turns < maxTurns && !cancelled) {
turns++;
turns++;
+ const bearer = spec.token ? await spec.token(env, doFetch) : key;
const res = await doFetch(`${base}/chat/completions`, {
const res = await doFetch(`${base}/chat/completions`, {
method: 'POST',
method: 'POST',
- headers: { 'content-type': 'application/json', ...(key ? { authorization: `Bearer ${key}` } : {}) },
+ headers: { 'content-type': 'application/json', ...(bearer ? { authorization: `Bearer ${bearer}` } : {}) },
body: JSON.stringify({ model, messages, tools, tool_choice: 'auto' }),
body: JSON.stringify({ model, messages, tools, tool_choice: 'auto' }),
signal: controller.signal,
signal: controller.signal,
});
});
@@ -188,6 +193,7 @@ function apiAgent(spec) {
@@ -188,6 +193,7 @@ function apiAgent(spec) {
break;
break;
}
}
const usage = body.usage || {};
const usage = body.usage || {};
+ if (body.x_openvibe && Number.isFinite(+body.x_openvibe.cost_usd)) { reported.usd += +body.x_openvibe.cost_usd; reported.any = true; }
u.input += +usage.prompt_tokens || 0; u.output += +usage.completion_tokens || 0;
u.input += +usage.prompt_tokens || 0; u.output += +usage.completion_tokens || 0;
u.cacheRead += +(usage.prompt_cache_hit_tokens || (usage.prompt_tokens_details && usage.prompt_tokens_details.cached_tokens)) || 0;
u.cacheRead += +(usage.prompt_cache_hit_tokens || (usage.prompt_tokens_details && usage.prompt_tokens_details.cached_tokens)) || 0;
const msg = body.choices[0].message || {};
const msg = body.choices[0].message || {};
@@ -216,7 +222,7 @@ function apiAgent(spec) {
@@ -216,7 +222,7 @@ function apiAgent(spec) {
yield ev.error(failure);
yield ev.error(failure);
}
}
if (ctx.sessions) ctx.sessions.save(sessionId, { agent: spec.id, model, messages: messages.slice(-200) });
if (ctx.sessions) ctx.sessions.save(sessionId, { agent: spec.id, model, messages: messages.slice(-200) });
- const cost = (u.input * (spec.priceIn || 0) + u.output * (spec.priceOut || 0)) / 1000;
+ const cost = reported.any ? reported.usd : (u.input * (spec.priceIn || 0) + u.output * (spec.priceOut || 0)) / 1000;
yield ev.result({ ok: !failure && !cancelled, text: failure || lastText, sessionId, model, startedAt, turns, costUsd: cost,
yield ev.result({ ok: !failure && !cancelled, text: failure || lastText, sessionId, model, startedAt, turns, costUsd: cost,
usage: ev.usage({ input: u.input - u.cacheRead, output: u.output, cacheRead: u.cacheRead }) });
usage: ev.usage({ input: u.input - u.cacheRead, output: u.output, cacheRead: u.cacheRead }) });
}());
}());
@@ -26,6 +26,27 @@ const { apiAgent } = require('./adapters/api');
@@ -26,6 +26,27 @@ const { apiAgent } = require('./adapters/api');
const ev = require('./events');
const ev = require('./events');
const { createStore } = require('./sessions');
const { createStore } = require('./sessions');
+/**
+ * OpenVibe.AI's token: a five-minute Network token for your app (client credentials, audience openvibe.ai), fetched when
+ * missing or within 30 s of expiring. OPENVIBE_AI_TOKEN, when set, is used as it is instead.
+ */
+function networkToken() {
+ let cached = null;
+ return async (env, doFetch) => {
+ if (env.OPENVIBE_AI_TOKEN) return env.OPENVIBE_AI_TOKEN;
+ if (cached && cached.until > Date.now()) return cached.token;
+ const network = String(env.OPENVIBE_NETWORK_URL || 'https://openvibe.network').replace(/\/+$/, '');
+ const res = await doFetch(`${network}/oauth/token`, {
+ method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' },
+ body: new URLSearchParams({ grant_type: 'client_credentials', client_id: env.OPENVIBE_APP_ID || '', client_secret: env.OPENVIBE_APP_SECRET || '', audience: 'openvibe.ai' }).toString(),
+ });
+ const body = await res.json().catch(() => null);
+ if (!res.ok || !body || !body.access_token) throw new Error(`OpenVibe.Network refused the app's token (HTTP ${res.status}${body && body.error ? `: ${body.error}` : ''})`);
+ cached = { token: body.access_token, until: Date.now() + Math.max(30, Number(body.expires_in) || 300) * 1000 - 30000 };
+ return cached.token;
+ };
+}
+
/** The adapter for each catalog harness (the catalog row's id). */
/** The adapter for each catalog harness (the catalog row's id). */
function adaptersFor(env) {
function adaptersFor(env) {
return {
return {
@@ -35,6 +56,8 @@ function adaptersFor(env) {
@@ -35,6 +56,8 @@ function adaptersFor(env) {
'command-code': cli.commandCode,
'command-code': cli.commandCode,
aider: cli.aider,
aider: cli.aider,
deepseek: apiAgent({ id: 'deepseek', name: 'DeepSeek', baseUrl: env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com', keyEnv: 'DEEPSEEK_API_KEY', model: 'deepseek-chat', priceIn: 0.00027, priceOut: 0.0011 }),
deepseek: apiAgent({ id: 'deepseek', name: 'DeepSeek', baseUrl: env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com', keyEnv: 'DEEPSEEK_API_KEY', model: 'deepseek-chat', priceIn: 0.00027, priceOut: 0.0011 }),
+ // OpenVibe.AI's OpenAI-compatible endpoint: its router picks the model, each answer reports its cost.
+ 'openvibe-ai': apiAgent({ id: 'openvibe-ai', name: 'OpenVibe.AI', baseUrl: env.OPENVIBE_AI_URL || 'https://ai.openvibe.services/v1', token: networkToken(), model: 'openvibe' }),
'openai-compatible': apiAgent({ id: 'openai-compatible', name: 'Your own model', baseUrl: env.OPENVIBE_CODES_BASE_URL || 'http://127.0.0.1:8080/v1', keyEnv: 'OPENVIBE_CODES_API_KEY', model: env.OPENVIBE_CODES_MODEL || 'local' }),
'openai-compatible': apiAgent({ id: 'openai-compatible', name: 'Your own model', baseUrl: env.OPENVIBE_CODES_BASE_URL || 'http://127.0.0.1:8080/v1', keyEnv: 'OPENVIBE_CODES_API_KEY', model: env.OPENVIBE_CODES_MODEL || 'local' }),
};
};
}
}
@@ -53,6 +76,7 @@ function availability(adapter, env) {
@@ -53,6 +76,7 @@ function availability(adapter, env) {
const where = onPath(adapter.bin, env);
const where = onPath(adapter.bin, env);
return where ? { available: true, where } : { available: false, reason: `\`${adapter.bin}\` is not on PATH (install ${adapter.name})` };
return where ? { available: true, where } : { available: false, reason: `\`${adapter.bin}\` is not on PATH (install ${adapter.name})` };
}
}
+ if (adapter.id === 'openvibe-ai') return env.OPENVIBE_AI_TOKEN || (env.OPENVIBE_APP_ID && env.OPENVIBE_APP_SECRET) ? { available: true, where: adapter.baseUrl } : { available: false, reason: 'set OPENVIBE_APP_ID and OPENVIBE_APP_SECRET (an app of your project on openvibe.services with ai.app.run)' };
if (adapter.id === 'openai-compatible') return env.OPENVIBE_CODES_BASE_URL ? { available: true, where: env.OPENVIBE_CODES_BASE_URL } : { available: false, reason: 'set OPENVIBE_CODES_BASE_URL (and OPENVIBE_CODES_MODEL) to your server' };
if (adapter.id === 'openai-compatible') return env.OPENVIBE_CODES_BASE_URL ? { available: true, where: env.OPENVIBE_CODES_BASE_URL } : { available: false, reason: 'set OPENVIBE_CODES_BASE_URL (and OPENVIBE_CODES_MODEL) to your server' };
return env[adapter.keyEnv] ? { available: true, where: adapter.baseUrl } : { available: false, reason: `set ${adapter.keyEnv}` };
return env[adapter.keyEnv] ? { available: true, where: adapter.baseUrl } : { available: false, reason: `set ${adapter.keyEnv}` };
}
}
@@ -301,13 +301,62 @@
@@ -301,13 +301,62 @@
"concurrency": 8,
"concurrency": 8,
"price_per_1k_tokens": {
"price_per_1k_tokens": {
"fresh_usd": 0.00027,
"fresh_usd": 0.00027,
- "cached_usd": 0.00007,
+ "cached_usd": 7e-05,
"output_usd": 0.0011
"output_usd": 0.0011
},
},
"availability_windows": []
"availability_windows": []
}
}
]
]
},
},
+ {
+ "id": "openvibe-ai",
+ "name": "OpenVibe.AI",
+ "provider": "openvibe",
+ "capabilities": {
+ "host_access": true,
+ "mcp": false,
+ "long_autonomy": false,
+ "resume": true,
+ "edit": true,
+ "review": true,
+ "tools": true,
+ "vision": false,
+ "browser": false,
+ "computer_use": false
+ },
+ "address": {
+ "kind": "api",
+ "target": "https://ai.openvibe.services/v1"
+ },
+ "price": {
+ "amount_usd": 0,
+ "unit": "hour"
+ },
+ "limits": {
+ "max_duration_seconds": 3600,
+ "max_concurrent_runs": 4,
+ "max_context_tokens": 128000
+ },
+ "agents": [
+ {
+ "id": "openvibe-ai-router",
+ "harness": "openvibe-ai",
+ "provider": "openvibe",
+ "model": "openvibe",
+ "context_limits": {
+ "input_tokens": 128000,
+ "output_tokens": 16000
+ },
+ "concurrency": 4,
+ "price_per_1k_tokens": {
+ "fresh_usd": 0,
+ "cached_usd": 0,
+ "output_usd": 0
+ },
+ "availability_windows": []
+ }
+ ]
+ },
{
{
"id": "openai-compatible",
"id": "openai-compatible",
"name": "Your own model",
"name": "Your own model",
@@ -176,6 +176,7 @@ ${table(['Agent', 'What it needs'], [
@@ -176,6 +176,7 @@ ${table(['Agent', 'What it needs'], [
[html`<a href="/harnesses#command-code">Command Code</a>`, html`<code>cmd</code> on PATH`],
[html`<a href="/harnesses#command-code">Command Code</a>`, html`<code>cmd</code> on PATH`],
[html`<a href="/harnesses#aider">Aider</a>`, html`<code>aider</code> on PATH`],
[html`<a href="/harnesses#aider">Aider</a>`, html`<code>aider</code> on PATH`],
[html`<a href="/harnesses#deepseek">DeepSeek</a>`, html`<code>DEEPSEEK_API_KEY</code>; the harness runs its own agent loop against the API`],
[html`<a href="/harnesses#deepseek">DeepSeek</a>`, html`<code>DEEPSEEK_API_KEY</code>; the harness runs its own agent loop against the API`],
+ [html`<a href="/harnesses#openvibe-ai">OpenVibe.AI</a>`, html`<code>OPENVIBE_APP_ID</code> and <code>OPENVIBE_APP_SECRET</code> (an app of your project on openvibe.services with <code>ai.app.run</code>; AI's router picks the model and your project pays), or a Network token in <code>OPENVIBE_AI_TOKEN</code>`],
[html`<a href="/harnesses#openai-compatible">Your own model</a>`, html`<code>OPENVIBE_CODES_BASE_URL</code> (an OpenAI-compatible server: llama.cpp, vLLM, Ollama, LM Studio…), <code>OPENVIBE_CODES_MODEL</code>, and <code>OPENVIBE_CODES_API_KEY</code> if it wants one`],
[html`<a href="/harnesses#openai-compatible">Your own model</a>`, html`<code>OPENVIBE_CODES_BASE_URL</code> (an OpenAI-compatible server: llama.cpp, vLLM, Ollama, LM Studio…), <code>OPENVIBE_CODES_MODEL</code>, and <code>OPENVIBE_CODES_API_KEY</code> if it wants one`],
])}
])}
<h2 id="use">Use it</h2>
<h2 id="use">Use it</h2>
@@ -28,7 +28,7 @@ async function mockApi() {
@@ -28,7 +28,7 @@ async function mockApi() {
if (!step) { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ choices: [{ message: { role: 'assistant', content: 'done' } }], usage: { prompt_tokens: 1, completion_tokens: 1 } })); return; }
if (!step) { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ choices: [{ message: { role: 'assistant', content: 'done' } }], usage: { prompt_tokens: 1, completion_tokens: 1 } })); return; }
if (step.status) { res.writeHead(step.status, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: { message: step.error } })); return; }
if (step.status) { res.writeHead(step.status, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: { message: step.error } })); return; }
res.writeHead(200, { 'content-type': 'application/json' });
res.writeHead(200, { 'content-type': 'application/json' });
- res.end(JSON.stringify({ choices: [{ message: { role: 'assistant', content: step.content || '', ...(step.calls ? { tool_calls: step.calls.map((c, i) => ({ id: `call_${requests.length}_${i}`, type: 'function', function: { name: c[0], arguments: JSON.stringify(c[1]) } })) } : {}) } }], usage: { prompt_tokens: 1000, completion_tokens: 100, prompt_cache_hit_tokens: 400 } }));
+ res.end(JSON.stringify({ choices: [{ message: { role: 'assistant', content: step.content || '', ...(step.calls ? { tool_calls: step.calls.map((c, i) => ({ id: `call_${requests.length}_${i}`, type: 'function', function: { name: c[0], arguments: JSON.stringify(c[1]) } })) } : {}) } }], usage: { prompt_tokens: 1000, completion_tokens: 100, prompt_cache_hit_tokens: 400 }, ...(step.x ? { x_openvibe: step.x } : {}) }));
});
});
});
});
await new Promise((r) => server.listen(0, '127.0.0.1', r));
await new Promise((r) => server.listen(0, '127.0.0.1', r));
@@ -76,6 +76,37 @@ const toolResults = (events) => events.filter((e) => e.type === 'user').flatMap(
@@ -76,6 +76,37 @@ const toolResults = (events) => events.filter((e) => e.type === 'user').flatMap(
assert.strictEqual(api.requests[2].body.messages.filter((m) => m.role === 'tool').length, 3, 'every tool result went back to the model');
assert.strictEqual(api.requests[2].body.messages.filter((m) => m.role === 'tool').length, 3, 'every tool result went back to the model');
});
});
+ await check('OpenVibe.AI: the app\'s Network token is fetched once and reused, and the cost is what AI reported', async () => {
+ const tokenCalls = [];
+ const network = http.createServer((req, res) => {
+ let body = '';
+ req.on('data', (d) => { body += d; });
+ req.on('end', () => {
+ tokenCalls.push({ path: req.url, body: Object.fromEntries(new URLSearchParams(body)) });
+ res.writeHead(200, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ access_token: 'ov-app-token', token_type: 'Bearer', expires_in: 300 }));
+ });
+ });
+ await new Promise((r) => network.listen(0, '127.0.0.1', r));
+ const aiEnv = { ...env, OPENVIBE_AI_URL: api.url, OPENVIBE_NETWORK_URL: `http://127.0.0.1:${network.address().port}`, OPENVIBE_APP_ID: 'app_01JAB2C3D4E5F6G7H8J9K0MNPQ', OPENVIBE_APP_SECRET: 's'.repeat(40) };
+ const ai = createHarness({ env: aiEnv, store: createStore({ env: aiEnv }) });
+ try {
+ api.set([
+ { calls: [['read_file', { path: 'src/a.js' }]], x: { run_id: 'run_1', cost_usd: 0.0021 } },
+ { content: 'It exports name.', x: { run_id: 'run_2', cost_usd: 0.0009 } },
+ ]);
+ const events = await collect(ai.run({ prompt: 'what does a.js export?', agent: 'openvibe-ai', cwd: work, permission: 'read', handoff: false }));
+ const r = events.find((e) => e.type === 'result');
+ assert.strictEqual(r.is_error, false, r.result);
+ assert.strictEqual(r.result, 'It exports name.');
+ assert.deepStrictEqual(api.requests.map((q) => q.auth), ['Bearer ov-app-token', 'Bearer ov-app-token']);
+ assert.strictEqual(api.requests[0].body.model, 'openvibe');
+ assert.strictEqual(tokenCalls.length, 1, 'one token for both turns');
+ assert.deepStrictEqual([tokenCalls[0].path, tokenCalls[0].body.grant_type, tokenCalls[0].body.audience, tokenCalls[0].body.client_id], ['/oauth/token', 'client_credentials', 'openvibe.ai', 'app_01JAB2C3D4E5F6G7H8J9K0MNPQ']);
+ assert.ok(Math.abs(r.total_cost_usd - 0.003) < 1e-9, `cost ${r.total_cost_usd}`);
+ } finally { await new Promise((r) => network.close(r)); }
+ });
+
await check('read permission offers no writing tool, and a forced write is refused', async () => {
await check('read permission offers no writing tool, and a forced write is refused', async () => {
api.set([{ calls: [['write_file', { path: 'src/new.js', content: 'x' }]] }, { content: 'ok' }]);
api.set([{ calls: [['write_file', { path: 'src/new.js', content: 'x' }]] }, { content: 'ok' }]);
const events = await collect(h.run({ prompt: 'look', agent: 'openai-compatible', cwd: work, permission: 'read', handoff: false }));
const events = await collect(h.run({ prompt: 'look', agent: 'openai-compatible', cwd: work, permission: 'read', handoff: false }));
@@ -65,7 +65,7 @@ function priced(row, freshUsd) {
@@ -65,7 +65,7 @@ function priced(row, freshUsd) {
await check('every seed offer validates as platform.resource-offer@1 of kind harness', async () => {
await check('every seed offer validates as platform.resource-offer@1 of kind harness', async () => {
const harnesses = createHarnesses({ catalogPath });
const harnesses = createHarnesses({ catalogPath });
const offers = harnesses.list().flatMap((harness) => harnesses.agents(harness.id).map((agent) => [harness, toOffer(harness, agent, { now })]));
const offers = harnesses.list().flatMap((harness) => harnesses.agents(harness.id).map((agent) => [harness, toOffer(harness, agent, { now })]));
- assert.strictEqual(offers.length, 7);
+ assert.strictEqual(offers.length, 8);
for (const [harness, offer] of offers) {
for (const [harness, offer] of offers) {
const result = contracts.validate('platform.resource-offer@1', JSON.parse(JSON.stringify(offer)));
const result = contracts.validate('platform.resource-offer@1', JSON.parse(JSON.stringify(offer)));
assert.ok(result.valid, `${offer.offer_id}: ${JSON.stringify(result.errors)}`);
assert.ok(result.valid, `${offer.offer_id}: ${JSON.stringify(result.errors)}`);
@@ -130,7 +130,7 @@ function priced(row, freshUsd) {
@@ -130,7 +130,7 @@ function priced(row, freshUsd) {
const [harnessId] = r.selected.split(':');
const [harnessId] = r.selected.split(':');
assert.ok(toOffer(harnesses.get(harnessId), harnesses.agents(harnessId)[0]).capabilities.includes('task:review'));
assert.ok(toOffer(harnesses.get(harnessId), harnesses.agents(harnessId)[0]).capabilities.includes('task:review'));
assert.ok(r.reasons.length);
assert.ok(r.reasons.length);
- assert.deepStrictEqual(r.candidates.map((c) => c.id), ['claude-code:claude-code-sonnet', 'codex:codex-default', 'command-code:command-code-default', 'opencode:opencode-default', 'aider:aider-default', 'deepseek:deepseek-chat', 'openai-compatible:openai-compatible-local']);
+ assert.deepStrictEqual(r.candidates.map((c) => c.id), ['claude-code:claude-code-sonnet', 'codex:codex-default', 'command-code:command-code-default', 'opencode:opencode-default', 'aider:aider-default', 'deepseek:deepseek-chat', 'openvibe-ai:openvibe-ai-router', 'openai-compatible:openai-compatible-local']);
});
});
await check('an offer whose health is down is excluded with a reason', async () => {
await check('an offer whose health is down is excluded with a reason', async () => {
@@ -13,7 +13,7 @@ const rows = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
@@ -13,7 +13,7 @@ const rows = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
(async () => {
(async () => {
await check('every seed row matches the pinned harness and agent contracts', async () => {
await check('every seed row matches the pinned harness and agent contracts', async () => {
- assert.strictEqual(rows.length, 7);
+ assert.strictEqual(rows.length, 8);
for (const { agents, ...offer } of rows) {
for (const { agents, ...offer } of rows) {
const harnessCheck = contracts.validate('platform.harness-offer@1', offer);
const harnessCheck = contracts.validate('platform.harness-offer@1', offer);
assert.strictEqual(harnessCheck.valid, true, JSON.stringify(harnessCheck.errors));
assert.strictEqual(harnessCheck.valid, true, JSON.stringify(harnessCheck.errors));