@revenium/openai 1.0.11 → 1.0.12
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/.env.example +20 -0
- package/CHANGELOG.md +21 -47
- package/README.md +141 -690
- package/dist/cjs/core/config/loader.js +1 -1
- package/dist/cjs/core/config/loader.js.map +1 -1
- package/dist/cjs/core/tracking/api-client.js +1 -1
- package/dist/cjs/core/tracking/api-client.js.map +1 -1
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/utils/url-builder.js +32 -7
- package/dist/cjs/utils/url-builder.js.map +1 -1
- package/dist/esm/core/config/loader.js +1 -1
- package/dist/esm/core/config/loader.js.map +1 -1
- package/dist/esm/core/tracking/api-client.js +1 -1
- package/dist/esm/core/tracking/api-client.js.map +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/utils/url-builder.js +32 -7
- package/dist/esm/utils/url-builder.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/types/index.d.ts +2 -2
- package/dist/types/types/index.d.ts.map +1 -1
- package/dist/types/utils/url-builder.d.ts +11 -3
- package/dist/types/utils/url-builder.d.ts.map +1 -1
- package/examples/README.md +250 -254
- package/examples/azure-basic.ts +25 -13
- package/examples/azure-responses-basic.ts +36 -7
- package/examples/azure-responses-streaming.ts +36 -7
- package/examples/azure-streaming.ts +40 -19
- package/examples/getting_started.ts +54 -0
- package/examples/openai-basic.ts +39 -17
- package/examples/openai-function-calling.ts +259 -0
- package/examples/openai-responses-basic.ts +36 -7
- package/examples/openai-responses-streaming.ts +36 -7
- package/examples/openai-streaming.ts +24 -13
- package/examples/openai-vision.ts +289 -0
- package/package.json +3 -9
- package/src/core/config/azure-config.ts +72 -0
- package/src/core/config/index.ts +23 -0
- package/src/core/config/loader.ts +66 -0
- package/src/core/config/manager.ts +94 -0
- package/src/core/config/validator.ts +89 -0
- package/src/core/providers/detector.ts +159 -0
- package/src/core/providers/index.ts +16 -0
- package/src/core/tracking/api-client.ts +78 -0
- package/src/core/tracking/index.ts +21 -0
- package/src/core/tracking/payload-builder.ts +132 -0
- package/src/core/tracking/usage-tracker.ts +189 -0
- package/src/core/wrapper/index.ts +9 -0
- package/src/core/wrapper/instance-patcher.ts +288 -0
- package/src/core/wrapper/request-handler.ts +423 -0
- package/src/core/wrapper/stream-wrapper.ts +100 -0
- package/src/index.ts +336 -0
- package/src/types/function-parameters.ts +251 -0
- package/src/types/index.ts +313 -0
- package/src/types/openai-augmentation.ts +233 -0
- package/src/types/responses-api.ts +308 -0
- package/src/utils/azure-model-resolver.ts +220 -0
- package/src/utils/constants.ts +21 -0
- package/src/utils/error-handler.ts +251 -0
- package/src/utils/metadata-builder.ts +219 -0
- package/src/utils/provider-detection.ts +257 -0
- package/src/utils/request-handler-factory.ts +285 -0
- package/src/utils/stop-reason-mapper.ts +74 -0
- package/src/utils/type-guards.ts +202 -0
- package/src/utils/url-builder.ts +68 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Vision Example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates how Revenium middleware seamlessly tracks GPT-4o vision API usage
|
|
5
|
+
* with multimodal inputs (text + images). Shows automatic tracking of:
|
|
6
|
+
* - Vision API calls with image URLs
|
|
7
|
+
* - Token usage including image processing tokens
|
|
8
|
+
* - Cost calculation for vision features
|
|
9
|
+
* - Different image detail levels (low, high, auto)
|
|
10
|
+
*
|
|
11
|
+
* All metadata fields are optional and work seamlessly with vision API!
|
|
12
|
+
*
|
|
13
|
+
* For complete metadata field reference, see:
|
|
14
|
+
* https://revenium.readme.io/reference/meter_ai_completion
|
|
15
|
+
*
|
|
16
|
+
* OpenAI Vision API Reference:
|
|
17
|
+
* https://platform.openai.com/docs/guides/vision
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import 'dotenv/config';
|
|
21
|
+
import { initializeReveniumFromEnv, patchOpenAIInstance } from '@revenium/openai';
|
|
22
|
+
import OpenAI from 'openai';
|
|
23
|
+
|
|
24
|
+
async function openAIVisionExample() {
|
|
25
|
+
console.log('🖼️ OpenAI Vision API with Revenium Tracking\n');
|
|
26
|
+
|
|
27
|
+
// Initialize Revenium middleware
|
|
28
|
+
const initResult = initializeReveniumFromEnv();
|
|
29
|
+
if (!initResult.success) {
|
|
30
|
+
console.error('❌ Failed to initialize Revenium:', initResult.message);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Create and patch OpenAI instance
|
|
35
|
+
const openai = patchOpenAIInstance(new OpenAI());
|
|
36
|
+
|
|
37
|
+
// Sample image URLs for demonstration
|
|
38
|
+
const imageUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg';
|
|
39
|
+
|
|
40
|
+
// Example 1: Basic vision request without metadata (automatic tracking)
|
|
41
|
+
console.log('📸 Example 1: Basic vision analysis (automatic tracking)\n');
|
|
42
|
+
|
|
43
|
+
const basicVisionResponse = await openai.chat.completions.create({
|
|
44
|
+
model: 'gpt-4o-mini',
|
|
45
|
+
messages: [
|
|
46
|
+
{
|
|
47
|
+
role: 'user',
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: 'text',
|
|
51
|
+
text: 'What is in this image? Describe it in one sentence.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'image_url',
|
|
55
|
+
image_url: {
|
|
56
|
+
url: imageUrl,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
max_tokens: 300,
|
|
63
|
+
// No usageMetadata - still automatically tracked!
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
console.log('AI Response:', basicVisionResponse.choices[0]?.message?.content);
|
|
67
|
+
console.log('Usage:', basicVisionResponse.usage);
|
|
68
|
+
console.log('✅ Vision API automatically tracked without metadata\n');
|
|
69
|
+
|
|
70
|
+
// Example 2: Vision with detailed image analysis and metadata
|
|
71
|
+
console.log('🔍 Example 2: High-detail vision analysis with comprehensive metadata\n');
|
|
72
|
+
|
|
73
|
+
const detailedVisionResponse = await openai.chat.completions.create({
|
|
74
|
+
model: 'gpt-4o',
|
|
75
|
+
messages: [
|
|
76
|
+
{
|
|
77
|
+
role: 'user',
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: 'text',
|
|
81
|
+
text: 'Please analyze this image in detail. Describe the scene, colors, composition, and any notable features.',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: 'image_url',
|
|
85
|
+
image_url: {
|
|
86
|
+
url: imageUrl,
|
|
87
|
+
detail: 'high', // High detail for better analysis (costs more tokens)
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
max_tokens: 500,
|
|
94
|
+
|
|
95
|
+
// ✨ All metadata fields are optional - perfect for tracking vision AI!
|
|
96
|
+
usageMetadata: {
|
|
97
|
+
subscriber: {
|
|
98
|
+
id: 'vision-user-123',
|
|
99
|
+
email: 'vision@company.com',
|
|
100
|
+
credential: {
|
|
101
|
+
name: 'api-key',
|
|
102
|
+
value: 'vision-key-456',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
organizationId: 'image-analysis-corp',
|
|
106
|
+
productId: 'ai-vision-platform',
|
|
107
|
+
subscriptionId: 'sub-vision-pro-789',
|
|
108
|
+
taskType: 'image-analysis-detailed',
|
|
109
|
+
traceId: `vision-${Date.now()}`,
|
|
110
|
+
agent: 'vision-analyzer-node',
|
|
111
|
+
responseQualityScore: 0.96,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
console.log('AI Detailed Analysis:', detailedVisionResponse.choices[0]?.message?.content);
|
|
116
|
+
console.log('Usage:', detailedVisionResponse.usage);
|
|
117
|
+
console.log('✅ Vision API tracked with rich metadata for image analytics\n');
|
|
118
|
+
|
|
119
|
+
// Example 3: Multiple images in one request
|
|
120
|
+
console.log('🖼️ 🖼️ Example 3: Multiple images with low-detail mode\n');
|
|
121
|
+
|
|
122
|
+
const multiImageResponse = await openai.chat.completions.create({
|
|
123
|
+
model: 'gpt-4o-mini',
|
|
124
|
+
messages: [
|
|
125
|
+
{
|
|
126
|
+
role: 'user',
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: 'text',
|
|
130
|
+
text: 'Compare these two images. What do they have in common?',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: 'image_url',
|
|
134
|
+
image_url: {
|
|
135
|
+
url: imageUrl,
|
|
136
|
+
detail: 'low', // Low detail for cost-effective processing
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: 'image_url',
|
|
141
|
+
image_url: {
|
|
142
|
+
url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Placeholder_view_vector.svg/310px-Placeholder_view_vector.svg.png',
|
|
143
|
+
detail: 'low',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
max_tokens: 300,
|
|
150
|
+
|
|
151
|
+
// ✨ Tracking multi-image requests
|
|
152
|
+
usageMetadata: {
|
|
153
|
+
subscriber: {
|
|
154
|
+
id: 'multi-vision-user-789',
|
|
155
|
+
email: 'multi@company.com',
|
|
156
|
+
credential: {
|
|
157
|
+
name: 'api-key',
|
|
158
|
+
value: 'multi-key-999',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
organizationId: 'comparison-ai-corp',
|
|
162
|
+
productId: 'image-comparison-tool',
|
|
163
|
+
subscriptionId: 'sub-comparison-basic-456',
|
|
164
|
+
taskType: 'multi-image-comparison',
|
|
165
|
+
traceId: `multi-vision-${Date.now()}`,
|
|
166
|
+
agent: 'comparison-analyzer-node',
|
|
167
|
+
responseQualityScore: 0.88,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
console.log('AI Comparison:', multiImageResponse.choices[0]?.message?.content);
|
|
172
|
+
console.log('Usage:', multiImageResponse.usage);
|
|
173
|
+
console.log('✅ Multiple images tracked with metadata for comparison analytics\n');
|
|
174
|
+
|
|
175
|
+
// Example 4: Vision with conversation context
|
|
176
|
+
console.log('💬 Example 4: Vision with multi-turn conversation\n');
|
|
177
|
+
|
|
178
|
+
const conversationMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
|
|
179
|
+
{
|
|
180
|
+
role: 'user',
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: 'text',
|
|
184
|
+
text: 'What colors are prominent in this image?',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
type: 'image_url',
|
|
188
|
+
image_url: {
|
|
189
|
+
url: imageUrl,
|
|
190
|
+
detail: 'auto', // Auto detail - OpenAI decides optimal level
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
const firstTurnResponse = await openai.chat.completions.create({
|
|
198
|
+
model: 'gpt-4o-mini',
|
|
199
|
+
messages: conversationMessages,
|
|
200
|
+
max_tokens: 200,
|
|
201
|
+
|
|
202
|
+
usageMetadata: {
|
|
203
|
+
subscriber: {
|
|
204
|
+
id: 'conversation-user-456',
|
|
205
|
+
email: 'conversation@company.com',
|
|
206
|
+
credential: {
|
|
207
|
+
name: 'api-key',
|
|
208
|
+
value: 'conv-key-123',
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
organizationId: 'chat-vision-corp',
|
|
212
|
+
productId: 'interactive-vision-assistant',
|
|
213
|
+
subscriptionId: 'sub-interactive-premium-321',
|
|
214
|
+
taskType: 'conversational-vision',
|
|
215
|
+
traceId: `conv-vision-${Date.now()}`,
|
|
216
|
+
agent: 'conversation-vision-node',
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
console.log('First Turn - AI:', firstTurnResponse.choices[0]?.message?.content);
|
|
221
|
+
console.log('Usage:', firstTurnResponse.usage);
|
|
222
|
+
|
|
223
|
+
// Add AI response to conversation
|
|
224
|
+
conversationMessages.push({
|
|
225
|
+
role: 'assistant',
|
|
226
|
+
content: firstTurnResponse.choices[0]?.message?.content || '',
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Follow-up question without image
|
|
230
|
+
conversationMessages.push({
|
|
231
|
+
role: 'user',
|
|
232
|
+
content: 'Based on those colors, what mood does the image convey?',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const secondTurnResponse = await openai.chat.completions.create({
|
|
236
|
+
model: 'gpt-4o-mini',
|
|
237
|
+
messages: conversationMessages,
|
|
238
|
+
max_tokens: 200,
|
|
239
|
+
|
|
240
|
+
usageMetadata: {
|
|
241
|
+
subscriber: {
|
|
242
|
+
id: 'conversation-user-456',
|
|
243
|
+
email: 'conversation@company.com',
|
|
244
|
+
credential: {
|
|
245
|
+
name: 'api-key',
|
|
246
|
+
value: 'conv-key-123',
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
organizationId: 'chat-vision-corp',
|
|
250
|
+
productId: 'interactive-vision-assistant',
|
|
251
|
+
subscriptionId: 'sub-interactive-premium-321',
|
|
252
|
+
taskType: 'conversational-vision-followup',
|
|
253
|
+
traceId: `conv-vision-${Date.now()}`,
|
|
254
|
+
agent: 'conversation-vision-node',
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
console.log('Second Turn - AI:', secondTurnResponse.choices[0]?.message?.content);
|
|
259
|
+
console.log('Usage:', secondTurnResponse.usage);
|
|
260
|
+
console.log('✅ Multi-turn vision conversation fully tracked\n');
|
|
261
|
+
|
|
262
|
+
// Summary
|
|
263
|
+
console.log('📈 Vision API Summary:');
|
|
264
|
+
console.log('✅ Image analysis with URLs fully supported');
|
|
265
|
+
console.log('✅ Token usage tracked including image processing tokens');
|
|
266
|
+
console.log('✅ Multiple images in one request work seamlessly');
|
|
267
|
+
console.log('✅ Detail levels (low, high, auto) all supported');
|
|
268
|
+
console.log('✅ Multi-turn conversations with image context tracked');
|
|
269
|
+
console.log('✅ All metadata fields optional and work perfectly');
|
|
270
|
+
console.log('✅ Cost calculation includes vision-specific tokens');
|
|
271
|
+
console.log('✅ No type casting required - native TypeScript support\n');
|
|
272
|
+
|
|
273
|
+
console.log('💡 Use Cases:');
|
|
274
|
+
console.log(' - Image content moderation and analysis');
|
|
275
|
+
console.log(' - Product catalog image descriptions');
|
|
276
|
+
console.log(' - Document and diagram understanding');
|
|
277
|
+
console.log(' - Visual question answering systems');
|
|
278
|
+
console.log(' - Accessibility tools (image descriptions)');
|
|
279
|
+
console.log(' - Quality control and inspection automation\n');
|
|
280
|
+
|
|
281
|
+
console.log('💰 Cost Optimization Tips:');
|
|
282
|
+
console.log(' - Use "low" detail for simple images to save tokens');
|
|
283
|
+
console.log(' - Use "high" detail only when fine details matter');
|
|
284
|
+
console.log(' - Use "auto" to let OpenAI optimize automatically');
|
|
285
|
+
console.log(' - Revenium tracks all token usage for accurate cost analytics');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Run the example
|
|
289
|
+
openAIVisionExample().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenium/openai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "Transparent TypeScript middleware for automatic Revenium usage tracking with OpenAI",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -25,10 +25,6 @@
|
|
|
25
25
|
"dev": "tsc --watch",
|
|
26
26
|
"clean": "node -e \"const fs = require('fs'); if (fs.existsSync('dist')) fs.rmSync('dist', {recursive: true, force: true})\"",
|
|
27
27
|
"prepublishOnly": "npm run clean && npm run build",
|
|
28
|
-
"lint": "eslint src/**/*.ts",
|
|
29
|
-
"lint:fix": "eslint src/**/*.ts --fix",
|
|
30
|
-
"format": "prettier --write src/**/*.ts",
|
|
31
|
-
"format:check": "prettier --check src/**/*.ts",
|
|
32
28
|
"example:openai-basic": "npm run build && npx ts-node examples/openai-basic.ts",
|
|
33
29
|
"example:openai-streaming": "npm run build && npx ts-node examples/openai-streaming.ts",
|
|
34
30
|
"example:azure-basic": "npm run build && npx ts-node examples/azure-basic.ts",
|
|
@@ -61,11 +57,7 @@
|
|
|
61
57
|
},
|
|
62
58
|
"devDependencies": {
|
|
63
59
|
"@types/node": "^20.0.0",
|
|
64
|
-
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
65
|
-
"@typescript-eslint/parser": "^6.21.0",
|
|
66
60
|
"dotenv": "^16.5.0",
|
|
67
|
-
"eslint": "^8.0.0",
|
|
68
|
-
"prettier": "^3.0.0",
|
|
69
61
|
"rimraf": "^6.0.1",
|
|
70
62
|
"ts-node": "^10.9.2",
|
|
71
63
|
"tsx": "^4.20.6",
|
|
@@ -76,7 +68,9 @@
|
|
|
76
68
|
},
|
|
77
69
|
"files": [
|
|
78
70
|
"dist/**/*",
|
|
71
|
+
"src/**/*",
|
|
79
72
|
"examples/**/*",
|
|
73
|
+
".env.example",
|
|
80
74
|
"README.md",
|
|
81
75
|
"CHANGELOG.md",
|
|
82
76
|
"LICENSE"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure Configuration Module
|
|
3
|
+
*
|
|
4
|
+
* Handles Azure-specific configuration logic.
|
|
5
|
+
* Extracted from provider.ts for better separation of concerns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AzureConfig } from '../../types/index.js';
|
|
9
|
+
import { OpenAIClientInstance } from '../../types/function-parameters.js';
|
|
10
|
+
import { getLogger } from './manager.js';
|
|
11
|
+
|
|
12
|
+
// Global logger
|
|
13
|
+
const logger = getLogger();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Gather Azure configuration from client and environment
|
|
17
|
+
*
|
|
18
|
+
* @param client - OpenAI/AzureOpenAI client instance
|
|
19
|
+
* @returns AzureConfig object
|
|
20
|
+
*/
|
|
21
|
+
export function gatherAzureConfig(client: OpenAIClientInstance): AzureConfig {
|
|
22
|
+
const config: AzureConfig = {};
|
|
23
|
+
try {
|
|
24
|
+
// Extract from client baseURL if available
|
|
25
|
+
if (client?.baseURL) {
|
|
26
|
+
const baseUrl =
|
|
27
|
+
typeof client.baseURL === 'string' ? client.baseURL : client.baseURL?.toString();
|
|
28
|
+
config.endpoint = baseUrl;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Extract from environment variables
|
|
32
|
+
if (process.env.AZURE_OPENAI_ENDPOINT) {
|
|
33
|
+
config.endpoint = process.env.AZURE_OPENAI_ENDPOINT;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (process.env.AZURE_OPENAI_DEPLOYMENT) {
|
|
37
|
+
config.deployment = process.env.AZURE_OPENAI_DEPLOYMENT;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (process.env.AZURE_OPENAI_API_VERSION) {
|
|
41
|
+
config.apiVersion = process.env.AZURE_OPENAI_API_VERSION;
|
|
42
|
+
} else {
|
|
43
|
+
// Default to preferred API version from Python learnings
|
|
44
|
+
config.apiVersion = '2024-12-01-preview';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (process.env.AZURE_OPENAI_API_KEY) {
|
|
48
|
+
config.apiKey = process.env.AZURE_OPENAI_API_KEY;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (process.env.AZURE_OPENAI_TENANT_ID) {
|
|
52
|
+
config.tenantId = process.env.AZURE_OPENAI_TENANT_ID;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (process.env.AZURE_OPENAI_RESOURCE_GROUP) {
|
|
56
|
+
config.resourceGroup = process.env.AZURE_OPENAI_RESOURCE_GROUP;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
logger.debug('Azure configuration gathered', {
|
|
60
|
+
hasEndpoint: !!config.endpoint,
|
|
61
|
+
hasDeployment: !!config.deployment,
|
|
62
|
+
hasApiKey: !!config.apiKey,
|
|
63
|
+
apiVersion: config.apiVersion,
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
logger.warn('Error gathering Azure configuration', {
|
|
67
|
+
error: error instanceof Error ? error.message : String(error),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return config;
|
|
72
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration module - Main exports
|
|
3
|
+
*
|
|
4
|
+
* This module provides a clean interface for configuration management,
|
|
5
|
+
* separating concerns into focused sub-modules.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export all configuration functionality
|
|
9
|
+
export { loadConfigFromEnv, loadAzureConfigFromEnv, hasAzureConfigInEnv } from './loader.js';
|
|
10
|
+
|
|
11
|
+
export { validateConfig, validateAzureConfig } from './validator.js';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
getConfig,
|
|
15
|
+
setConfig,
|
|
16
|
+
getLogger,
|
|
17
|
+
setLogger,
|
|
18
|
+
initializeConfig,
|
|
19
|
+
defaultLogger,
|
|
20
|
+
} from './manager.js';
|
|
21
|
+
|
|
22
|
+
// Export Azure-specific configuration
|
|
23
|
+
export * from './azure-config.js';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Loader Module
|
|
3
|
+
*
|
|
4
|
+
* Handles loading configuration from environment variables.
|
|
5
|
+
* Separated from validation and management for single responsibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ReveniumConfig, AzureConfig } from '../../types/index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default Revenium base URL for consistency with other middleware
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_REVENIUM_BASE_URL = 'https://api.revenium.io';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load configuration from environment variables
|
|
17
|
+
*/
|
|
18
|
+
export function loadConfigFromEnv(): ReveniumConfig | null {
|
|
19
|
+
const reveniumApiKey = process.env.REVENIUM_METERING_API_KEY || process.env.REVENIUM_API_KEY;
|
|
20
|
+
const reveniumBaseUrl =
|
|
21
|
+
process.env.REVENIUM_METERING_BASE_URL ||
|
|
22
|
+
process.env.REVENIUM_BASE_URL ||
|
|
23
|
+
DEFAULT_REVENIUM_BASE_URL;
|
|
24
|
+
const openaiApiKey = process.env.OPENAI_API_KEY;
|
|
25
|
+
|
|
26
|
+
if (!reveniumApiKey) return null;
|
|
27
|
+
return {
|
|
28
|
+
reveniumApiKey,
|
|
29
|
+
reveniumBaseUrl,
|
|
30
|
+
openaiApiKey,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load Azure configuration from environment variables
|
|
36
|
+
*/
|
|
37
|
+
export function loadAzureConfigFromEnv(): AzureConfig | null {
|
|
38
|
+
const endpoint = process.env.AZURE_OPENAI_ENDPOINT;
|
|
39
|
+
const deployment = process.env.AZURE_OPENAI_DEPLOYMENT;
|
|
40
|
+
const apiVersion = process.env.AZURE_OPENAI_API_VERSION;
|
|
41
|
+
const apiKey = process.env.AZURE_OPENAI_API_KEY;
|
|
42
|
+
const tenantId = process.env.AZURE_OPENAI_TENANT_ID;
|
|
43
|
+
const resourceGroup = process.env.AZURE_OPENAI_RESOURCE_GROUP;
|
|
44
|
+
|
|
45
|
+
// Return null if no Azure config is present
|
|
46
|
+
if (!endpoint && !deployment && !apiKey) return null;
|
|
47
|
+
return {
|
|
48
|
+
endpoint,
|
|
49
|
+
deployment,
|
|
50
|
+
apiVersion: apiVersion || '2024-12-01-preview', // Default from Python learnings
|
|
51
|
+
apiKey,
|
|
52
|
+
tenantId,
|
|
53
|
+
resourceGroup,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if Azure configuration is available in environment
|
|
59
|
+
*/
|
|
60
|
+
export function hasAzureConfigInEnv(): boolean {
|
|
61
|
+
return !!(
|
|
62
|
+
process.env.AZURE_OPENAI_ENDPOINT ||
|
|
63
|
+
process.env.AZURE_OPENAI_DEPLOYMENT ||
|
|
64
|
+
process.env.AZURE_OPENAI_API_KEY
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Manager Module
|
|
3
|
+
*
|
|
4
|
+
* Handles global configuration state management and logging.
|
|
5
|
+
* Separated from loading and validation for single responsibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ReveniumConfig, Logger } from '../../types/index.js';
|
|
9
|
+
import { loadConfigFromEnv, hasAzureConfigInEnv } from './loader.js';
|
|
10
|
+
import { validateConfig } from './validator.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Default console logger implementation
|
|
14
|
+
*/
|
|
15
|
+
export const defaultLogger: Logger = {
|
|
16
|
+
debug: (message: string, ...args: unknown[]) => {
|
|
17
|
+
if (process.env.REVENIUM_DEBUG === 'true') {
|
|
18
|
+
console.debug(`[Revenium Debug] ${message}`, ...args);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
info: (message: string, ...args: unknown[]) => {
|
|
22
|
+
console.info(`[Revenium] ${message}`, ...args);
|
|
23
|
+
},
|
|
24
|
+
warn: (message: string, ...args: unknown[]) => {
|
|
25
|
+
console.warn(`[Revenium Warning] ${message}`, ...args);
|
|
26
|
+
},
|
|
27
|
+
error: (message: string, ...args: unknown[]) => {
|
|
28
|
+
console.error(`[Revenium Error] ${message}`, ...args);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Global configuration instance
|
|
34
|
+
*/
|
|
35
|
+
let globalConfig: ReveniumConfig | null = null;
|
|
36
|
+
let globalLogger: Logger = defaultLogger;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the current global configuration
|
|
40
|
+
*/
|
|
41
|
+
export function getConfig(): ReveniumConfig | null {
|
|
42
|
+
return globalConfig;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Set the global configuration
|
|
47
|
+
*/
|
|
48
|
+
export function setConfig(config: ReveniumConfig): void {
|
|
49
|
+
validateConfig(config);
|
|
50
|
+
globalConfig = config;
|
|
51
|
+
globalLogger.debug('Revenium configuration updated', {
|
|
52
|
+
baseUrl: config.reveniumBaseUrl,
|
|
53
|
+
hasApiKey: !!config.reveniumApiKey,
|
|
54
|
+
hasOpenAIKey: !!config.openaiApiKey,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the current logger
|
|
60
|
+
*/
|
|
61
|
+
export function getLogger(): Logger {
|
|
62
|
+
return globalLogger;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Set a custom logger
|
|
67
|
+
*/
|
|
68
|
+
export function setLogger(logger: Logger): void {
|
|
69
|
+
globalLogger = logger;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Initialize configuration from environment variables
|
|
74
|
+
*/
|
|
75
|
+
export function initializeConfig(): boolean {
|
|
76
|
+
const envConfig = loadConfigFromEnv();
|
|
77
|
+
if (envConfig) {
|
|
78
|
+
try {
|
|
79
|
+
setConfig(envConfig);
|
|
80
|
+
globalLogger.debug('Revenium middleware initialized from environment variables');
|
|
81
|
+
|
|
82
|
+
// Log Azure config availability for debugging
|
|
83
|
+
if (hasAzureConfigInEnv()) {
|
|
84
|
+
globalLogger.debug('Azure OpenAI configuration detected in environment');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
globalLogger.error('Failed to initialize Revenium configuration:', error);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Validator Module
|
|
3
|
+
*
|
|
4
|
+
* Handles validation of configuration objects.
|
|
5
|
+
* Separated from loading and management for single responsibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ReveniumConfig, AzureConfig } from '../../types/index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate Revenium configuration
|
|
12
|
+
*/
|
|
13
|
+
export function validateConfig(config: ReveniumConfig): void {
|
|
14
|
+
if (!config.reveniumApiKey) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'Revenium API key is required. Set REVENIUM_METERING_API_KEY environment variable or provide reveniumApiKey in config.'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!config.reveniumApiKey.startsWith('hak_')) {
|
|
21
|
+
throw new Error('Invalid Revenium API key format. Revenium API keys should start with "hak_"');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!config.reveniumBaseUrl) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'Revenium base URL is missing. This should not happen as a default URL should be provided.'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Validate URL format
|
|
31
|
+
try {
|
|
32
|
+
new URL(config.reveniumBaseUrl);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new Error(`Invalid Revenium base URL format: ${config.reveniumBaseUrl}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate Azure configuration completeness
|
|
40
|
+
*
|
|
41
|
+
* @param config - Azure configuration to validate
|
|
42
|
+
* @returns validation result with missing fields
|
|
43
|
+
*/
|
|
44
|
+
export function validateAzureConfig(config: AzureConfig): {
|
|
45
|
+
isValid: boolean;
|
|
46
|
+
missingFields: string[];
|
|
47
|
+
warnings: string[];
|
|
48
|
+
} {
|
|
49
|
+
const missingFields: string[] = [];
|
|
50
|
+
const warnings: string[] = [];
|
|
51
|
+
|
|
52
|
+
// Required fields for basic Azure OpenAI operation
|
|
53
|
+
if (!config.endpoint) {
|
|
54
|
+
missingFields.push('endpoint');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!config.apiKey) {
|
|
58
|
+
missingFields.push('apiKey');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Optional but recommended fields
|
|
62
|
+
if (!config.deployment) {
|
|
63
|
+
warnings.push('deployment name not specified - may need to be included in model parameter');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!config.apiVersion) {
|
|
67
|
+
warnings.push('API version not specified - using default 2024-12-01-preview');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validate endpoint format
|
|
71
|
+
if (config.endpoint) {
|
|
72
|
+
try {
|
|
73
|
+
new URL(config.endpoint);
|
|
74
|
+
if (!config.endpoint.toLowerCase().includes('azure')) {
|
|
75
|
+
warnings.push(
|
|
76
|
+
'endpoint does not contain "azure" - please verify this is an Azure OpenAI endpoint'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
missingFields.push('valid endpoint URL');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
isValid: missingFields.length === 0,
|
|
86
|
+
missingFields,
|
|
87
|
+
warnings,
|
|
88
|
+
};
|
|
89
|
+
}
|