openvibers/openvibe.codes Commits
Codes: the OpenVibe.AI agent names the project's own provider (OPENVIBE_AI_SECRET → x_openvibe.provider) (#72)
Changes
@@@ -49,7 +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` |++| OpenVibe.AI | the same loop against AI's `/v1/chat/completions`; 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`. App runs on AI's router use free capacity without tools, so name your project's own provider: `OPENVIBE_AI_SECRET` (a project secret granted to `ai` for reveal to the provider's origin), `OPENVIBE_AI_PROVIDER_URL` (default `https://api.openai.com/v1`), `OPENVIBE_AI_PROVIDER_MODEL`. The key stays in OpenVibe.Network, never on the machine || 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.@@@ -142,6 +142,7 @@ function runCommand(command, cwd, signal) {/*** apiAgent({ id, name, baseUrl, keyEnv, model, priceIn, priceOut, token }) → an adapter.++ * extra(env) → fields added to every request body (OpenVibe.AI's x_openvibe.provider: the project's own provider).* 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.@@@ -183,7 +184,7 @@ function apiAgent(spec) {const res = await doFetch(`${base}/chat/completions`, {method: 'POST',headers: { 'content-type': 'application/json', ...(bearer ? { authorization: `Bearer ${bearer}` } : {}) },-- body: JSON.stringify({ model, messages, tools, tool_choice: 'auto' }),++ body: JSON.stringify({ ...(spec.extra ? spec.extra(env) : {}), model, messages, tools, tool_choice: 'auto' }),signal: controller.signal,});const body = await res.json().catch(() => null);@@@ -47,6 +47,12 @@ function networkToken() {};}++/** x_openvibe.provider for OpenVibe.AI when OPENVIBE_AI_SECRET names the project's own provider key, else nothing. */++function projectProvider(env) {++ if (!env.OPENVIBE_AI_SECRET) return {};++ return { x_openvibe: { provider: { base_url: env.OPENVIBE_AI_PROVIDER_URL || 'https://api.openai.com/v1', model: env.OPENVIBE_AI_PROVIDER_MODEL || 'gpt-5-mini', secret: env.OPENVIBE_AI_SECRET } } };++}++/** The adapter for each catalog harness (the catalog row's id). */function adaptersFor(env) {return {@@@ -57,7 +63,9 @@ function adaptersFor(env) {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' }),++ // OPENVIBE_AI_SECRET names a project secret granted to ai (ADR-056): AI sends each request to the project's own++ // provider (OPENVIBE_AI_PROVIDER_URL, OPENVIBE_AI_PROVIDER_MODEL) with that key, revealed per request by Network.++ 'openvibe-ai': apiAgent({ id: 'openvibe-ai', name: 'OpenVibe.AI', baseUrl: env.OPENVIBE_AI_URL || 'https://ai.openvibe.services/v1', token: networkToken(), model: 'openvibe', extra: projectProvider }),'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' }),};}@@@ -176,7 +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#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>), or a Network token in <code>OPENVIBE_AI_TOKEN</code>. For tool-capable models, keep your provider's key as a project secret granted to <code>ai</code> and name it in <code>OPENVIBE_AI_SECRET</code>, with <code>OPENVIBE_AI_PROVIDER_URL</code> and <code>OPENVIBE_AI_PROVIDER_MODEL</code>: the key never sits on this machine`],[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>@@@ -104,6 +104,15 @@ const toolResults = (events) => events.filter((e) => e.type === 'user').flatMap(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}`);++ assert.ok(!('x_openvibe' in api.requests[0].body), 'the router, when no project provider is named');++ // OPENVIBE_AI_SECRET names the project's own provider: every request carries it for AI to use.++ const ownEnv = { ...aiEnv, OPENVIBE_AI_SECRET: 'deepseek-key', OPENVIBE_AI_PROVIDER_URL: 'https://api.deepseek.com/v1', OPENVIBE_AI_PROVIDER_MODEL: 'deepseek-chat' };++ const own = createHarness({ env: ownEnv, store: createStore({ env: ownEnv }) });++ api.set([{ content: 'done' }]);++ const before = api.requests.length;++ const done = (await collect(own.run({ prompt: 'hi', agent: 'openvibe-ai', cwd: work, permission: 'read', handoff: false }))).find((e) => e.type === 'result');++ assert.strictEqual(done.is_error, false, done.result);++ assert.deepStrictEqual(api.requests[before].body.x_openvibe, { provider: { base_url: 'https://api.deepseek.com/v1', model: 'deepseek-chat', secret: 'deepseek-key' } });} finally { await new Promise((r) => network.close(r)); }});
@@ -49,7 +49,7 @@ openvibe-codes resume <run id> "now add a test"
@@ -49,7 +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` |
+| OpenVibe.AI | the same loop against AI's `/v1/chat/completions`; 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`. App runs on AI's router use free capacity without tools, so name your project's own provider: `OPENVIBE_AI_SECRET` (a project secret granted to `ai` for reveal to the provider's origin), `OPENVIBE_AI_PROVIDER_URL` (default `https://api.openai.com/v1`), `OPENVIBE_AI_PROVIDER_MODEL`. The key stays in OpenVibe.Network, never on the machine |
| 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.
@@ -142,6 +142,7 @@ function runCommand(command, cwd, signal) {
@@ -142,6 +142,7 @@ function runCommand(command, cwd, signal) {
/**
/**
* apiAgent({ id, name, baseUrl, keyEnv, model, priceIn, priceOut, token }) → an adapter.
* apiAgent({ id, name, baseUrl, keyEnv, model, priceIn, priceOut, token }) → an adapter.
+ * extra(env) → fields added to every request body (OpenVibe.AI's x_openvibe.provider: the project's own provider).
* token(env, fetch) → a bearer, asked before every request (OpenVibe.AI takes a five-minute Network token, refreshed by
* 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
* 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.
* x_openvibe.cost_usd), the run's cost is the sum of those, not a price list.
@@ -183,7 +184,7 @@ function apiAgent(spec) {
@@ -183,7 +184,7 @@ function apiAgent(spec) {
const res = await doFetch(`${base}/chat/completions`, {
const res = await doFetch(`${base}/chat/completions`, {
method: 'POST',
method: 'POST',
headers: { 'content-type': 'application/json', ...(bearer ? { authorization: `Bearer ${bearer}` } : {}) },
headers: { 'content-type': 'application/json', ...(bearer ? { authorization: `Bearer ${bearer}` } : {}) },
- body: JSON.stringify({ model, messages, tools, tool_choice: 'auto' }),
+ body: JSON.stringify({ ...(spec.extra ? spec.extra(env) : {}), model, messages, tools, tool_choice: 'auto' }),
signal: controller.signal,
signal: controller.signal,
});
});
const body = await res.json().catch(() => null);
const body = await res.json().catch(() => null);
@@ -47,6 +47,12 @@ function networkToken() {
@@ -47,6 +47,12 @@ function networkToken() {
};
};
}
}
+/** x_openvibe.provider for OpenVibe.AI when OPENVIBE_AI_SECRET names the project's own provider key, else nothing. */
+function projectProvider(env) {
+ if (!env.OPENVIBE_AI_SECRET) return {};
+ return { x_openvibe: { provider: { base_url: env.OPENVIBE_AI_PROVIDER_URL || 'https://api.openai.com/v1', model: env.OPENVIBE_AI_PROVIDER_MODEL || 'gpt-5-mini', secret: env.OPENVIBE_AI_SECRET } } };
+}
+
/** 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 {
@@ -57,7 +63,9 @@ function adaptersFor(env) {
@@ -57,7 +63,9 @@ function adaptersFor(env) {
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'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' }),
+ // OPENVIBE_AI_SECRET names a project secret granted to ai (ADR-056): AI sends each request to the project's own
+ // provider (OPENVIBE_AI_PROVIDER_URL, OPENVIBE_AI_PROVIDER_MODEL) with that key, revealed per request by Network.
+ 'openvibe-ai': apiAgent({ id: 'openvibe-ai', name: 'OpenVibe.AI', baseUrl: env.OPENVIBE_AI_URL || 'https://ai.openvibe.services/v1', token: networkToken(), model: 'openvibe', extra: projectProvider }),
'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' }),
};
};
}
}
@@ -176,7 +176,7 @@ ${table(['Agent', 'What it needs'], [
@@ -176,7 +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#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>), or a Network token in <code>OPENVIBE_AI_TOKEN</code>. For tool-capable models, keep your provider's key as a project secret granted to <code>ai</code> and name it in <code>OPENVIBE_AI_SECRET</code>, with <code>OPENVIBE_AI_PROVIDER_URL</code> and <code>OPENVIBE_AI_PROVIDER_MODEL</code>: the key never sits on this machine`],
[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>
@@ -104,6 +104,15 @@ const toolResults = (events) => events.filter((e) => e.type === 'user').flatMap(
@@ -104,6 +104,15 @@ const toolResults = (events) => events.filter((e) => e.type === 'user').flatMap(
assert.strictEqual(tokenCalls.length, 1, 'one token for both turns');
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.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}`);
assert.ok(Math.abs(r.total_cost_usd - 0.003) < 1e-9, `cost ${r.total_cost_usd}`);
+ assert.ok(!('x_openvibe' in api.requests[0].body), 'the router, when no project provider is named');
+ // OPENVIBE_AI_SECRET names the project's own provider: every request carries it for AI to use.
+ const ownEnv = { ...aiEnv, OPENVIBE_AI_SECRET: 'deepseek-key', OPENVIBE_AI_PROVIDER_URL: 'https://api.deepseek.com/v1', OPENVIBE_AI_PROVIDER_MODEL: 'deepseek-chat' };
+ const own = createHarness({ env: ownEnv, store: createStore({ env: ownEnv }) });
+ api.set([{ content: 'done' }]);
+ const before = api.requests.length;
+ const done = (await collect(own.run({ prompt: 'hi', agent: 'openvibe-ai', cwd: work, permission: 'read', handoff: false }))).find((e) => e.type === 'result');
+ assert.strictEqual(done.is_error, false, done.result);
+ assert.deepStrictEqual(api.requests[before].body.x_openvibe, { provider: { base_url: 'https://api.deepseek.com/v1', model: 'deepseek-chat', secret: 'deepseek-key' } });
} finally { await new Promise((r) => network.close(r)); }
} finally { await new Promise((r) => network.close(r)); }
});
});