diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 496c567c4..c2988d19a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ on: branches: ["main"] jobs: - test: + test-backend: runs-on: ubuntu-latest strategy: @@ -22,13 +22,13 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Build + - name: Backend Tests run: | rm package-lock.json npm install -g npm@latest npm install npm run build - npm run test + npm run test:backend api-test: name: backend (node env, api-test) diff --git a/package.json b/package.json index 48643a30e..711efb1d3 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "scripts": { "test": "npx mocha src/phoenix/test && npx vitest run src/backend && node src/backend/tools/test.mjs", "test:puterjs-api": "vitest run tests/puterJsApiTests", - "test:backend": "vitest run --config=src/backend/vitest.config.ts src/backend", + "test:backend": "npm run build:ts; vitest run --config=src/backend/vitest.config.ts src/backend", "start=gui": "nodemon --exec \"node dev-server.js\" ", "start": "node ./tools/run-selfhosted.js", "prestart": "npm run build:ts", diff --git a/src/backend/src/modules/puterai/TogetherAIService.js b/src/backend/src/modules/puterai/TogetherAIService.js index d48111292..df12eb275 100644 --- a/src/backend/src/modules/puterai/TogetherAIService.js +++ b/src/backend/src/modules/puterai/TogetherAIService.js @@ -156,14 +156,21 @@ class TogetherAIService extends BaseService { }; } - // return completion.choices[0]; const ret = completion.choices[0]; + ret.usage = { input_tokens: completion.usage.prompt_tokens, output_tokens: completion.usage.completion_tokens, }; + + const trackedUsage = OpenAIUtil.extractMeteredUsage(completion.usage); + const costOverrides = { + prompt_tokens: trackedUsage.prompt_tokens * (modelDetails?.cost?.input ?? 0), + completion_tokens: trackedUsage.completion_tokens * (modelDetails?.cost?.output ?? 0), + }; // Metering: record usage for non-streamed completion - this.meteringService.utilRecordUsageObject(completion.usage, actor, modelId); + this.meteringService.utilRecordUsageObject(completion.usage, actor, modelId, costOverrides); + return ret; }, }, diff --git a/src/backend/src/modules/test-core/TestCoreModule.js b/src/backend/src/modules/test-core/TestCoreModule.js index 2e967d764..4f6c7d8de 100644 --- a/src/backend/src/modules/test-core/TestCoreModule.js +++ b/src/backend/src/modules/test-core/TestCoreModule.js @@ -1,8 +1,13 @@ +const { AnomalyService } = require('../../services/AnomalyService'); +const { GroupService } = require('../../services/auth/GroupService'); +const { PermissionService } = require('../../services/auth/PermissionService'); const { CommandService } = require('../../services/CommandService'); const { SqliteDatabaseAccessService } = require('../../services/database/SqliteDatabaseAccessService'); const { DetailProviderService } = require('../../services/DetailProviderService'); const { EventService } = require('../../services/EventService'); const { GetUserService } = require('../../services/GetUserService'); +const { MeteringServiceWrapper } = require('../../services/MeteringService/MeteringServiceWrapper.mjs'); +const { DBKVServiceWrapper } = require('../../services/repositories/DBKVStore/index.mjs'); const { SUService } = require('../../services/SUService'); const { TraceService } = require('../../services/TraceService'); const { AlarmService } = require('../core/AlarmService'); @@ -18,6 +23,11 @@ class TestCoreModule { services.registerService('alarm', AlarmService); services.registerService('event', EventService); services.registerService('commands', CommandService); + services.registerService('meteringService', MeteringServiceWrapper); + services.registerService('puter-kvstore', DBKVServiceWrapper); + services.registerService('permission', PermissionService); + services.registerService('group', GroupService); + services.registerService('anomaly', AnomalyService); } } diff --git a/src/backend/src/services/MeteringService/MeteringService.test.ts b/src/backend/src/services/MeteringService/MeteringService.test.ts index 8aff1adac..d5dbfb243 100644 --- a/src/backend/src/services/MeteringService/MeteringService.test.ts +++ b/src/backend/src/services/MeteringService/MeteringService.test.ts @@ -223,5 +223,5 @@ describe('MeteringService', async () => { count: 1, }); } - }); + }, 10000); }); diff --git a/src/backend/src/services/repositories/DBKVStore/DBKVStore.test.ts b/src/backend/src/services/repositories/DBKVStore/DBKVStore.test.ts index 198a903f3..8e059ae0f 100644 --- a/src/backend/src/services/repositories/DBKVStore/DBKVStore.test.ts +++ b/src/backend/src/services/repositories/DBKVStore/DBKVStore.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'; import { createTestKernel } from '../../../../tools/test.mjs'; import * as config from '../../../config'; import { Actor } from '../../auth/Actor'; -import { MeteringServiceWrapper } from '../../MeteringService/MeteringServiceWrapper.mjs'; import { DBKVServiceWrapper } from './index.mjs'; describe('DBKVStore', async () => { @@ -18,10 +17,7 @@ describe('DBKVStore', async () => { }); const testKernel = await createTestKernel({ - serviceMap: { - meteringService: MeteringServiceWrapper, - 'puter-kvstore': DBKVServiceWrapper, - }, + serviceMap: {}, initLevelString: 'init', testCore: true, }); @@ -64,6 +60,29 @@ describe('DBKVStore', async () => { expect(fromTwo).toBe('two'); }); + it('retrieves single and multiple keys respecting app vs global scope', async () => { + const userId = 12; + const globalActor = makeActor(userId); + const appActor = makeActor(userId, 'scoped-app'); + + await su.sudo(globalActor, () => kvStore.set({ key: 'shared', value: 'global-shared' })); + await su.sudo(globalActor, () => kvStore.set({ key: 'global-only', value: 'global' })); + await su.sudo(appActor, () => kvStore.set({ key: 'shared', value: 'app-shared' })); + await su.sudo(appActor, () => kvStore.set({ key: 'app-only', value: 'app' })); + + const globalSingle = await su.sudo(globalActor, () => kvStore.get({ key: 'shared' })); + const appSingle = await su.sudo(appActor, () => kvStore.get({ key: 'shared' })); + + expect(globalSingle).toBe('global-shared'); + expect(appSingle).toBe('app-shared'); + + const globalList = await su.sudo(globalActor, () => kvStore.get({ key: ['shared', 'app-only', 'global-only'] })); + const appList = await su.sudo(appActor, () => kvStore.get({ key: ['shared', 'app-only', 'global-only'] })); + + expect(globalList).toEqual(['global-shared', null, 'global']); + expect(appList).toEqual(['app-shared', 'app', null]); + }); + it('increments nested numeric paths and persists the aggregated totals', async () => { const actor = makeActor(3); const key = 'counter-key'; diff --git a/src/backend/src/services/repositories/DBKVStore/DBKVStore.ts b/src/backend/src/services/repositories/DBKVStore/DBKVStore.ts index 760392380..3f05bdaac 100644 --- a/src/backend/src/services/repositories/DBKVStore/DBKVStore.ts +++ b/src/backend/src/services/repositories/DBKVStore/DBKVStore.ts @@ -53,10 +53,14 @@ export class DBKVStore implements IDBKVStore { if ( Array.isArray(key) ) { const keys = key; const key_hashes = keys.map((key: string) => murmurhash.v3(key)); + const placeholders = key_hashes.map(() => '?').join(','); + const params = app + ? [user.id, app.uid, ...key_hashes] + : [user.id, ...key_hashes]; const rows = app - ? await this.#db.read('SELECT kkey, value, expireAt FROM kv WHERE user_id=? AND app=? AND kkey_hash IN (?)', [user.id, app.uid, key_hashes]) - : await this.#db.read(`SELECT kkey, value, expireAt FROM kv WHERE user_id=? AND (app IS NULL OR app = '${GLOBAL_APP_KEY}') AND kkey_hash IN (${key_hashes.map(() => '?').join(',')})`, - [user.id, key_hashes]); + ? await this.#db.read(`SELECT kkey, value, expireAt FROM kv WHERE user_id=? AND app=? AND kkey_hash IN (${placeholders})`, params) + : await this.#db.read(`SELECT kkey, value, expireAt FROM kv WHERE user_id=? AND (app IS NULL OR app = '${GLOBAL_APP_KEY}') AND kkey_hash IN (${placeholders})`, + params); const kvPairs: Record = {}; rows.forEach((row: { kkey: string, value: string }) => { @@ -82,7 +86,7 @@ export class DBKVStore implements IDBKVStore { deleteExpired(expiredKeys); } - return keys.map((key: string) => kvPairs[key]) as unknown[]; + return keys.map((key: string) => Object.prototype.hasOwnProperty.call(kvPairs, key) ? kvPairs[key] : null) as unknown[]; } const key_hash = murmurhash.v3(key);