diff --git a/entitlements-node/.env.example b/entitlements-node/.env.example new file mode 100644 index 0000000..ccc0444 --- /dev/null +++ b/entitlements-node/.env.example @@ -0,0 +1,2 @@ +OPENMETER_URL=https://openmeter.cloud +OPENMETER_TOKEN=om_ diff --git a/entitlements-node/.gitignore b/entitlements-node/.gitignore new file mode 100644 index 0000000..c6bba59 --- /dev/null +++ b/entitlements-node/.gitignore @@ -0,0 +1,130 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/entitlements-node/.prettierrc.json b/entitlements-node/.prettierrc.json new file mode 100644 index 0000000..e63fa60 --- /dev/null +++ b/entitlements-node/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "trailingComma": "all", + "semi": false, + "singleQuote": true +} diff --git a/entitlements-node/README.md b/entitlements-node/README.md new file mode 100644 index 0000000..2d9f56f --- /dev/null +++ b/entitlements-node/README.md @@ -0,0 +1,15 @@ +# Entitlements - Node.js + +In this example, a metered entitlement is assigned to all subjects with a starting balance. + +- Subjects are stored in `subject.json` +- Feature tracks GPT-4o input tokens + +Set your environment variables in `.env`. Use `.env.example` as a starting point. + +```sh +pnpm install +pnpm start +# or +# pnpm dev +``` diff --git a/entitlements-node/index.js b/entitlements-node/index.js new file mode 100644 index 0000000..26efd56 --- /dev/null +++ b/entitlements-node/index.js @@ -0,0 +1,87 @@ +import { OpenMeter } from '@openmeter/sdk' +import subjects from './subjects.json' with { type: 'json' } + +const openmeter = new OpenMeter({ + baseUrl: process.env.OPENMETER_URL, + token: process.env.OPENMETER_TOKEN, +}) + +// Upsert subjects +await openmeter.subjects.upsert(subjects) + +// Create meter +try { + await openmeter.meters.create({ + slug: 'tokens_total', + description: 'LLM tokens total', + aggregation: 'SUM', + windowSize: 'MINUTE', + eventType: 'prompt', + valueProperty: '$.tokens', + groupBy: { + model: '$.model', + type: '$.type', + }, + }) +} catch (err) { + if (err.statusCode !== 409) { + throw err + } +} + +// Create feature +try { + await openmeter.features.create({ + name: 'GPT-4o inputs', + key: 'gpt-4o-inputs', + meterSlug: 'tokens_total', + meterGroupByFilters: { + model: 'gpt-4o', + type: 'input', + }, + }) +} catch (err) { + if (err.statusCode !== 409) { + throw err + } +} + +// Create entitlements +for (const subject of subjects) { + try { + await openmeter.subjects.createEntitlement(subject.key, { + featureKey: 'gpt-4o-inputs', + type: 'metered', + usagePeriod: { + interval: 'DAY', + }, + issueAfterReset: 1000, + }) + } catch (err) { + if (err.statusCode !== 409) { + throw err + } + } +} + +// Ingest usage +// await openmeter.events.ingest({ +// type: 'prompt', +// subject: subjects[0].key, +// data: { +// tokens: 100, +// model: 'gpt-4o', +// type: 'input', +// } +// }) +// The event will be processed asynchronously... +// await new Promise(resolve => setTimeout(resolve, 3000)) + +// Check entitlement value +for (const subject of subjects) { + const value = await openmeter.subjects.getEntitlementValue( + subject.key, + 'gpt-4o-inputs', + ) + console.log(subject.key, value) +} diff --git a/entitlements-node/package.json b/entitlements-node/package.json new file mode 100644 index 0000000..ba31d51 --- /dev/null +++ b/entitlements-node/package.json @@ -0,0 +1,22 @@ +{ + "name": "entitlements-node", + "private": true, + "version": "1.0.0", + "description": "Entitlements Demo", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "TZ=utc node --env-file=.env index.js", + "dev": "TZ=utc node --watch --env-file=.env index.js" + }, + "author": "Andras Toth", + "license": "Apache-2.0", + "packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903", + "dependencies": { + "@openmeter/sdk": "1.0.0-beta.120" + }, + "devDependencies": { + "prettier": "^3.3.3" + } +} diff --git a/entitlements-node/pnpm-lock.yaml b/entitlements-node/pnpm-lock.yaml new file mode 100644 index 0000000..712a10d --- /dev/null +++ b/entitlements-node/pnpm-lock.yaml @@ -0,0 +1,42 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@openmeter/sdk': + specifier: 1.0.0-beta.120 + version: 1.0.0-beta.120 + devDependencies: + prettier: + specifier: ^3.3.3 + version: 3.3.3 + +packages: + + '@openmeter/sdk@1.0.0-beta.120': + resolution: {integrity: sha512-ICEpJMmHjQ77FCUN0ApnIpLrMQGvf6M//nA8fNAWi02HXUvQlrzaraCOW5k+mmzpCRUjVO5B4WLcmPvFxjEyMQ==} + engines: {node: '>=18.16.1'} + + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + + undici@6.19.2: + resolution: {integrity: sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==} + engines: {node: '>=18.17'} + +snapshots: + + '@openmeter/sdk@1.0.0-beta.120': + dependencies: + undici: 6.19.2 + + prettier@3.3.3: {} + + undici@6.19.2: {} diff --git a/entitlements-node/subjects.json b/entitlements-node/subjects.json new file mode 100644 index 0000000..8642b54 --- /dev/null +++ b/entitlements-node/subjects.json @@ -0,0 +1,18 @@ +[ + { + "key": "1df50e6e-e539-42e6-b524-9479004a4c93", + "displayName": "Harris Inc" + }, + { + "key": "498b61ac-5754-417e-a301-a25293e598c2", + "displayName": "Mertz and Sons" + }, + { + "key": "4cf5a03f-4d26-471e-b4ee-77671caf0838", + "displayName": "Lowe LLC" + }, + { + "key": "0c37bf17-361c-4234-b257-0d8c1b0726ea", + "displayName": "Bailey Inc" + } +]