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.
@@ -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;