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)

OP OpenVibers committed 29 days ago · 0307a4c07d · 8 files, +120 −8

**Codes can run on OpenVibe.AI: a new `openvibe-ai` agent drives the harness's own loop against AI's OpenAI-compatible endpoint (plan T16, "Codes: BYO keys from AI").**

**Why:** AI now answers `POST /v1/chat/completions` with tools (AI#70). A project's app can therefore run Codes on AI's router. AI picks the model, the project pays, and every answer carries its cost. No model key is kept on the machine.

**The agent (`harness/index.js`):**
- `openvibe-ai` uses the API loop that DeepSeek and "Your own model" already use. Its base URL is `OPENVIBE_AI_URL`, defaulting to `https://ai.openvibe.services/v1`, and its model is `openvibe`.
- **Token:** `networkToken()` asks OpenVibe.Network for a client-credentials token with audience `openvibe.ai`, using `OPENVIBE_APP_ID` and `OPENVIBE_APP_SECRET`. The token is cached until 30 s before it expires. `OPENVIBE_AI_TOKEN`, when set, is used as it is.
- **Availability:** the agent is available with the app's id and secret, or with a token. Otherwise it reports which variables to set.

**The loop (`harness/adapters/api.js`):**
- `spec.token(env, fetch)` supplies the bearer before every request.
- When answers report `x_openvibe.cost_usd`, the run's cost is the sum of those amounts rather than a price-list estimate.

**Catalog:** the new offer `openvibe-ai` (agent `openvibe-ai-router`) sits before `openai-compatible`. It carries no list price, because AI reports the real cost.

**Tests:**
- `harness-api.test.js` adds one check against a fake Network and a fake AI: the token is fetched once and reused for both turns, the `client_credentials` request has the right shape, `model` is `openvibe`, and the cost is the sum AI reported.
- The offer counts in `harnesses.test.js` and `harness-placement.test.js` go from 7 to 8.

**Docs:** README's agent table and `/start`'s "The agents it can use" both gain an OpenVibe.AI row. All 11 test files that cover the change pass.

Changes

ddiff --git a/README.md b/README.md
iindex a1b4def..0e1af32 100644
---- a/README.md
++++ b/README.md
@@@ -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.
ddiff --git a/harness/adapters/api.js b/harness/adapters/api.js
iindex dfcaf85..b9d4591 100644
---- a/harness/adapters/api.js
++++ b/harness/adapters/api.js
@@@ -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 }) });
}());
ddiff --git a/harness/index.js b/harness/index.js
iindex 6f66f08..87eda75 100644
---- a/harness/index.js
++++ b/harness/index.js
@@@ -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}` };
}
ddiff --git a/server/data/harness-offers.json b/server/data/harness-offers.json
iindex dc44e19..910dc74 100644
---- a/server/data/harness-offers.json
++++ b/server/data/harness-offers.json
@@@ -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",
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 2ceb20f..b9bd253 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -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>
ddiff --git a/test/harness-api.test.js b/test/harness-api.test.js
iindex 36be9b0..a5ccde0 100644
---- a/test/harness-api.test.js
++++ b/test/harness-api.test.js
@@@ -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 }));
ddiff --git a/test/harness-placement.test.js b/test/harness-placement.test.js
iindex 35cbf37..fb1d754 100644
---- a/test/harness-placement.test.js
++++ b/test/harness-placement.test.js
@@@ -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 () => {
ddiff --git a/test/harnesses.test.js b/test/harnesses.test.js
iindex c37cf23..8cb2a2d 100644
---- a/test/harnesses.test.js
++++ b/test/harnesses.test.js
@@@ -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));