@robhowley/pi-openrouter 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -3
- package/extensions/openrouter/__tests__/client.test.ts +169 -0
- package/extensions/openrouter/__tests__/fixtures.ts +134 -0
- package/extensions/openrouter/__tests__/format.test.ts +25 -59
- package/extensions/openrouter/__tests__/session.test.ts +309 -69
- package/extensions/openrouter/client.ts +70 -12
- package/extensions/openrouter/index.ts +138 -18
- package/extensions/openrouter/models/__tests__/cache.test.ts +140 -0
- package/extensions/openrouter/models/__tests__/mapper.test.ts +221 -0
- package/extensions/openrouter/models/__tests__/sync.test.ts +313 -0
- package/extensions/openrouter/models/cache.ts +105 -0
- package/extensions/openrouter/models/mapper.ts +182 -0
- package/extensions/openrouter/models/sync.ts +312 -0
- package/extensions/openrouter/models/types.ts +148 -0
- package/extensions/openrouter/session.ts +20 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-openrouter
|
|
2
2
|
|
|
3
|
-
A [Pi](https://pi.dev/) extension for live OpenRouter visibility
|
|
3
|
+
A [Pi](https://pi.dev/) extension for live OpenRouter visibility and environment sync: usage/account TUI overlays, automatic `session_id` tagging, and user-scoped model catalog sync.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -13,13 +13,53 @@ pi install npm:@robhowley/pi-openrouter
|
|
|
13
13
|
Set one of these environment variables:
|
|
14
14
|
|
|
15
15
|
- `OPENROUTER_MANAGEMENT_KEY` (preferred), provides full usage data including model breakdowns
|
|
16
|
-
- `OPENROUTER_API_KEY`, basic usage data
|
|
16
|
+
- `OPENROUTER_API_KEY`, basic usage data plus user-scoped model sync
|
|
17
17
|
|
|
18
18
|
```shell
|
|
19
19
|
export OPENROUTER_MANAGEMENT_KEY=sk-or-...
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
/openrouter usage # usage/spend overlay
|
|
26
|
+
/openrouter account # credits, key limits, account health
|
|
27
|
+
/openrouter session # current OpenRouter session_id
|
|
28
|
+
/openrouter models-sync # sync user-scoped OpenRouter models into Pi
|
|
29
|
+
/openrouter models-status # show model sync/cache status
|
|
30
|
+
/openrouter models-status --skipped # show skipped model reasons
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Model catalog sync
|
|
34
|
+
|
|
35
|
+
`pi-openrouter` can sync Pi’s OpenRouter model catalog from your user-scoped OpenRouter model list.
|
|
36
|
+
|
|
37
|
+
`/openrouter models-sync`
|
|
38
|
+
|
|
39
|
+
The sync uses OpenRouter’s authenticated user model catalog, so Pi can see the models available to your account instead of only the default provider list.
|
|
40
|
+
|
|
41
|
+
`/openrouter models-status`
|
|
42
|
+
|
|
43
|
+
Example status output:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
OpenRouter models healthy
|
|
47
|
+
363 registered · 2 skipped · cache age: 2m
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
To see why models were skipped:
|
|
51
|
+
|
|
52
|
+
`/openrouter models-status --skipped`
|
|
53
|
+
|
|
54
|
+
Skipped models do not make the sync fail; models are skipped when required metadata cannot be safely mapped into Pi’s provider model config. The last successful catalog is cached so Pi can keep using it if a later refresh fails, and the cache persists across sessions. If a session starts with a cached catalog that has not been registered yet, status will show:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
OpenRouter models cached
|
|
58
|
+
368 models in cache · age: 4m
|
|
59
|
+
Run '/openrouter models-sync' to register models
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Usage overlay
|
|
23
63
|
|
|
24
64
|
Type `/openrouter usage` in Pi to open the usage overlay.
|
|
25
65
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { fetchUserModels, isConfigured, getApiKey, ApiError, AuthError } from '../client.js';
|
|
3
|
+
import { restoreEnv } from './fixtures.js';
|
|
4
|
+
import type { Mock } from 'vitest';
|
|
5
|
+
|
|
6
|
+
// Mock SDK at the module level
|
|
7
|
+
vi.mock('@openrouter/sdk/sdk/sdk.js', () => ({
|
|
8
|
+
OpenRouter: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Factory function to create a minimal mock SDK client.
|
|
13
|
+
* Reduces boilerplate from ~25 lines to 1 line per test.
|
|
14
|
+
*/
|
|
15
|
+
function createMockSDKClient(overrides: { listForUser?: Mock } = {}) {
|
|
16
|
+
return {
|
|
17
|
+
models: { listForUser: overrides.listForUser ?? vi.fn() },
|
|
18
|
+
credits: {},
|
|
19
|
+
analytics: {},
|
|
20
|
+
chat: {},
|
|
21
|
+
embeddings: {},
|
|
22
|
+
images: {},
|
|
23
|
+
fine_tuning: {},
|
|
24
|
+
batches: {},
|
|
25
|
+
files: {},
|
|
26
|
+
audio: {},
|
|
27
|
+
moderation: {},
|
|
28
|
+
beta: {},
|
|
29
|
+
webhooks: {},
|
|
30
|
+
fineTunes: {},
|
|
31
|
+
jobs: {},
|
|
32
|
+
uploads: {},
|
|
33
|
+
assistants: {},
|
|
34
|
+
threads: {},
|
|
35
|
+
runs: {},
|
|
36
|
+
messages: {},
|
|
37
|
+
vectorStores: {},
|
|
38
|
+
tools: {},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('fetchUserModels', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.resetAllMocks();
|
|
45
|
+
restoreEnv();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should throw AuthError when API key not set', async () => {
|
|
49
|
+
delete process.env['OPENROUTER_API_KEY'];
|
|
50
|
+
|
|
51
|
+
await expect(fetchUserModels()).rejects.toThrow('OPENROUTER_API_KEY not set');
|
|
52
|
+
await expect(fetchUserModels()).rejects.toBeInstanceOf(AuthError);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should fetch models with SDK', async () => {
|
|
56
|
+
process.env['OPENROUTER_API_KEY'] = 'test-key';
|
|
57
|
+
|
|
58
|
+
const mockResponse = {
|
|
59
|
+
data: [
|
|
60
|
+
{
|
|
61
|
+
id: 'openai/gpt-4',
|
|
62
|
+
name: 'GPT-4',
|
|
63
|
+
context_length: 8192,
|
|
64
|
+
pricing: { prompt: '0.00003', completion: '0.00006' },
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Mock SDK OpenRouter class
|
|
70
|
+
const MockOpenRouter = vi.mocked((await import('@openrouter/sdk/sdk/sdk.js')).OpenRouter);
|
|
71
|
+
const mockClient = createMockSDKClient({
|
|
72
|
+
listForUser: vi.fn().mockResolvedValue(mockResponse),
|
|
73
|
+
});
|
|
74
|
+
MockOpenRouter.mockImplementation(() => mockClient as any);
|
|
75
|
+
|
|
76
|
+
const result = await fetchUserModels();
|
|
77
|
+
|
|
78
|
+
expect(result!.data).toHaveLength(1);
|
|
79
|
+
expect(result!.data[0]!.id).toBe('openai/gpt-4');
|
|
80
|
+
expect(mockClient.models.listForUser).toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should throw ApiError on 401 unauthorized', async () => {
|
|
84
|
+
process.env['OPENROUTER_API_KEY'] = 'invalid-key';
|
|
85
|
+
|
|
86
|
+
const MockOpenRouter = vi.mocked((await import('@openrouter/sdk/sdk/sdk.js')).OpenRouter);
|
|
87
|
+
const mockClient = createMockSDKClient({
|
|
88
|
+
listForUser: vi.fn().mockRejectedValue(new Error('Unauthorized')),
|
|
89
|
+
});
|
|
90
|
+
MockOpenRouter.mockImplementation(() => mockClient as any);
|
|
91
|
+
|
|
92
|
+
const error = await fetchUserModels().catch((e) => e);
|
|
93
|
+
expect(error).toBeInstanceOf(ApiError);
|
|
94
|
+
expect(error.message).toContain('Unauthorized');
|
|
95
|
+
expect((error as ApiError).statusCode).toBe(401);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should throw ApiError on rate limit (429)', async () => {
|
|
99
|
+
process.env['OPENROUTER_API_KEY'] = 'test-key';
|
|
100
|
+
|
|
101
|
+
const MockOpenRouter = vi.mocked((await import('@openrouter/sdk/sdk/sdk.js')).OpenRouter);
|
|
102
|
+
const mockClient = createMockSDKClient({
|
|
103
|
+
listForUser: vi.fn().mockRejectedValue(new Error('Rate limited')),
|
|
104
|
+
});
|
|
105
|
+
MockOpenRouter.mockImplementation(() => mockClient as any);
|
|
106
|
+
|
|
107
|
+
const error = await fetchUserModels().catch((e) => e);
|
|
108
|
+
expect(error).toBeInstanceOf(ApiError);
|
|
109
|
+
expect(error.message).toContain('Rate limited');
|
|
110
|
+
expect((error as ApiError).statusCode).toBe(429);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should throw ApiError on server error', async () => {
|
|
114
|
+
process.env['OPENROUTER_API_KEY'] = 'test-key';
|
|
115
|
+
|
|
116
|
+
const MockOpenRouter = vi.mocked((await import('@openrouter/sdk/sdk/sdk.js')).OpenRouter);
|
|
117
|
+
const mockClient = createMockSDKClient({
|
|
118
|
+
listForUser: vi.fn().mockRejectedValue(new Error('Server error')),
|
|
119
|
+
});
|
|
120
|
+
MockOpenRouter.mockImplementation(() => mockClient as any);
|
|
121
|
+
|
|
122
|
+
const error = await fetchUserModels().catch((e) => e);
|
|
123
|
+
expect(error).toBeInstanceOf(ApiError);
|
|
124
|
+
expect(error.message).toContain('Server error');
|
|
125
|
+
expect((error as ApiError).statusCode).toBe(500);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('isConfigured', () => {
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
restoreEnv();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should return true when API key is set', () => {
|
|
135
|
+
process.env['OPENROUTER_API_KEY'] = 'test-key';
|
|
136
|
+
expect(isConfigured()).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should return false when API key is not set', () => {
|
|
140
|
+
delete process.env['OPENROUTER_API_KEY'];
|
|
141
|
+
expect(isConfigured()).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should return false when API key is empty string', () => {
|
|
145
|
+
process.env['OPENROUTER_API_KEY'] = '';
|
|
146
|
+
expect(isConfigured()).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('getApiKey', () => {
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
restoreEnv();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should return the API key when set', () => {
|
|
156
|
+
process.env['OPENROUTER_API_KEY'] = 'test-key';
|
|
157
|
+
expect(getApiKey()).toBe('test-key');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should return undefined when API key is not set', () => {
|
|
161
|
+
delete process.env['OPENROUTER_API_KEY'];
|
|
162
|
+
expect(getApiKey()).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should return empty string when API key is empty', () => {
|
|
166
|
+
process.env['OPENROUTER_API_KEY'] = '';
|
|
167
|
+
expect(getApiKey()).toBe('');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test fixtures for OpenRouter extension tests.
|
|
3
|
+
*
|
|
4
|
+
* Centralizes common test data shapes to reduce duplication across test files.
|
|
5
|
+
* Import: import { createValidModel, createActivityItem, ... } from './fixtures.js'
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OpenRouterModel, ModelsCache, PiModelConfig } from '../models/types.js';
|
|
9
|
+
import type { ActivityItem } from '@openrouter/sdk/models/index.js';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Environment Helpers
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Snapshot of process.env captured at module load time.
|
|
17
|
+
* Used as the baseline for restoreEnv() in tests.
|
|
18
|
+
*/
|
|
19
|
+
const originalEnvSnapshot: Record<string, string | undefined> = { ...process.env };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Restore process.env to its original state.
|
|
23
|
+
* Call in beforeEach or afterEach to ensure clean environment between tests.
|
|
24
|
+
*/
|
|
25
|
+
export function restoreEnv(): void {
|
|
26
|
+
process.env = { ...originalEnvSnapshot };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Clear a specific environment variable.
|
|
31
|
+
*/
|
|
32
|
+
export function clearEnv(key: string): void {
|
|
33
|
+
delete process.env[key];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Set an environment variable for testing.
|
|
38
|
+
*/
|
|
39
|
+
export function setEnv(key: string, value: string): void {
|
|
40
|
+
process.env[key] = value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Model Fixtures
|
|
45
|
+
// =============================================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a valid OpenRouterModel with sensible defaults.
|
|
49
|
+
* Use overrides to customize specific properties for test cases.
|
|
50
|
+
*/
|
|
51
|
+
export function createValidModel(overrides?: Partial<OpenRouterModel>): OpenRouterModel {
|
|
52
|
+
return {
|
|
53
|
+
id: 'test/model',
|
|
54
|
+
name: 'Test Model',
|
|
55
|
+
context_length: 128000,
|
|
56
|
+
pricing: {
|
|
57
|
+
prompt: '0.0000005',
|
|
58
|
+
completion: '0.0000015',
|
|
59
|
+
},
|
|
60
|
+
...overrides,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Creates a valid ModelsCache with a single model.
|
|
66
|
+
*/
|
|
67
|
+
export function createMockCache(overrides: Partial<ModelsCache> = {}): ModelsCache {
|
|
68
|
+
return {
|
|
69
|
+
models: [createValidModel()],
|
|
70
|
+
timestamp: Date.now() - 1000, // 1 second ago
|
|
71
|
+
...overrides,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// Activity/Analytics Fixtures
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates a UTC date string (YYYY-MM-DD) relative to now.
|
|
81
|
+
* @param daysAgo - Days to subtract from current UTC date (0 for today)
|
|
82
|
+
* @returns Date string in YYYY-MM-DD format using UTC
|
|
83
|
+
*/
|
|
84
|
+
export function createTestDate(daysAgo: number): string {
|
|
85
|
+
const now = new Date();
|
|
86
|
+
const targetDate = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);
|
|
87
|
+
return `${targetDate.getUTCFullYear()}-${String(targetDate.getUTCMonth() + 1).padStart(2, '0')}-${String(targetDate.getUTCDate()).padStart(2, '0')}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Creates an ActivityItem with sensible defaults.
|
|
92
|
+
* Use overrides to customize specific properties for test cases.
|
|
93
|
+
*/
|
|
94
|
+
export function createActivityItem(overrides?: Partial<ActivityItem>): ActivityItem {
|
|
95
|
+
return {
|
|
96
|
+
date: createTestDate(0),
|
|
97
|
+
model: 'gpt-4',
|
|
98
|
+
modelPermaslug: 'gpt-4-perma',
|
|
99
|
+
endpointId: 'ep-1',
|
|
100
|
+
usage: 5.0,
|
|
101
|
+
byokUsageInference: 0,
|
|
102
|
+
requests: 10,
|
|
103
|
+
promptTokens: 1000,
|
|
104
|
+
completionTokens: 100,
|
|
105
|
+
reasoningTokens: 0,
|
|
106
|
+
providerName: 'openai',
|
|
107
|
+
...overrides,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// PiModelConfig Fixtures
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates a valid PiModelConfig with sensible defaults.
|
|
117
|
+
*/
|
|
118
|
+
export function createPiModelConfig(overrides?: Partial<PiModelConfig>): PiModelConfig {
|
|
119
|
+
return {
|
|
120
|
+
id: 'test/model',
|
|
121
|
+
name: 'Test Model',
|
|
122
|
+
reasoning: false,
|
|
123
|
+
input: ['text'],
|
|
124
|
+
cost: {
|
|
125
|
+
input: 0.5,
|
|
126
|
+
output: 1.5,
|
|
127
|
+
cacheRead: 0,
|
|
128
|
+
cacheWrite: 0,
|
|
129
|
+
},
|
|
130
|
+
contextWindow: 128000,
|
|
131
|
+
maxTokens: 4096,
|
|
132
|
+
...overrides,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { aggregateUsage } from '../format.js';
|
|
3
3
|
import { renderSpendSparkline } from '../chart.js';
|
|
4
|
+
import { createTestDate, createActivityItem } from './fixtures.js';
|
|
4
5
|
import type { ActivityItem } from '@openrouter/sdk/models/index.js';
|
|
5
6
|
|
|
6
7
|
describe('aggregateUsage', () => {
|
|
@@ -13,24 +14,9 @@ describe('aggregateUsage', () => {
|
|
|
13
14
|
// Get today's date in YYYY-MM-DD format using UTC date
|
|
14
15
|
// This matches how the API returns dates (YYYY-MM-DD without timezone)
|
|
15
16
|
// and how the implementation calculates 'today' (using UTC)
|
|
16
|
-
const
|
|
17
|
-
const todayStr = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-${String(now.getUTCDate()).padStart(2, '0')}`;
|
|
17
|
+
const todayStr = createTestDate(0);
|
|
18
18
|
|
|
19
|
-
const analytics: ActivityItem[] = [
|
|
20
|
-
{
|
|
21
|
-
date: todayStr,
|
|
22
|
-
model: 'gpt-4',
|
|
23
|
-
modelPermaslug: 'gpt-4-perma',
|
|
24
|
-
endpointId: 'ep-1',
|
|
25
|
-
usage: 6.55,
|
|
26
|
-
byokUsageInference: 0,
|
|
27
|
-
requests: 10,
|
|
28
|
-
promptTokens: 1000,
|
|
29
|
-
completionTokens: 100,
|
|
30
|
-
reasoningTokens: 0,
|
|
31
|
-
providerName: 'openai',
|
|
32
|
-
},
|
|
33
|
-
];
|
|
19
|
+
const analytics: ActivityItem[] = [createActivityItem({ date: todayStr, usage: 6.55 })];
|
|
34
20
|
|
|
35
21
|
const result = aggregateUsage(credits, analytics);
|
|
36
22
|
|
|
@@ -43,37 +29,27 @@ describe('aggregateUsage', () => {
|
|
|
43
29
|
totalCredits: 100,
|
|
44
30
|
};
|
|
45
31
|
// Use a date that's within the last 7 days of when the test runs.
|
|
46
|
-
|
|
47
|
-
const now = new Date();
|
|
48
|
-
const testDate = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000); // 3 days ago
|
|
49
|
-
const date = `${testDate.getUTCFullYear()}-${String(testDate.getUTCMonth() + 1).padStart(2, '0')}-${String(testDate.getUTCDate()).padStart(2, '0')}`;
|
|
32
|
+
const date = createTestDate(3); // 3 days ago
|
|
50
33
|
const analytics: ActivityItem[] = [
|
|
51
|
-
{
|
|
52
|
-
date
|
|
34
|
+
createActivityItem({
|
|
35
|
+
date,
|
|
53
36
|
model: 'model-1',
|
|
54
37
|
modelPermaslug: 'model-1-perma',
|
|
55
38
|
endpointId: 'ep-1',
|
|
56
39
|
usage: 5.42,
|
|
57
|
-
byokUsageInference: 0,
|
|
58
|
-
requests: 10,
|
|
59
|
-
promptTokens: 1000,
|
|
60
|
-
completionTokens: 100,
|
|
61
|
-
reasoningTokens: 0,
|
|
62
40
|
providerName: 'provider-1',
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
date
|
|
41
|
+
}),
|
|
42
|
+
createActivityItem({
|
|
43
|
+
date,
|
|
66
44
|
model: 'model-2',
|
|
67
45
|
modelPermaslug: 'model-2-perma',
|
|
68
46
|
endpointId: 'ep-2',
|
|
69
47
|
usage: 3.11,
|
|
70
|
-
byokUsageInference: 0,
|
|
71
48
|
requests: 5,
|
|
72
49
|
promptTokens: 500,
|
|
73
50
|
completionTokens: 50,
|
|
74
|
-
reasoningTokens: 0,
|
|
75
51
|
providerName: 'provider-2',
|
|
76
|
-
},
|
|
52
|
+
}),
|
|
77
53
|
];
|
|
78
54
|
|
|
79
55
|
const result = aggregateUsage(credits, analytics);
|
|
@@ -119,35 +95,30 @@ describe('aggregateUsage', () => {
|
|
|
119
95
|
totalUsage: 10,
|
|
120
96
|
totalCredits: 100,
|
|
121
97
|
};
|
|
122
|
-
// Use a
|
|
123
|
-
const date =
|
|
98
|
+
// Use a dynamic date that's within the last 7 days
|
|
99
|
+
const date = createTestDate(3); // 3 days ago
|
|
124
100
|
const analytics: ActivityItem[] = [
|
|
125
|
-
{
|
|
126
|
-
date
|
|
101
|
+
createActivityItem({
|
|
102
|
+
date,
|
|
127
103
|
model: 'gpt-4',
|
|
128
104
|
modelPermaslug: 'gpt-4-perma',
|
|
129
|
-
endpointId: 'ep-1',
|
|
130
105
|
usage: 5.0,
|
|
131
|
-
byokUsageInference: 0,
|
|
132
106
|
requests: 5,
|
|
133
107
|
promptTokens: 100,
|
|
134
108
|
completionTokens: 50,
|
|
135
|
-
reasoningTokens: 0,
|
|
136
109
|
providerName: 'openai',
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
date
|
|
110
|
+
}),
|
|
111
|
+
createActivityItem({
|
|
112
|
+
date,
|
|
140
113
|
model: 'claude-3',
|
|
141
114
|
modelPermaslug: 'claude-3-perma',
|
|
142
115
|
endpointId: 'ep-2',
|
|
143
116
|
usage: 3.0,
|
|
144
|
-
byokUsageInference: 0,
|
|
145
117
|
requests: 3,
|
|
146
118
|
promptTokens: 60,
|
|
147
119
|
completionTokens: 30,
|
|
148
|
-
reasoningTokens: 0,
|
|
149
120
|
providerName: 'anthropic',
|
|
150
|
-
},
|
|
121
|
+
}),
|
|
151
122
|
];
|
|
152
123
|
|
|
153
124
|
const result = aggregateUsage(credits, analytics);
|
|
@@ -218,32 +189,27 @@ describe('aggregateUsage', () => {
|
|
|
218
189
|
};
|
|
219
190
|
const date = '2026-05-01';
|
|
220
191
|
const analytics: ActivityItem[] = [
|
|
221
|
-
{
|
|
222
|
-
date
|
|
192
|
+
createActivityItem({
|
|
193
|
+
date,
|
|
223
194
|
model: 'gpt-4',
|
|
224
195
|
modelPermaslug: 'gpt-4-perma',
|
|
225
|
-
endpointId: 'ep-1',
|
|
226
196
|
usage: 5.0,
|
|
227
|
-
byokUsageInference: 0,
|
|
228
197
|
requests: 5,
|
|
229
198
|
promptTokens: 100,
|
|
230
199
|
completionTokens: 50,
|
|
231
|
-
reasoningTokens: 0,
|
|
232
200
|
providerName: 'openai',
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
date
|
|
201
|
+
}),
|
|
202
|
+
createActivityItem({
|
|
203
|
+
date,
|
|
236
204
|
model: 'claude-3',
|
|
237
205
|
modelPermaslug: 'claude-3-perma',
|
|
238
206
|
endpointId: 'ep-2',
|
|
239
207
|
usage: 3.0,
|
|
240
|
-
byokUsageInference: 0,
|
|
241
208
|
requests: 3,
|
|
242
209
|
promptTokens: 60,
|
|
243
210
|
completionTokens: 30,
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
},
|
|
211
|
+
providerName: 'openai',
|
|
212
|
+
}),
|
|
247
213
|
];
|
|
248
214
|
|
|
249
215
|
const result = aggregateUsage(credits, analytics);
|