openvibers/openvibe.examples Commits
ai-anthropic and ai-python: OpenVibe AI from the Anthropic SDK and from Python (#55)
Changes
@@@ -41,6 +41,8 @@ Each example is small, runs on its own and proves one thing with a smoke test.| [space-deploy](examples/space-deploy) | Deploy a static site to OpenVibe.Space from CI with an app token (space.deploy.create, deployer role): the built folder as one deploy, live on main or a preview for a pull request, a refusal that changes nothing, rollback; a ready GitHub Actions workflow | `openvibe-sdk/space` (`deploys.uploadFiles`, `rollback`), `/auth` || [zone-s3](examples/zone-s3) | OpenVibe.Zone's private buckets over S3 with the official AWS SDK v3: put, get, a paged list, a presigned share link (at most 7 days) and delete; path-style, us-east-1, SigV4, an access key scoped to the zone | `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner` || [ai-chat](examples/ai-chat) | The official `openai` npm client on `https://ai.openvibe.services/v1` with a Network app token: plain and streamed Chat Completions, `x_openvibe` run info, the project's own provider by secret name | `openai` (`baseURL`, `apiKey` as a token function), `openvibe-sdk/auth` |++| [ai-anthropic](examples/ai-anthropic) | The official `@anthropic-ai/sdk` client on `https://ai.openvibe.services` (POST /v1/messages) with a Network app token through the SDK's `credentials`: the answer names the model that ran and carries `x_openvibe`. |++| [ai-python](examples/ai-python) | The official `openai` Python client on `https://ai.openvibe.services/v1`, with a Network app token from client credentials kept and renewed a minute early. |Each example folder has a README, a `.env.example`, its own `package.json` and a`test/smoke.test.js`. Configuration is environment variables only (`OV_CLIENT_ID`,@@@ -129,6 +131,8 @@ at the root one: `node --env-file=../../.env upload.js ./logo.png`.| [chat-bot](examples/chat-bot) | none: a Live API token (`hbt_…`) of a dedicated bot account | Live token scope `chat` | `OV_CHAT_TOKEN`, one of `OV_CHAT_CHANNEL_USER_ID` / `OV_CHAT_STREAM_ID` || [mod-manifest](examples/mod-manifest) | none | none | `OV_NETWORK_URL` for `--registry` || [ai-chat](examples/ai-chat) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_AI_URL`; own provider: `OV_PROVIDER_SECRET` (a secret's name), `OV_PROVIDER_BASE_URL`, `OV_PROVIDER_MODEL` |++| [ai-anthropic](examples/ai-anthropic) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_MODEL`, `OV_NETWORK_URL`, `OV_AI_URL` |++| [ai-python](examples/ai-python) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_MODEL`, `OV_NETWORK_URL`, `OV_AI_URL` |Rotate a secret in the portal or with `projects.credentials.rotate()`, revoke the app with`projects.apps.revoke()`. Issued tokens end within their 5-minute lifetime.nnew file mode 100644@@@ -0,0 +1,8 @@++# A sandbox app of your project (OpenVibe.Services -> Projects -> Apps), holding ai.app.run.++OV_CLIENT_ID=++OV_CLIENT_SECRET=++# Optional: a dynamic name (openvibe, openvibe/economy, ...) or an exact model id from openvibe.network/models.++# OV_MODEL=openvibe++# Optional: the Network and AI origins (the defaults are production).++# OV_NETWORK_URL=https://openvibe.network++# OV_AI_URL=https://ai.openvibe.servicesnnew file mode 100644@@@ -0,0 +1,31 @@++# AI with the Anthropic SDK++++Use OpenVibe.AI from the official [`@anthropic-ai/sdk`](https://www.npmjs.com/package/@anthropic-ai/sdk) client.++Point its `baseURL` at `https://ai.openvibe.services` (the SDK adds `/v1/messages`) and give it a Network app token++through `credentials`.++++```bash++node --env-file=.env ask.js "Say hi in five words"++# Hi there, nice to meet you!++# (qwen/qwen3-8b-abliterated:q4_k_m@3f2a1c9, $0.000001)++```++++## What it proves++++- **An app token of `openvibe.ai`, scoped `ai.app.run`.** This is the public capability developer apps call AI++ with; a sandbox app holds it without staff.++ - Network tokens last five minutes, so the SDK's `credentials` provider asks `openvibe-sdk/auth` for a fresh one.++ - The SDK sends it as `Authorization: Bearer`, never as an Anthropic `x-api-key`.++- **`messages.create()` works unchanged.**++ - `model` is a dynamic name (`openvibe`, `openvibe/economy`, …) or an exact model id from the model list.++ - The answer's `model` names the model that ran, and `x_openvibe` adds the run, the provider and the cost.++++## Run the smoke test++++`npm test -w examples/ai-anthropic` runs the real SDK against stand-ins of Network and AI, with no network.++++## Run it against the real platform++++1. Make a sandbox app in your project on OpenVibe.Services (Projects → Apps). It holds `ai.app.run`.++2. Copy `.env.example` to `.env` and fill in the app's client id and secret.++3. Run `node --env-file=.env ask.js "Say hi in five words"`.nnew file mode 100644@@@ -0,0 +1,60 @@++#!/usr/bin/env node++'use strict';++/**++ * AI with the Anthropic SDK: the official `@anthropic-ai/sdk` client, pointed at OpenVibe.AI's Anthropic Messages++ * endpoint (POST https://ai.openvibe.services/v1/messages).++ *++ * node --env-file=.env ask.js "Say hi in five words"++ *++ * 1. An app token for openvibe.ai (client credentials, scope ai.app.run: the public capability developer apps++ * call AI with). Network tokens last five minutes: the SDK's `credentials` provider asks openvibe-sdk for one++ * before it expires, and the SDK sends it as a Bearer token.++ * 2. new Anthropic({ baseURL: 'https://ai.openvibe.services', credentials }).++ * 3. messages.create(): `model` is a dynamic name (openvibe picks within your rules) or an exact model id from++ * https://openvibe.network/models; the answer names the model that ran.++ */++const Anthropic = require('@anthropic-ai/sdk');++const { createServiceTokenClient } = require('openvibe-sdk/auth');++++function loadConfig(env = process.env) {++ const missing = ['OV_CLIENT_ID', 'OV_CLIENT_SECRET'].filter((k) => !env[k]);++ if (missing.length) throw Object.assign(new Error(`missing environment variables: ${missing.join(', ')} (see .env.example)`), { code: 'config.missing' });++ return {++ network: env.OV_NETWORK_URL || 'https://openvibe.network',++ clientId: env.OV_CLIENT_ID,++ clientSecret: env.OV_CLIENT_SECRET,++ aiUrl: env.OV_AI_URL || 'https://ai.openvibe.services',++ model: env.OV_MODEL || 'openvibe',++ };++}++++function createClient(config, { fetch } = {}) {++ const tokens = createServiceTokenClient({++ network: config.network, clientId: config.clientId, clientSecret: config.clientSecret, fetch,++ audience: 'openvibe.ai', scope: 'ai.app.run',++ });++ return new Anthropic({++ baseURL: config.aiUrl,++ // The SDK caches what this returns until expiresAt; openvibe-sdk refreshes the token a minute early.++ credentials: async () => ({ token: await tokens.getToken(), expiresAt: Math.floor(Date.now() / 1000) + 60 }),++ ...(fetch ? { fetch } : {}),++ });++}++++async function ask(client, config, prompt) {++ const reply = await client.messages.create({ model: config.model, max_tokens: 512, messages: [{ role: 'user', content: prompt }] });++ const text = reply.content.filter((b) => b.type === 'text').map((b) => b.text).join('');++ return { text, model: reply.model, usage: reply.usage, x_openvibe: reply.x_openvibe || null };++}++++if (require.main === module) {++ (async () => {++ const config = loadConfig();++ const prompt = process.argv.slice(2).join(' ') || 'Say hi in five words';++ const out = await ask(createClient(config), config, prompt);++ console.log(out.text);++ console.log(`(${out.model}${out.x_openvibe && out.x_openvibe.cost_usd != null ? `, $${out.x_openvibe.cost_usd}` : ''})`);++ })().catch((e) => { console.error(e.message || e); process.exit(1); });++}++++module.exports = { loadConfig, createClient, ask };nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "name": "openvibe-example-ai-anthropic",++ "version": "0.1.0",++ "private": true,++ "description": "AI with the Anthropic SDK: the official @anthropic-ai/sdk client pointed at ai.openvibe.services (POST /v1/messages), with a Network app token.",++ "license": "MIT",++ "main": "ask.js",++ "scripts": {++ "start": "node --env-file-if-exists=.env ask.js",++ "test": "node test/smoke.test.js"++ },++ "engines": {++ "node": ">=22"++ },++ "dependencies": {++ "@anthropic-ai/sdk": "1.4.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ }++}nnew file mode 100644@@@ -0,0 +1,48 @@++'use strict';++/**++ * Smoke test: the Anthropic SDK against stand-ins of Network's /oauth/token and AI's /v1/messages, answering in the++ * shapes server/api/messages.js answers (a message with content blocks, usage and x_openvibe). No network.++ */++const assert = require('node:assert/strict');++const { loadConfig, createClient, ask } = require('../ask');++++globalThis.fetch = () => { throw new Error('network access in a smoke test'); };++const calls = [];++const json = (status, o) => new Response(JSON.stringify(o), { status, headers: { 'content-type': 'application/json' } });++++async function fakeFetch(url, init = {}) {++ const u = String(url);++ const headers = new Headers(init.headers);++ if (u === 'https://openvibe.network/oauth/token') {++ const form = new URLSearchParams(String(init.body));++ calls.push({ kind: 'token', audience: form.get('audience'), scope: form.get('scope') });++ return json(200, { access_token: 'tok-1', token_type: 'Bearer', expires_in: 300, scope: 'ai.app.run' });++ }++ if (u === 'https://ai.openvibe.services/v1/messages') {++ calls.push({ kind: 'messages', auth: headers.get('authorization'), apiKey: headers.get('x-api-key'), body: JSON.parse(init.body) });++ return json(200, {++ id: 'msg_01TEST', type: 'message', role: 'assistant', model: 'qwen/qwen3-8b-abliterated:q4_k_m@3f2a1c9', stop_reason: 'end_turn', stop_sequence: null,++ content: [{ type: 'text', text: 'Hi there, nice to meet you!' }], usage: { input_tokens: 12, output_tokens: 7 },++ x_openvibe: { run_id: 'run_01TEST', provider: 'own', fallback_used: false, cost_usd: 0.000001 },++ });++ }++ throw new Error(`unexpected request ${u}`);++}++++(async () => {++ assert.throws(() => loadConfig({}), /missing environment variables: OV_CLIENT_ID, OV_CLIENT_SECRET/);++ const config = loadConfig({ OV_CLIENT_ID: 'app_01TEST', OV_CLIENT_SECRET: 'not-a-real-secret' });++ const client = createClient(config, { fetch: fakeFetch });++ const out = await ask(client, config, 'Say hi in five words');++ assert.equal(out.text, 'Hi there, nice to meet you!');++ assert.equal(out.model, 'qwen/qwen3-8b-abliterated:q4_k_m@3f2a1c9', 'the answer names the model that ran');++ assert.equal(out.x_openvibe.run_id, 'run_01TEST');++ const token = calls.find((c) => c.kind === 'token');++ assert.deepEqual([token.audience, token.scope], ['openvibe.ai', 'ai.app.run'], 'an app token of openvibe.ai, scoped ai.app.run');++ const m = calls.find((c) => c.kind === 'messages');++ assert.equal(m.auth, 'Bearer tok-1', 'the Network token as a Bearer token');++ assert.ok(!m.apiKey, 'no x-api-key: this is not an Anthropic key');++ assert.equal(m.body.model, 'openvibe');++ assert.deepEqual(m.body.messages, [{ role: 'user', content: 'Say hi in five words' }]);++ console.log('ai-anthropic smoke: all checks passed');++})().catch((e) => { console.error(e); process.exit(1); });nnew file mode 100644@@@ -0,0 +1,7 @@++# A sandbox app of your project (OpenVibe.Services -> Projects -> Apps), holding ai.app.run.++OV_CLIENT_ID=++OV_CLIENT_SECRET=++# Optional: a dynamic name or an exact model id; the Network and AI origins (the defaults are production).++# OV_MODEL=openvibe++# OV_NETWORK_URL=https://openvibe.network++# OV_AI_URL=https://ai.openvibe.services/v1nnew file mode 100644@@@ -0,0 +1,32 @@++# AI from Python++++Use OpenVibe.AI from the official [`openai`](https://pypi.org/project/openai/) Python client:++- point `base_url` at `https://ai.openvibe.services/v1`;++- give it a Network app token for `openvibe.ai`, scoped `ai.app.run`.++++```bash++pip install openai httpx++OV_CLIENT_ID=… OV_CLIENT_SECRET=… python3 chat.py "Say hi in five words"++# Hi there, nice to meet you!++# (qwen/qwen3-8b-abliterated:q4_k_m@3f2a1c9, $1e-06)++```++++## What it proves++++- **The token.** The app's client credentials get a five-minute token from Network. `chat.py` keeps the token and++ asks again a minute before it ends.++- **The call.** `chat.completions.create()` works unchanged.++- **The model.** `model` is a dynamic name or an exact model id.++- **The answer.** It names the model that ran, and `x_openvibe` (in the response's extra fields) carries the run,++ the provider and the cost.++++## Run the smoke test++++`npm test -w examples/ai-python` checks the token request and the base URL, and compiles the script when Python is++on the machine.++++## Run it against the real platform++++1. Make a sandbox app in your project on OpenVibe.Services (Projects → Apps). It holds `ai.app.run`.++2. Install the client: `pip install openai httpx`.++3. Run `OV_CLIENT_ID=… OV_CLIENT_SECRET=… python3 chat.py "Say hi in five words"`.nnew file mode 100644@@@ -0,0 +1,52 @@++#!/usr/bin/env python3++"""AI from Python: the official `openai` client pointed at OpenVibe.AI.++++ pip install openai httpx++ OV_CLIENT_ID=... OV_CLIENT_SECRET=... python3 chat.py "Say hi in five words"++++1. An app token of openvibe.ai from Network (client credentials, scope ai.app.run: the public capability developer++ apps call AI with). Network tokens last five minutes; this keeps one and asks again a minute before it ends.++2. OpenAI(base_url="https://ai.openvibe.services/v1", api_key=<the token>).++3. `model` is a dynamic name (openvibe picks within your rules) or an exact model id from openvibe.network/models;++ the answer names the model that ran, and `x_openvibe` carries the run, the provider and the cost.++"""++import os++import sys++import time++++import httpx++from openai import OpenAI++++NETWORK = os.environ.get("OV_NETWORK_URL", "https://openvibe.network")++AI = os.environ.get("OV_AI_URL", "https://ai.openvibe.services/v1")++_token = {"value": None, "expires": 0.0}++++++def app_token() -> str:++ """A Network app token of openvibe.ai, reused until a minute before it expires."""++ if _token["value"] and time.time() < _token["expires"] - 60:++ return _token["value"]++ res = httpx.post(f"{NETWORK}/oauth/token", data={++ "grant_type": "client_credentials",++ "client_id": os.environ["OV_CLIENT_ID"],++ "client_secret": os.environ["OV_CLIENT_SECRET"],++ "audience": "openvibe.ai",++ "scope": "ai.app.run",++ }, timeout=10)++ res.raise_for_status()++ doc = res.json()++ _token.update(value=doc["access_token"], expires=time.time() + int(doc.get("expires_in", 300)))++ return _token["value"]++++++def ask(prompt: str, model: str = os.environ.get("OV_MODEL", "openvibe")):++ client = OpenAI(base_url=AI, api_key=app_token())++ reply = client.chat.completions.create(model=model, messages=[{"role": "user", "content": prompt}])++ extra = (reply.model_extra or {}).get("x_openvibe") or {}++ return reply.choices[0].message.content, reply.model, extra++++++if __name__ == "__main__":++ text, model, extra = ask(" ".join(sys.argv[1:]) or "Say hi in five words")++ print(text)++ print(f"({model}, ${extra.get('cost_usd')})" if extra.get("cost_usd") is not None else f"({model})")nnew file mode 100644@@@ -0,0 +1,13 @@++{++ "name": "openvibe-example-ai-python",++ "version": "0.1.0",++ "private": true,++ "description": "AI from Python: the official openai Python client pointed at ai.openvibe.services/v1, with a Network app token from client credentials.",++ "license": "MIT",++ "scripts": {++ "test": "node test/smoke.test.js"++ },++ "engines": {++ "node": ">=22"++ }++}nnew file mode 100644@@@ -0,0 +1,22 @@++'use strict';++/**++ * Smoke test of the Python example: it compiles (python3 -m py_compile, when Python is on the machine), and it asks++ * Network the right token (audience openvibe.ai, scope ai.app.run) and calls the right base URL. No network.++ */++const assert = require('node:assert/strict');++const fs = require('node:fs');++const path = require('node:path');++const { spawnSync } = require('node:child_process');++++const file = path.join(__dirname, '..', 'chat.py');++const src = fs.readFileSync(file, 'utf8');++assert.match(src, /"audience": "openvibe\.ai"/);++assert.match(src, /"scope": "ai\.app\.run"/);++assert.match(src, /https:\/\/ai\.openvibe\.services\/v1/);++assert.match(src, /OpenAI\(base_url=AI, api_key=app_token\(\)\)/);++const py = spawnSync('python3', ['-m', 'py_compile', file], { encoding: 'utf8' });++if (py.error && py.error.code === 'ENOENT') console.log('ai-python smoke: no python3 here; the source checks passed');++else {++ assert.equal(py.status, 0, py.stderr);++ console.log('ai-python smoke: compiles; all checks passed');++}@@@ -21,6 +21,46 @@"node": ">=22"}},++ "examples/ai-anthropic": {++ "name": "openvibe-example-ai-anthropic",++ "version": "0.1.0",++ "license": "MIT",++ "dependencies": {++ "@anthropic-ai/sdk": "1.4.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"++ },++ "engines": {++ "node": ">=22"++ }++ },++ "examples/ai-anthropic/node_modules/openvibe-sdk": {++ "version": "0.83.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.83.0",++ "integrity": "sha512-xHFcsZmRVuQX0HlMFA7Kc53GMpdZUXxkcR17whknFgUQznuWCwcnhwS87HnnqsrmnHp8AHC0d5uywLjfOwOYBw==",++ "license": "MIT",++ "bin": {++ "openvibe-download": "bin/openvibe-download.js"++ },++ "engines": {++ "node": ">=20"++ },++ "peerDependencies": {++ "@electric-sql/pglite": "^0.5.8",++ "iovalkey": "^0.4.0",++ "pg": "^8.23.0"++ },++ "peerDependenciesMeta": {++ "@electric-sql/pglite": {++ "optional": true++ },++ "iovalkey": {++ "optional": true++ },++ "pg": {++ "optional": true++ }++ }++ },"examples/ai-chat": {"name": "openvibe-example-ai-chat","version": "0.1.0",@@@ -33,6 +73,14 @@"node": ">=22"}},++ "examples/ai-python": {++ "name": "openvibe-example-ai-python",++ "version": "0.1.0",++ "license": "MIT",++ "engines": {++ "node": ">=22"++ }++ },"examples/browser-app": {"name": "openvibe-example-browser-app","version": "0.2.0",@@@ -191,6 +239,28 @@"node": ">=22"}},++ "node_modules/@anthropic-ai/sdk": {++ "version": "1.4.0",++ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.4.0.tgz",++ "integrity": "sha512-tyuibnhQL57kLOx5oomY4Ld7jOVOPNC5v3IiyRtR3l+Friv66tI4VbuZYF2NMel/T0WMuQMBFOMKDBUbUh8gPA==",++ "license": "Apache-2.0",++ "dependencies": {++ "@opentelemetry/api": "^1.4.1",++ "json-schema-to-ts": "^3.1.1",++ "standardwebhooks": "^1.0.0"++ },++ "bin": {++ "anthropic-ai-sdk": "bin/cli"++ },++ "peerDependencies": {++ "zod": "^3.25.0 || ^4.0.0"++ },++ "peerDependenciesMeta": {++ "zod": {++ "optional": true++ }++ }++ },"node_modules/@aws-sdk/checksums": {"version": "3.1001.5","resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.5.tgz",@@@ -516,12 +586,30 @@"node": ">=18.0.0"}},++ "node_modules/@babel/runtime": {++ "version": "7.29.10",++ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz",++ "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==",++ "license": "MIT",++ "engines": {++ "node": ">=6.9.0"++ }++ },"node_modules/@electric-sql/pglite": {"version": "0.5.8","resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz","integrity": "sha512-n9tsbUOhwx2epK1V0ZG9Ar4SHWUju04dhmzZXiSBXwBoleOvIfals33NAaWgagQVAL4Rbvx/Ptsu3P+pA09f6Q==","license": "Apache-2.0"},++ "node_modules/@opentelemetry/api": {++ "version": "1.10.0",++ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.10.0.tgz",++ "integrity": "sha512-lMkyes8iHrG+mk6WH7X70MdCLI2RS0FwNBY1PyQ8UemaijE2m5mv5A0Niy3HtbYTFrgkUEQjFMXmSAk2hQDuPQ==",++ "license": "Apache-2.0",++ "engines": {++ "node": ">=8.0.0"++ }++ },"node_modules/@smithy/core": {"version": "3.36.2","resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.36.2.tgz",@@@ -603,6 +691,12 @@"node": ">=18.0.0"}},++ "node_modules/@stablelib/base64": {++ "version": "1.0.1",++ "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",++ "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",++ "license": "MIT"++ },"node_modules/ajv": {"version": "8.20.0","resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",@@@ -648,6 +742,12 @@"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==","license": "MIT"},++ "node_modules/fast-sha256": {++ "version": "1.3.0",++ "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",++ "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",++ "license": "Unlicense"++ },"node_modules/fast-uri": {"version": "3.1.8","resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",@@@ -664,6 +764,19 @@],"license": "BSD-3-Clause"},++ "node_modules/json-schema-to-ts": {++ "version": "3.1.1",++ "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",++ "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",++ "license": "MIT",++ "dependencies": {++ "@babel/runtime": "^7.18.3",++ "ts-algebra": "^2.0.0"++ },++ "engines": {++ "node": ">=16"++ }++ },"node_modules/json-schema-traverse": {"version": "1.0.0","resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",@@@ -727,10 +840,18 @@"node": ">=20"}},++ "node_modules/openvibe-example-ai-anthropic": {++ "resolved": "examples/ai-anthropic",++ "link": true++ },"node_modules/openvibe-example-ai-chat": {"resolved": "examples/ai-chat","link": true},++ "node_modules/openvibe-example-ai-python": {++ "resolved": "examples/ai-python",++ "link": true++ },"node_modules/openvibe-example-browser-app": {"resolved": "examples/browser-app","link": true@@@ -961,6 +1082,22 @@"node": ">= 10.x"}},++ "node_modules/standardwebhooks": {++ "version": "1.1.1",++ "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.1.1.tgz",++ "integrity": "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==",++ "license": "MIT",++ "dependencies": {++ "@stablelib/base64": "^1.0.0",++ "fast-sha256": "^1.3.0"++ }++ },++ "node_modules/ts-algebra": {++ "version": "2.0.0",++ "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",++ "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",++ "license": "MIT"++ },"node_modules/tslib": {"version": "2.8.1","resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",@@@ -11,7 +11,7 @@ const fs = require('node:fs');const path = require('node:path');const ROOT = path.join(__dirname, '..');--const EXPECTED = ['ai-chat', 'browser-app', 'chat-bot', 'event-subscriber', 'game-scores', 'go-live', 'inventory-wear', 'media-uploader', 'mod-manifest', 'node-server-app', 'oauth-app', 'space-deploy', 'tool-job', 'webhook-consumer', 'zone-s3'];++const EXPECTED = ['ai-anthropic', 'ai-chat', 'ai-python', 'browser-app', 'chat-bot', 'event-subscriber', 'game-scores', 'go-live', 'inventory-wear', 'media-uploader', 'mod-manifest', 'node-server-app', 'oauth-app', 'space-deploy', 'tool-job', 'webhook-consumer', 'zone-s3'];// The pin every example must share: the repository's own (the pin bot moves them all at once).const ROOT_PKG = require(path.join(ROOT, 'package.json'));const SDK = (ROOT_PKG.dependencies || {})['openvibe-sdk'] || (ROOT_PKG.devDependencies || {})['openvibe-sdk'];
@@ -41,6 +41,8 @@ Each example is small, runs on its own and proves one thing with a smoke test.
@@ -41,6 +41,8 @@ Each example is small, runs on its own and proves one thing with a smoke test.
| [space-deploy](examples/space-deploy) | Deploy a static site to OpenVibe.Space from CI with an app token (space.deploy.create, deployer role): the built folder as one deploy, live on main or a preview for a pull request, a refusal that changes nothing, rollback; a ready GitHub Actions workflow | `openvibe-sdk/space` (`deploys.uploadFiles`, `rollback`), `/auth` |
| [space-deploy](examples/space-deploy) | Deploy a static site to OpenVibe.Space from CI with an app token (space.deploy.create, deployer role): the built folder as one deploy, live on main or a preview for a pull request, a refusal that changes nothing, rollback; a ready GitHub Actions workflow | `openvibe-sdk/space` (`deploys.uploadFiles`, `rollback`), `/auth` |
| [zone-s3](examples/zone-s3) | OpenVibe.Zone's private buckets over S3 with the official AWS SDK v3: put, get, a paged list, a presigned share link (at most 7 days) and delete; path-style, us-east-1, SigV4, an access key scoped to the zone | `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner` |
| [zone-s3](examples/zone-s3) | OpenVibe.Zone's private buckets over S3 with the official AWS SDK v3: put, get, a paged list, a presigned share link (at most 7 days) and delete; path-style, us-east-1, SigV4, an access key scoped to the zone | `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner` |
| [ai-chat](examples/ai-chat) | The official `openai` npm client on `https://ai.openvibe.services/v1` with a Network app token: plain and streamed Chat Completions, `x_openvibe` run info, the project's own provider by secret name | `openai` (`baseURL`, `apiKey` as a token function), `openvibe-sdk/auth` |
| [ai-chat](examples/ai-chat) | The official `openai` npm client on `https://ai.openvibe.services/v1` with a Network app token: plain and streamed Chat Completions, `x_openvibe` run info, the project's own provider by secret name | `openai` (`baseURL`, `apiKey` as a token function), `openvibe-sdk/auth` |
+| [ai-anthropic](examples/ai-anthropic) | The official `@anthropic-ai/sdk` client on `https://ai.openvibe.services` (POST /v1/messages) with a Network app token through the SDK's `credentials`: the answer names the model that ran and carries `x_openvibe`. |
+| [ai-python](examples/ai-python) | The official `openai` Python client on `https://ai.openvibe.services/v1`, with a Network app token from client credentials kept and renewed a minute early. |
Each example folder has a README, a `.env.example`, its own `package.json` and a
Each example folder has a README, a `.env.example`, its own `package.json` and a
`test/smoke.test.js`. Configuration is environment variables only (`OV_CLIENT_ID`,
`test/smoke.test.js`. Configuration is environment variables only (`OV_CLIENT_ID`,
@@ -129,6 +131,8 @@ at the root one: `node --env-file=../../.env upload.js ./logo.png`.
@@ -129,6 +131,8 @@ at the root one: `node --env-file=../../.env upload.js ./logo.png`.
| [chat-bot](examples/chat-bot) | none: a Live API token (`hbt_…`) of a dedicated bot account | Live token scope `chat` | `OV_CHAT_TOKEN`, one of `OV_CHAT_CHANNEL_USER_ID` / `OV_CHAT_STREAM_ID` |
| [chat-bot](examples/chat-bot) | none: a Live API token (`hbt_…`) of a dedicated bot account | Live token scope `chat` | `OV_CHAT_TOKEN`, one of `OV_CHAT_CHANNEL_USER_ID` / `OV_CHAT_STREAM_ID` |
| [mod-manifest](examples/mod-manifest) | none | none | `OV_NETWORK_URL` for `--registry` |
| [mod-manifest](examples/mod-manifest) | none | none | `OV_NETWORK_URL` for `--registry` |
| [ai-chat](examples/ai-chat) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_AI_URL`; own provider: `OV_PROVIDER_SECRET` (a secret's name), `OV_PROVIDER_BASE_URL`, `OV_PROVIDER_MODEL` |
| [ai-chat](examples/ai-chat) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_AI_URL`; own provider: `OV_PROVIDER_SECRET` (a secret's name), `OV_PROVIDER_BASE_URL`, `OV_PROVIDER_MODEL` |
+| [ai-anthropic](examples/ai-anthropic) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_MODEL`, `OV_NETWORK_URL`, `OV_AI_URL` |
+| [ai-python](examples/ai-python) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_MODEL`, `OV_NETWORK_URL`, `OV_AI_URL` |
Rotate a secret in the portal or with `projects.credentials.rotate()`, revoke the app with
Rotate a secret in the portal or with `projects.credentials.rotate()`, revoke the app with
`projects.apps.revoke()`. Issued tokens end within their 5-minute lifetime.
`projects.apps.revoke()`. Issued tokens end within their 5-minute lifetime.
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+# A sandbox app of your project (OpenVibe.Services -> Projects -> Apps), holding ai.app.run.
+OV_CLIENT_ID=
+OV_CLIENT_SECRET=
+# Optional: a dynamic name (openvibe, openvibe/economy, ...) or an exact model id from openvibe.network/models.
+# OV_MODEL=openvibe
+# Optional: the Network and AI origins (the defaults are production).
+# OV_NETWORK_URL=https://openvibe.network
+# OV_AI_URL=https://ai.openvibe.services
new file mode 100644
new file mode 100644
@@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
+# AI with the Anthropic SDK
+
+Use OpenVibe.AI from the official [`@anthropic-ai/sdk`](https://www.npmjs.com/package/@anthropic-ai/sdk) client.
+Point its `baseURL` at `https://ai.openvibe.services` (the SDK adds `/v1/messages`) and give it a Network app token
+through `credentials`.
+
+```bash
+node --env-file=.env ask.js "Say hi in five words"
+# Hi there, nice to meet you!
+# (qwen/qwen3-8b-abliterated:q4_k_m@3f2a1c9, $0.000001)
+```
+
+## What it proves
+
+- **An app token of `openvibe.ai`, scoped `ai.app.run`.** This is the public capability developer apps call AI
+ with; a sandbox app holds it without staff.
+ - Network tokens last five minutes, so the SDK's `credentials` provider asks `openvibe-sdk/auth` for a fresh one.
+ - The SDK sends it as `Authorization: Bearer`, never as an Anthropic `x-api-key`.
+- **`messages.create()` works unchanged.**
+ - `model` is a dynamic name (`openvibe`, `openvibe/economy`, …) or an exact model id from the model list.
+ - The answer's `model` names the model that ran, and `x_openvibe` adds the run, the provider and the cost.
+
+## Run the smoke test
+
+`npm test -w examples/ai-anthropic` runs the real SDK against stand-ins of Network and AI, with no network.
+
+## Run it against the real platform
+
+1. Make a sandbox app in your project on OpenVibe.Services (Projects → Apps). It holds `ai.app.run`.
+2. Copy `.env.example` to `.env` and fill in the app's client id and secret.
+3. Run `node --env-file=.env ask.js "Say hi in five words"`.
new file mode 100644
new file mode 100644
@@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
+#!/usr/bin/env node
+'use strict';
+/**
+ * AI with the Anthropic SDK: the official `@anthropic-ai/sdk` client, pointed at OpenVibe.AI's Anthropic Messages
+ * endpoint (POST https://ai.openvibe.services/v1/messages).
+ *
+ * node --env-file=.env ask.js "Say hi in five words"
+ *
+ * 1. An app token for openvibe.ai (client credentials, scope ai.app.run: the public capability developer apps
+ * call AI with). Network tokens last five minutes: the SDK's `credentials` provider asks openvibe-sdk for one
+ * before it expires, and the SDK sends it as a Bearer token.
+ * 2. new Anthropic({ baseURL: 'https://ai.openvibe.services', credentials }).
+ * 3. messages.create(): `model` is a dynamic name (openvibe picks within your rules) or an exact model id from
+ * https://openvibe.network/models; the answer names the model that ran.
+ */
+const Anthropic = require('@anthropic-ai/sdk');
+const { createServiceTokenClient } = require('openvibe-sdk/auth');
+
+function loadConfig(env = process.env) {
+ const missing = ['OV_CLIENT_ID', 'OV_CLIENT_SECRET'].filter((k) => !env[k]);
+ if (missing.length) throw Object.assign(new Error(`missing environment variables: ${missing.join(', ')} (see .env.example)`), { code: 'config.missing' });
+ return {
+ network: env.OV_NETWORK_URL || 'https://openvibe.network',
+ clientId: env.OV_CLIENT_ID,
+ clientSecret: env.OV_CLIENT_SECRET,
+ aiUrl: env.OV_AI_URL || 'https://ai.openvibe.services',
+ model: env.OV_MODEL || 'openvibe',
+ };
+}
+
+function createClient(config, { fetch } = {}) {
+ const tokens = createServiceTokenClient({
+ network: config.network, clientId: config.clientId, clientSecret: config.clientSecret, fetch,
+ audience: 'openvibe.ai', scope: 'ai.app.run',
+ });
+ return new Anthropic({
+ baseURL: config.aiUrl,
+ // The SDK caches what this returns until expiresAt; openvibe-sdk refreshes the token a minute early.
+ credentials: async () => ({ token: await tokens.getToken(), expiresAt: Math.floor(Date.now() / 1000) + 60 }),
+ ...(fetch ? { fetch } : {}),
+ });
+}
+
+async function ask(client, config, prompt) {
+ const reply = await client.messages.create({ model: config.model, max_tokens: 512, messages: [{ role: 'user', content: prompt }] });
+ const text = reply.content.filter((b) => b.type === 'text').map((b) => b.text).join('');
+ return { text, model: reply.model, usage: reply.usage, x_openvibe: reply.x_openvibe || null };
+}
+
+if (require.main === module) {
+ (async () => {
+ const config = loadConfig();
+ const prompt = process.argv.slice(2).join(' ') || 'Say hi in five words';
+ const out = await ask(createClient(config), config, prompt);
+ console.log(out.text);
+ console.log(`(${out.model}${out.x_openvibe && out.x_openvibe.cost_usd != null ? `, $${out.x_openvibe.cost_usd}` : ''})`);
+ })().catch((e) => { console.error(e.message || e); process.exit(1); });
+}
+
+module.exports = { loadConfig, createClient, ask };
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "name": "openvibe-example-ai-anthropic",
+ "version": "0.1.0",
+ "private": true,
+ "description": "AI with the Anthropic SDK: the official @anthropic-ai/sdk client pointed at ai.openvibe.services (POST /v1/messages), with a Network app token.",
+ "license": "MIT",
+ "main": "ask.js",
+ "scripts": {
+ "start": "node --env-file-if-exists=.env ask.js",
+ "test": "node test/smoke.test.js"
+ },
+ "engines": {
+ "node": ">=22"
+ },
+ "dependencies": {
+ "@anthropic-ai/sdk": "1.4.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
+'use strict';
+/**
+ * Smoke test: the Anthropic SDK against stand-ins of Network's /oauth/token and AI's /v1/messages, answering in the
+ * shapes server/api/messages.js answers (a message with content blocks, usage and x_openvibe). No network.
+ */
+const assert = require('node:assert/strict');
+const { loadConfig, createClient, ask } = require('../ask');
+
+globalThis.fetch = () => { throw new Error('network access in a smoke test'); };
+const calls = [];
+const json = (status, o) => new Response(JSON.stringify(o), { status, headers: { 'content-type': 'application/json' } });
+
+async function fakeFetch(url, init = {}) {
+ const u = String(url);
+ const headers = new Headers(init.headers);
+ if (u === 'https://openvibe.network/oauth/token') {
+ const form = new URLSearchParams(String(init.body));
+ calls.push({ kind: 'token', audience: form.get('audience'), scope: form.get('scope') });
+ return json(200, { access_token: 'tok-1', token_type: 'Bearer', expires_in: 300, scope: 'ai.app.run' });
+ }
+ if (u === 'https://ai.openvibe.services/v1/messages') {
+ calls.push({ kind: 'messages', auth: headers.get('authorization'), apiKey: headers.get('x-api-key'), body: JSON.parse(init.body) });
+ return json(200, {
+ id: 'msg_01TEST', type: 'message', role: 'assistant', model: 'qwen/qwen3-8b-abliterated:q4_k_m@3f2a1c9', stop_reason: 'end_turn', stop_sequence: null,
+ content: [{ type: 'text', text: 'Hi there, nice to meet you!' }], usage: { input_tokens: 12, output_tokens: 7 },
+ x_openvibe: { run_id: 'run_01TEST', provider: 'own', fallback_used: false, cost_usd: 0.000001 },
+ });
+ }
+ throw new Error(`unexpected request ${u}`);
+}
+
+(async () => {
+ assert.throws(() => loadConfig({}), /missing environment variables: OV_CLIENT_ID, OV_CLIENT_SECRET/);
+ const config = loadConfig({ OV_CLIENT_ID: 'app_01TEST', OV_CLIENT_SECRET: 'not-a-real-secret' });
+ const client = createClient(config, { fetch: fakeFetch });
+ const out = await ask(client, config, 'Say hi in five words');
+ assert.equal(out.text, 'Hi there, nice to meet you!');
+ assert.equal(out.model, 'qwen/qwen3-8b-abliterated:q4_k_m@3f2a1c9', 'the answer names the model that ran');
+ assert.equal(out.x_openvibe.run_id, 'run_01TEST');
+ const token = calls.find((c) => c.kind === 'token');
+ assert.deepEqual([token.audience, token.scope], ['openvibe.ai', 'ai.app.run'], 'an app token of openvibe.ai, scoped ai.app.run');
+ const m = calls.find((c) => c.kind === 'messages');
+ assert.equal(m.auth, 'Bearer tok-1', 'the Network token as a Bearer token');
+ assert.ok(!m.apiKey, 'no x-api-key: this is not an Anthropic key');
+ assert.equal(m.body.model, 'openvibe');
+ assert.deepEqual(m.body.messages, [{ role: 'user', content: 'Say hi in five words' }]);
+ console.log('ai-anthropic smoke: all checks passed');
+})().catch((e) => { console.error(e); process.exit(1); });
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+# A sandbox app of your project (OpenVibe.Services -> Projects -> Apps), holding ai.app.run.
+OV_CLIENT_ID=
+OV_CLIENT_SECRET=
+# Optional: a dynamic name or an exact model id; the Network and AI origins (the defaults are production).
+# OV_MODEL=openvibe
+# OV_NETWORK_URL=https://openvibe.network
+# OV_AI_URL=https://ai.openvibe.services/v1
new file mode 100644
new file mode 100644
@@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
+# AI from Python
+
+Use OpenVibe.AI from the official [`openai`](https://pypi.org/project/openai/) Python client:
+- point `base_url` at `https://ai.openvibe.services/v1`;
+- give it a Network app token for `openvibe.ai`, scoped `ai.app.run`.
+
+```bash
+pip install openai httpx
+OV_CLIENT_ID=… OV_CLIENT_SECRET=… python3 chat.py "Say hi in five words"
+# Hi there, nice to meet you!
+# (qwen/qwen3-8b-abliterated:q4_k_m@3f2a1c9, $1e-06)
+```
+
+## What it proves
+
+- **The token.** The app's client credentials get a five-minute token from Network. `chat.py` keeps the token and
+ asks again a minute before it ends.
+- **The call.** `chat.completions.create()` works unchanged.
+- **The model.** `model` is a dynamic name or an exact model id.
+- **The answer.** It names the model that ran, and `x_openvibe` (in the response's extra fields) carries the run,
+ the provider and the cost.
+
+## Run the smoke test
+
+`npm test -w examples/ai-python` checks the token request and the base URL, and compiles the script when Python is
+on the machine.
+
+## Run it against the real platform
+
+1. Make a sandbox app in your project on OpenVibe.Services (Projects → Apps). It holds `ai.app.run`.
+2. Install the client: `pip install openai httpx`.
+3. Run `OV_CLIENT_ID=… OV_CLIENT_SECRET=… python3 chat.py "Say hi in five words"`.
new file mode 100644
new file mode 100644
@@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+"""AI from Python: the official `openai` client pointed at OpenVibe.AI.
+
+ pip install openai httpx
+ OV_CLIENT_ID=... OV_CLIENT_SECRET=... python3 chat.py "Say hi in five words"
+
+1. An app token of openvibe.ai from Network (client credentials, scope ai.app.run: the public capability developer
+ apps call AI with). Network tokens last five minutes; this keeps one and asks again a minute before it ends.
+2. OpenAI(base_url="https://ai.openvibe.services/v1", api_key=<the token>).
+3. `model` is a dynamic name (openvibe picks within your rules) or an exact model id from openvibe.network/models;
+ the answer names the model that ran, and `x_openvibe` carries the run, the provider and the cost.
+"""
+import os
+import sys
+import time
+
+import httpx
+from openai import OpenAI
+
+NETWORK = os.environ.get("OV_NETWORK_URL", "https://openvibe.network")
+AI = os.environ.get("OV_AI_URL", "https://ai.openvibe.services/v1")
+_token = {"value": None, "expires": 0.0}
+
+
+def app_token() -> str:
+ """A Network app token of openvibe.ai, reused until a minute before it expires."""
+ if _token["value"] and time.time() < _token["expires"] - 60:
+ return _token["value"]
+ res = httpx.post(f"{NETWORK}/oauth/token", data={
+ "grant_type": "client_credentials",
+ "client_id": os.environ["OV_CLIENT_ID"],
+ "client_secret": os.environ["OV_CLIENT_SECRET"],
+ "audience": "openvibe.ai",
+ "scope": "ai.app.run",
+ }, timeout=10)
+ res.raise_for_status()
+ doc = res.json()
+ _token.update(value=doc["access_token"], expires=time.time() + int(doc.get("expires_in", 300)))
+ return _token["value"]
+
+
+def ask(prompt: str, model: str = os.environ.get("OV_MODEL", "openvibe")):
+ client = OpenAI(base_url=AI, api_key=app_token())
+ reply = client.chat.completions.create(model=model, messages=[{"role": "user", "content": prompt}])
+ extra = (reply.model_extra or {}).get("x_openvibe") or {}
+ return reply.choices[0].message.content, reply.model, extra
+
+
+if __name__ == "__main__":
+ text, model, extra = ask(" ".join(sys.argv[1:]) or "Say hi in five words")
+ print(text)
+ print(f"({model}, ${extra.get('cost_usd')})" if extra.get("cost_usd") is not None else f"({model})")
new file mode 100644
new file mode 100644
@@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
+{
+ "name": "openvibe-example-ai-python",
+ "version": "0.1.0",
+ "private": true,
+ "description": "AI from Python: the official openai Python client pointed at ai.openvibe.services/v1, with a Network app token from client credentials.",
+ "license": "MIT",
+ "scripts": {
+ "test": "node test/smoke.test.js"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+'use strict';
+/**
+ * Smoke test of the Python example: it compiles (python3 -m py_compile, when Python is on the machine), and it asks
+ * Network the right token (audience openvibe.ai, scope ai.app.run) and calls the right base URL. No network.
+ */
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { spawnSync } = require('node:child_process');
+
+const file = path.join(__dirname, '..', 'chat.py');
+const src = fs.readFileSync(file, 'utf8');
+assert.match(src, /"audience": "openvibe\.ai"/);
+assert.match(src, /"scope": "ai\.app\.run"/);
+assert.match(src, /https:\/\/ai\.openvibe\.services\/v1/);
+assert.match(src, /OpenAI\(base_url=AI, api_key=app_token\(\)\)/);
+const py = spawnSync('python3', ['-m', 'py_compile', file], { encoding: 'utf8' });
+if (py.error && py.error.code === 'ENOENT') console.log('ai-python smoke: no python3 here; the source checks passed');
+else {
+ assert.equal(py.status, 0, py.stderr);
+ console.log('ai-python smoke: compiles; all checks passed');
+}
@@ -21,6 +21,46 @@
@@ -21,6 +21,46 @@
"node": ">=22"
"node": ">=22"
}
}
},
},
+ "examples/ai-anthropic": {
+ "name": "openvibe-example-ai-anthropic",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@anthropic-ai/sdk": "1.4.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "examples/ai-anthropic/node_modules/openvibe-sdk": {
+ "version": "0.83.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.83.0",
+ "integrity": "sha512-xHFcsZmRVuQX0HlMFA7Kc53GMpdZUXxkcR17whknFgUQznuWCwcnhwS87HnnqsrmnHp8AHC0d5uywLjfOwOYBw==",
+ "license": "MIT",
+ "bin": {
+ "openvibe-download": "bin/openvibe-download.js"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@electric-sql/pglite": "^0.5.8",
+ "iovalkey": "^0.4.0",
+ "pg": "^8.23.0"
+ },
+ "peerDependenciesMeta": {
+ "@electric-sql/pglite": {
+ "optional": true
+ },
+ "iovalkey": {
+ "optional": true
+ },
+ "pg": {
+ "optional": true
+ }
+ }
+ },
"examples/ai-chat": {
"examples/ai-chat": {
"name": "openvibe-example-ai-chat",
"name": "openvibe-example-ai-chat",
"version": "0.1.0",
"version": "0.1.0",
@@ -33,6 +73,14 @@
@@ -33,6 +73,14 @@
"node": ">=22"
"node": ">=22"
}
}
},
},
+ "examples/ai-python": {
+ "name": "openvibe-example-ai-python",
+ "version": "0.1.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22"
+ }
+ },
"examples/browser-app": {
"examples/browser-app": {
"name": "openvibe-example-browser-app",
"name": "openvibe-example-browser-app",
"version": "0.2.0",
"version": "0.2.0",
@@ -191,6 +239,28 @@
@@ -191,6 +239,28 @@
"node": ">=22"
"node": ">=22"
}
}
},
},
+ "node_modules/@anthropic-ai/sdk": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.4.0.tgz",
+ "integrity": "sha512-tyuibnhQL57kLOx5oomY4Ld7jOVOPNC5v3IiyRtR3l+Friv66tI4VbuZYF2NMel/T0WMuQMBFOMKDBUbUh8gPA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.4.1",
+ "json-schema-to-ts": "^3.1.1",
+ "standardwebhooks": "^1.0.0"
+ },
+ "bin": {
+ "anthropic-ai-sdk": "bin/cli"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@aws-sdk/checksums": {
"node_modules/@aws-sdk/checksums": {
"version": "3.1001.5",
"version": "3.1001.5",
"resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.5.tgz",
"resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.5.tgz",
@@ -516,12 +586,30 @@
@@ -516,12 +586,30 @@
"node": ">=18.0.0"
"node": ">=18.0.0"
}
}
},
},
+ "node_modules/@babel/runtime": {
+ "version": "7.29.10",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz",
+ "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@electric-sql/pglite": {
"node_modules/@electric-sql/pglite": {
"version": "0.5.8",
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",
"integrity": "sha512-n9tsbUOhwx2epK1V0ZG9Ar4SHWUju04dhmzZXiSBXwBoleOvIfals33NAaWgagQVAL4Rbvx/Ptsu3P+pA09f6Q==",
"integrity": "sha512-n9tsbUOhwx2epK1V0ZG9Ar4SHWUju04dhmzZXiSBXwBoleOvIfals33NAaWgagQVAL4Rbvx/Ptsu3P+pA09f6Q==",
"license": "Apache-2.0"
"license": "Apache-2.0"
},
},
+ "node_modules/@opentelemetry/api": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.10.0.tgz",
+ "integrity": "sha512-lMkyes8iHrG+mk6WH7X70MdCLI2RS0FwNBY1PyQ8UemaijE2m5mv5A0Niy3HtbYTFrgkUEQjFMXmSAk2hQDuPQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/@smithy/core": {
"node_modules/@smithy/core": {
"version": "3.36.2",
"version": "3.36.2",
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.36.2.tgz",
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.36.2.tgz",
@@ -603,6 +691,12 @@
@@ -603,6 +691,12 @@
"node": ">=18.0.0"
"node": ">=18.0.0"
}
}
},
},
+ "node_modules/@stablelib/base64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
+ "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
+ "license": "MIT"
+ },
"node_modules/ajv": {
"node_modules/ajv": {
"version": "8.20.0",
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
@@ -648,6 +742,12 @@
@@ -648,6 +742,12 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
"license": "MIT"
},
},
+ "node_modules/fast-sha256": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
+ "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
+ "license": "Unlicense"
+ },
"node_modules/fast-uri": {
"node_modules/fast-uri": {
"version": "3.1.8",
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
@@ -664,6 +764,19 @@
@@ -664,6 +764,19 @@
],
],
"license": "BSD-3-Clause"
"license": "BSD-3-Clause"
},
},
+ "node_modules/json-schema-to-ts": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
+ "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "ts-algebra": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/json-schema-traverse": {
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -727,10 +840,18 @@
@@ -727,10 +840,18 @@
"node": ">=20"
"node": ">=20"
}
}
},
},
+ "node_modules/openvibe-example-ai-anthropic": {
+ "resolved": "examples/ai-anthropic",
+ "link": true
+ },
"node_modules/openvibe-example-ai-chat": {
"node_modules/openvibe-example-ai-chat": {
"resolved": "examples/ai-chat",
"resolved": "examples/ai-chat",
"link": true
"link": true
},
},
+ "node_modules/openvibe-example-ai-python": {
+ "resolved": "examples/ai-python",
+ "link": true
+ },
"node_modules/openvibe-example-browser-app": {
"node_modules/openvibe-example-browser-app": {
"resolved": "examples/browser-app",
"resolved": "examples/browser-app",
"link": true
"link": true
@@ -961,6 +1082,22 @@
@@ -961,6 +1082,22 @@
"node": ">= 10.x"
"node": ">= 10.x"
}
}
},
},
+ "node_modules/standardwebhooks": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.1.1.tgz",
+ "integrity": "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@stablelib/base64": "^1.0.0",
+ "fast-sha256": "^1.3.0"
+ }
+ },
+ "node_modules/ts-algebra": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
+ "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
+ "license": "MIT"
+ },
"node_modules/tslib": {
"node_modules/tslib": {
"version": "2.8.1",
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -11,7 +11,7 @@ const fs = require('node:fs');
@@ -11,7 +11,7 @@ const fs = require('node:fs');
const path = require('node:path');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const ROOT = path.join(__dirname, '..');
-const EXPECTED = ['ai-chat', 'browser-app', 'chat-bot', 'event-subscriber', 'game-scores', 'go-live', 'inventory-wear', 'media-uploader', 'mod-manifest', 'node-server-app', 'oauth-app', 'space-deploy', 'tool-job', 'webhook-consumer', 'zone-s3'];
+const EXPECTED = ['ai-anthropic', 'ai-chat', 'ai-python', 'browser-app', 'chat-bot', 'event-subscriber', 'game-scores', 'go-live', 'inventory-wear', 'media-uploader', 'mod-manifest', 'node-server-app', 'oauth-app', 'space-deploy', 'tool-job', 'webhook-consumer', 'zone-s3'];
// The pin every example must share: the repository's own (the pin bot moves them all at once).
// The pin every example must share: the repository's own (the pin bot moves them all at once).
const ROOT_PKG = require(path.join(ROOT, 'package.json'));
const ROOT_PKG = require(path.join(ROOT, 'package.json'));
const SDK = (ROOT_PKG.dependencies || {})['openvibe-sdk'] || (ROOT_PKG.devDependencies || {})['openvibe-sdk'];
const SDK = (ROOT_PKG.dependencies || {})['openvibe-sdk'] || (ROOT_PKG.devDependencies || {})['openvibe-sdk'];