openvibers/openvibe.codes Commits

Route hosted CLI calls through Exchange (#397)

OP OpenVibers committed 2 days ago · 05d6af474f · 4 files, +38 −2

Changes

ddiff --git a/.env.example b/.env.example
iindex 23ee708..a08d7d1 100644
---- a/.env.example
++++ b/.env.example
@@@ -52,6 +52,8 @@ OV_OAUTH_CLIENT_SECRET=
# OpenVibe.Exchange's compatible API: the deepseek harness's default path (a run token names the person; the same
# chat-completions API and model ids as OpenVibe.AI). CODES_EXCHANGE_URL overrides https://api.openvibe.exchange.
#CODES_EXCHANGE_URL=https://api.openvibe.exchange
++# Hosted CLI runs use Exchange by default with their task-scoped run token. Set CODES_CLI_EXCHANGE=0 to roll back to AI.
++#CODES_CLI_EXCHANGE=1
# Goals and the independent verifier (AL1), off by default. With CODES_GOALS_ENABLED=1 a task may carry a goal and a
# typed check; the harness runs the check in the task's workspace and a cheap fast model on OpenVibe.AI accepts or
ddiff --git a/server/config.js b/server/config.js
iindex 630c578..56364f4 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -100,6 +100,8 @@ function load(env = process.env) {
// key and whose budget the run is. Two hours is the longest a coding attempt may run.
runTokens: {
ttlSeconds: Math.min(7200, Math.max(60, int(env.CODES_RUN_TOKEN_TTL_S, 7200))),
++ // Hosted CLI requests use Exchange by default after the production canary; set to 0 to roll back to AI.
++ cliExchange: env.CODES_CLI_EXCHANGE == null || bool(env.CODES_CLI_EXCHANGE),
// The harness env variable names a job may set (harness/job.js): the model APIs' own names, nothing else.
envNames: ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_CUSTOM_HEADERS', 'OPENAI_BASE_URL',
'OPENAI_API_KEY', 'OPENAI_CUSTOM_HEADERS', 'DEEPSEEK_BASE_URL', 'DEEPSEEK_API_KEY', 'OPENVIBE_AI_URL',
ddiff --git a/server/domain/run-tokens.js b/server/domain/run-tokens.js
iindex 022ca72..58f3187 100644
---- a/server/domain/run-tokens.js
++++ b/server/domain/run-tokens.js
@@@ -10,7 +10,8 @@
* are, so a run can be traced), never logged and never answered; the transcript normaliser redacts it everywhere a
* line is written (server/domain/redact.js).
*
-- * The environment the harness gets points it at OpenVibe.AI:
++ * The environment the harness gets points hosted CLI requests at OpenVibe.Exchange, with the run token as its key.
++ * The rollback switch points them at OpenVibe.AI. The harness env names remain the CLI's own API names:
* Claude Code ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_CUSTOM_HEADERS
* Codex, OpenAI-like OPENAI_BASE_URL, OPENAI_API_KEY
* The credential header (x-openvibe-credential) names the provider whose key the run uses, when the person has one.
@@@ -31,6 +32,7 @@ const USER = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
/** The harnesses that speak Anthropic's Messages API (the rest speak OpenAI's Chat Completions). */
const ANTHROPIC_HARNESSES = ['claude-code', 'claude-other'];
++const CLI_HARNESSES = new Set(['claude-code', 'claude-other', 'codex', 'opencode', 'command-code', 'aider', 'minimax-code', 'muse']);
/** The claims of a JWT, decoded but not verified (the receiver verifies; this is for the jti and the expiry). */
function claims(token) {
@@@ -61,7 +63,8 @@ function exchangeBase(config) {
* has a key for that provider) the header that says which key.
*/
function envFor({ config, harness, token, provider, ownKey }) {
-- const base = aiBase(config);
++ const cliExchange = CLI_HARNESSES.has(harness) && (!config.runTokens || config.runTokens.cliExchange !== false);
++ const base = cliExchange ? exchangeBase(config) : aiBase(config);
const credential = ownKey && provider ? `x-openvibe-credential: ${provider}` : null;
if (ANTHROPIC_HARNESSES.includes(harness)) {
return {
ddiff --git a/test/harness-exchange.test.js b/test/harness-exchange.test.js
iindex 07fd8b5..41aa205 100644
---- a/test/harness-exchange.test.js
++++ b/test/harness-exchange.test.js
@@@ -11,6 +11,7 @@ const os = require('os');
const path = require('path');
const { createHarness, exchangeKeyOf, exchangeModelOf, platformHost } = require('../harness');
const { createStore } = require('../harness/sessions');
++const { load: loadConfig } = require('../server/config');
const { check, done } = require('./helpers/boot');
/** A stand-in Exchange: records every request; fails the test if the direct DeepSeek host is called on the default path. */
@@@ -120,6 +121,34 @@ const collect = async (run) => { const out = []; for await (const e of run) out.
assert.strictEqual(own.DEEPSEEK_API_KEY, 'run-token');
});
++ await check('hosted CLI run tokens use Exchange for both API shapes and retain the provider selection header', () => {
++ const { envFor } = require('../server/domain/run-tokens');
++ const config = loadConfig({ CODES_EXCHANGE_URL: 'https://api.openvibe.exchange' });
++ assert.strictEqual(config.runTokens.cliExchange, true);
++ for (const harness of ['claude-code', 'claude-other']) {
++ const env = envFor({ config, harness, token: 'run-token', provider: 'anthropic', ownKey: true });
++ assert.strictEqual(env.ANTHROPIC_BASE_URL, 'https://api.openvibe.exchange');
++ assert.strictEqual(env.ANTHROPIC_AUTH_TOKEN, 'run-token');
++ assert.strictEqual(env.ANTHROPIC_CUSTOM_HEADERS, 'x-openvibe-credential: anthropic');
++ }
++ for (const harness of ['codex', 'opencode', 'command-code', 'aider', 'minimax-code', 'muse']) {
++ const env = envFor({ config, harness, token: 'run-token', provider: 'openai', ownKey: true });
++ assert.strictEqual(env.OPENAI_BASE_URL, 'https://api.openvibe.exchange/v1');
++ assert.strictEqual(env.OPENAI_API_KEY, 'run-token');
++ assert.strictEqual(env.OPENAI_CUSTOM_HEADERS, 'x-openvibe-credential: openai');
++ }
++ });
++
++ await check('CODES_CLI_EXCHANGE=0 rolls hosted CLI requests back to OpenVibe.AI', () => {
++ const { envFor } = require('../server/domain/run-tokens');
++ const config = loadConfig({ CODES_CLI_EXCHANGE: '0' });
++ assert.strictEqual(config.runTokens.cliExchange, false);
++ const anthropic = envFor({ config, harness: 'claude-code', token: 'run-token', provider: 'anthropic', ownKey: false });
++ const openai = envFor({ config, harness: 'codex', token: 'run-token', provider: 'openai', ownKey: false });
++ assert.strictEqual(anthropic.ANTHROPIC_BASE_URL, 'https://ai.openvibe.services');
++ assert.strictEqual(openai.OPENAI_BASE_URL, 'https://ai.openvibe.services/v1');
++ });
++
await check('without an Exchange key or an own DeepSeek key the agent is unavailable, and the reason names both', () => {
const h = createHarness({ env: baseEnv, store: createStore({ env: baseEnv }) });
const ds = h.agents().find((a) => a.id === 'deepseek');