llmflow 0.3.1
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 +142 -0
- package/bin/llmflow.js +91 -0
- package/db.js +857 -0
- package/logger.js +122 -0
- package/otlp-export.js +564 -0
- package/otlp-logs.js +238 -0
- package/otlp-metrics.js +300 -0
- package/otlp.js +398 -0
- package/package.json +62 -0
- package/pricing.fallback.json +58 -0
- package/pricing.js +154 -0
- package/providers/anthropic.js +195 -0
- package/providers/azure.js +159 -0
- package/providers/base.js +145 -0
- package/providers/cohere.js +225 -0
- package/providers/gemini.js +278 -0
- package/providers/index.js +130 -0
- package/providers/ollama.js +36 -0
- package/providers/openai-compatible.js +77 -0
- package/providers/openai.js +217 -0
- package/providers/passthrough.js +573 -0
- package/public/app.js +1484 -0
- package/public/index.html +367 -0
- package/public/style.css +1152 -0
- package/server.js +1222 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const BaseProvider = require('./base');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generic OpenAI-compatible provider.
|
|
5
|
+
* Used for Groq, Mistral, Together, etc.
|
|
6
|
+
*/
|
|
7
|
+
class OpenAICompatibleProvider extends BaseProvider {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
super();
|
|
10
|
+
this.name = config.name;
|
|
11
|
+
this.displayName = config.displayName || config.name;
|
|
12
|
+
this.hostname = config.hostname;
|
|
13
|
+
this.port = config.port || 443;
|
|
14
|
+
this.basePath = config.basePath || '';
|
|
15
|
+
this.extraHeaders = config.extraHeaders || {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getTarget(req) {
|
|
19
|
+
return {
|
|
20
|
+
hostname: this.hostname,
|
|
21
|
+
port: this.port,
|
|
22
|
+
path: this.basePath + req.path,
|
|
23
|
+
protocol: 'https'
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
transformRequestHeaders(headers, req) {
|
|
28
|
+
return {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
'Authorization': headers.authorization,
|
|
31
|
+
...this.extraHeaders
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Pre-configured providers
|
|
37
|
+
const GroqProvider = new OpenAICompatibleProvider({
|
|
38
|
+
name: 'groq',
|
|
39
|
+
displayName: 'Groq',
|
|
40
|
+
hostname: 'api.groq.com',
|
|
41
|
+
basePath: '/openai'
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const MistralProvider = new OpenAICompatibleProvider({
|
|
45
|
+
name: 'mistral',
|
|
46
|
+
displayName: 'Mistral AI',
|
|
47
|
+
hostname: 'api.mistral.ai'
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const TogetherProvider = new OpenAICompatibleProvider({
|
|
51
|
+
name: 'together',
|
|
52
|
+
displayName: 'Together AI',
|
|
53
|
+
hostname: 'api.together.xyz'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const PerplexityProvider = new OpenAICompatibleProvider({
|
|
57
|
+
name: 'perplexity',
|
|
58
|
+
displayName: 'Perplexity',
|
|
59
|
+
hostname: 'api.perplexity.ai',
|
|
60
|
+
basePath: '' // No /v1 prefix for perplexity
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const OpenRouterProvider = new OpenAICompatibleProvider({
|
|
64
|
+
name: 'openrouter',
|
|
65
|
+
displayName: 'OpenRouter',
|
|
66
|
+
hostname: 'openrouter.ai',
|
|
67
|
+
basePath: '/api'
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
OpenAICompatibleProvider,
|
|
72
|
+
GroqProvider,
|
|
73
|
+
MistralProvider,
|
|
74
|
+
TogetherProvider,
|
|
75
|
+
PerplexityProvider,
|
|
76
|
+
OpenRouterProvider
|
|
77
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
const BaseProvider = require('./base');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenAI provider - the reference implementation.
|
|
5
|
+
* Supports both Chat Completions (/v1/chat/completions) and Responses (/v1/responses) APIs.
|
|
6
|
+
* All OpenAI-compatible providers can extend this.
|
|
7
|
+
*/
|
|
8
|
+
class OpenAIProvider extends BaseProvider {
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
super();
|
|
11
|
+
this.name = 'openai';
|
|
12
|
+
this.displayName = 'OpenAI';
|
|
13
|
+
this.hostname = config.hostname || 'api.openai.com';
|
|
14
|
+
this.port = config.port || 443;
|
|
15
|
+
this.basePath = config.basePath || '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getTarget(req) {
|
|
19
|
+
return {
|
|
20
|
+
hostname: this.hostname,
|
|
21
|
+
port: this.port,
|
|
22
|
+
path: this.basePath + req.path,
|
|
23
|
+
protocol: 'https'
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
transformRequestHeaders(headers, req) {
|
|
28
|
+
return {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
'Authorization': headers.authorization
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if this is a Responses API request
|
|
36
|
+
*/
|
|
37
|
+
isResponsesAPI(req) {
|
|
38
|
+
return req.path.includes('/responses');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Normalize response - handles both Chat Completions and Responses API formats
|
|
43
|
+
*/
|
|
44
|
+
normalizeResponse(body, req) {
|
|
45
|
+
if (this.isResponsesAPI(req)) {
|
|
46
|
+
return this.normalizeResponsesAPIResponse(body, req);
|
|
47
|
+
}
|
|
48
|
+
return super.normalizeResponse(body, req);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Normalize Responses API response to common format for logging
|
|
53
|
+
*/
|
|
54
|
+
normalizeResponsesAPIResponse(body, req) {
|
|
55
|
+
if (!body || body.error) {
|
|
56
|
+
return { data: body, usage: null, model: req.body?.model };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Extract text content from output items
|
|
60
|
+
let textContent = '';
|
|
61
|
+
if (Array.isArray(body.output)) {
|
|
62
|
+
for (const item of body.output) {
|
|
63
|
+
if (item.type === 'message' && Array.isArray(item.content)) {
|
|
64
|
+
for (const content of item.content) {
|
|
65
|
+
if (content.type === 'output_text') {
|
|
66
|
+
textContent += content.text || '';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Also check output_text helper if available
|
|
74
|
+
if (!textContent && body.output_text) {
|
|
75
|
+
textContent = body.output_text;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const usage = body.usage || {};
|
|
79
|
+
const normalizedUsage = {
|
|
80
|
+
prompt_tokens: usage.input_tokens || 0,
|
|
81
|
+
completion_tokens: usage.output_tokens || 0,
|
|
82
|
+
total_tokens: usage.total_tokens || (usage.input_tokens || 0) + (usage.output_tokens || 0)
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
data: body,
|
|
87
|
+
usage: normalizedUsage,
|
|
88
|
+
model: body.model || req.body?.model || 'unknown',
|
|
89
|
+
// Store extracted text for easier access
|
|
90
|
+
_extractedContent: textContent
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract usage from response - handles both API formats
|
|
96
|
+
*/
|
|
97
|
+
extractUsage(response) {
|
|
98
|
+
const usage = response.usage || {};
|
|
99
|
+
|
|
100
|
+
// Responses API uses input_tokens/output_tokens
|
|
101
|
+
if (usage.input_tokens !== undefined) {
|
|
102
|
+
return {
|
|
103
|
+
prompt_tokens: usage.input_tokens || 0,
|
|
104
|
+
completion_tokens: usage.output_tokens || 0,
|
|
105
|
+
total_tokens: usage.total_tokens || (usage.input_tokens || 0) + (usage.output_tokens || 0)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Chat Completions API uses prompt_tokens/completion_tokens
|
|
110
|
+
return {
|
|
111
|
+
prompt_tokens: usage.prompt_tokens || 0,
|
|
112
|
+
completion_tokens: usage.completion_tokens || 0,
|
|
113
|
+
total_tokens: usage.total_tokens || (usage.prompt_tokens || 0) + (usage.completion_tokens || 0)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parse streaming chunks - handles both API formats
|
|
119
|
+
*/
|
|
120
|
+
parseStreamChunk(chunk) {
|
|
121
|
+
const lines = chunk.split('\n');
|
|
122
|
+
let content = '';
|
|
123
|
+
let usage = null;
|
|
124
|
+
let done = false;
|
|
125
|
+
|
|
126
|
+
for (const line of lines) {
|
|
127
|
+
const trimmed = line.trim();
|
|
128
|
+
|
|
129
|
+
// Handle event: lines for Responses API
|
|
130
|
+
if (trimmed.startsWith('event:')) {
|
|
131
|
+
const eventType = trimmed.slice(6).trim();
|
|
132
|
+
if (eventType === 'response.done' || eventType === 'done') {
|
|
133
|
+
done = true;
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!trimmed.startsWith('data:')) continue;
|
|
139
|
+
|
|
140
|
+
const payload = trimmed.slice(5).trim();
|
|
141
|
+
if (payload === '[DONE]') {
|
|
142
|
+
done = true;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const json = JSON.parse(payload);
|
|
148
|
+
|
|
149
|
+
// Chat Completions format
|
|
150
|
+
if (json.choices?.[0]?.delta?.content) {
|
|
151
|
+
content += json.choices[0].delta.content;
|
|
152
|
+
}
|
|
153
|
+
if (json.usage) {
|
|
154
|
+
usage = json.usage;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Responses API format
|
|
158
|
+
if (json.type === 'response.output_text.delta') {
|
|
159
|
+
content += json.delta || '';
|
|
160
|
+
}
|
|
161
|
+
if (json.type === 'response.done' && json.response?.usage) {
|
|
162
|
+
usage = {
|
|
163
|
+
prompt_tokens: json.response.usage.input_tokens || 0,
|
|
164
|
+
completion_tokens: json.response.usage.output_tokens || 0,
|
|
165
|
+
total_tokens: json.response.usage.total_tokens || 0
|
|
166
|
+
};
|
|
167
|
+
done = true;
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Ignore parse errors
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { content, usage, done };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Assemble streaming response - handles both API formats
|
|
179
|
+
*/
|
|
180
|
+
assembleStreamingResponse(fullContent, usage, req, traceId) {
|
|
181
|
+
const isResponses = this.isResponsesAPI(req);
|
|
182
|
+
|
|
183
|
+
if (isResponses) {
|
|
184
|
+
return {
|
|
185
|
+
id: traceId,
|
|
186
|
+
object: 'response',
|
|
187
|
+
model: req.body?.model,
|
|
188
|
+
output: [{
|
|
189
|
+
type: 'message',
|
|
190
|
+
role: 'assistant',
|
|
191
|
+
content: [{
|
|
192
|
+
type: 'output_text',
|
|
193
|
+
text: fullContent
|
|
194
|
+
}]
|
|
195
|
+
}],
|
|
196
|
+
output_text: fullContent,
|
|
197
|
+
usage: usage,
|
|
198
|
+
_streaming: true
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Chat Completions format
|
|
203
|
+
return {
|
|
204
|
+
id: traceId,
|
|
205
|
+
object: 'chat.completion',
|
|
206
|
+
model: req.body?.model,
|
|
207
|
+
choices: [{
|
|
208
|
+
message: { role: 'assistant', content: fullContent },
|
|
209
|
+
finish_reason: 'stop'
|
|
210
|
+
}],
|
|
211
|
+
usage: usage,
|
|
212
|
+
_streaming: true
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = OpenAIProvider;
|