cachegate 1.2.0 → 1.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/LICENSE +21 -21
- package/failover.js +76 -76
- package/metrics.js +52 -2
- package/package.json +1 -1
- package/providers/anthropic.js +122 -122
- package/providers/openai.js +114 -114
- package/public/dashboard.html +1119 -1119
- package/server.js +19 -0
- package/streaming.js +77 -77
package/providers/openai.js
CHANGED
|
@@ -1,114 +1,114 @@
|
|
|
1
|
-
// model-router/providers/openai.js
|
|
2
|
-
const { OpenAI } = require('openai');
|
|
3
|
-
|
|
4
|
-
function buildClient(apiKey) {
|
|
5
|
-
return new OpenAI({ apiKey });
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function estimateCost(model, inputTokens, outputTokens) {
|
|
9
|
-
// Approximate pricing per 1M tokens
|
|
10
|
-
const rates = {
|
|
11
|
-
'gpt-4o-mini': { input: 0.15, output: 0.6 },
|
|
12
|
-
'gpt-4o': { input: 2.5, output: 10.0 }
|
|
13
|
-
};
|
|
14
|
-
const rate = rates[model] || { input: 2.5, output: 10.0 };
|
|
15
|
-
return ((inputTokens * rate.input) + (outputTokens * rate.output)) / 1_000_000;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function chat(client, payload) {
|
|
19
|
-
const request = {
|
|
20
|
-
model: payload.model,
|
|
21
|
-
messages: payload.messages,
|
|
22
|
-
temperature: typeof payload.temperature === 'number' ? payload.temperature : 0.0,
|
|
23
|
-
max_tokens: payload.max_tokens || 1024,
|
|
24
|
-
...(payload.tools && { tools: payload.tools }),
|
|
25
|
-
...(payload.tool_choice && { tool_choice: payload.tool_choice }),
|
|
26
|
-
...(payload.response_format && { response_format: payload.response_format })
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const start = Date.now();
|
|
30
|
-
const response = await client.chat.completions.create(request);
|
|
31
|
-
const latencyMs = Date.now() - start;
|
|
32
|
-
|
|
33
|
-
const choice = response.choices[0];
|
|
34
|
-
const inputTokens = response.usage.prompt_tokens;
|
|
35
|
-
const outputTokens = response.usage.completion_tokens;
|
|
36
|
-
const costUsd = estimateCost(payload.model, inputTokens, outputTokens);
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
provider: 'openai',
|
|
40
|
-
model: payload.model,
|
|
41
|
-
latency_ms: latencyMs,
|
|
42
|
-
usage: { input_tokens: inputTokens, output_tokens: outputTokens },
|
|
43
|
-
cost_usd: costUsd,
|
|
44
|
-
content: choice.message.content || '',
|
|
45
|
-
tool_calls: choice.message.tool_calls,
|
|
46
|
-
raw: response
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Pure state-accumulation for one OpenAI streaming chunk - factored out
|
|
52
|
-
* from chatStream() so the usage/cost extraction is directly
|
|
53
|
-
* unit-testable with canned chunks, no live API needed. Mutates `state`
|
|
54
|
-
* ({content, inputTokens, outputTokens}) and calls onDelta() with each
|
|
55
|
-
* new piece of assistant text.
|
|
56
|
-
*
|
|
57
|
-
* OpenAI only includes `usage` on a final, choice-less chunk, and only
|
|
58
|
-
* when the request explicitly asked for it (`stream_options:
|
|
59
|
-
* {include_usage: true}`, set in chatStream() below) - without that
|
|
60
|
-
* flag a streamed OpenAI response has NO usage data at all, which would
|
|
61
|
-
* silently make cost_usd wrong (stuck at 0) for every streamed OpenAI
|
|
62
|
-
* call. Requesting it explicitly is required, not optional, for the
|
|
63
|
-
* cost tracking this whole project is built around to stay honest.
|
|
64
|
-
*/
|
|
65
|
-
function applyStreamChunk(state, chunk, onDelta) {
|
|
66
|
-
const choice = chunk.choices && chunk.choices[0];
|
|
67
|
-
if (choice && choice.delta && choice.delta.content) {
|
|
68
|
-
state.content += choice.delta.content;
|
|
69
|
-
onDelta(choice.delta.content);
|
|
70
|
-
}
|
|
71
|
-
if (chunk.usage) {
|
|
72
|
-
state.inputTokens = chunk.usage.prompt_tokens;
|
|
73
|
-
state.outputTokens = chunk.usage.completion_tokens;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Streaming counterpart to chat(). Scope: plain text content only - no
|
|
79
|
-
* tools/tool_choice forwarded (server.js rejects stream:true + tools
|
|
80
|
-
* before this is ever called; see streaming.js for why).
|
|
81
|
-
*/
|
|
82
|
-
async function chatStream(client, payload, { onDelta, signal } = {}) {
|
|
83
|
-
const request = {
|
|
84
|
-
model: payload.model,
|
|
85
|
-
messages: payload.messages,
|
|
86
|
-
temperature: typeof payload.temperature === 'number' ? payload.temperature : 0.0,
|
|
87
|
-
max_tokens: payload.max_tokens || 1024,
|
|
88
|
-
stream: true,
|
|
89
|
-
stream_options: { include_usage: true }
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const start = Date.now();
|
|
93
|
-
const stream = await client.chat.completions.create(request, signal ? { signal } : undefined);
|
|
94
|
-
|
|
95
|
-
const state = { content: '', inputTokens: 0, outputTokens: 0 };
|
|
96
|
-
for await (const chunk of stream) {
|
|
97
|
-
applyStreamChunk(state, chunk, onDelta || (() => {}));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const latencyMs = Date.now() - start;
|
|
101
|
-
const costUsd = estimateCost(payload.model, state.inputTokens, state.outputTokens);
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
provider: 'openai',
|
|
105
|
-
model: payload.model,
|
|
106
|
-
latency_ms: latencyMs,
|
|
107
|
-
usage: { input_tokens: state.inputTokens, output_tokens: state.outputTokens },
|
|
108
|
-
cost_usd: costUsd,
|
|
109
|
-
content: state.content,
|
|
110
|
-
tool_calls: undefined
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
module.exports = { buildClient, chat, chatStream, applyStreamChunk, estimateCost };
|
|
1
|
+
// model-router/providers/openai.js
|
|
2
|
+
const { OpenAI } = require('openai');
|
|
3
|
+
|
|
4
|
+
function buildClient(apiKey) {
|
|
5
|
+
return new OpenAI({ apiKey });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function estimateCost(model, inputTokens, outputTokens) {
|
|
9
|
+
// Approximate pricing per 1M tokens
|
|
10
|
+
const rates = {
|
|
11
|
+
'gpt-4o-mini': { input: 0.15, output: 0.6 },
|
|
12
|
+
'gpt-4o': { input: 2.5, output: 10.0 }
|
|
13
|
+
};
|
|
14
|
+
const rate = rates[model] || { input: 2.5, output: 10.0 };
|
|
15
|
+
return ((inputTokens * rate.input) + (outputTokens * rate.output)) / 1_000_000;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function chat(client, payload) {
|
|
19
|
+
const request = {
|
|
20
|
+
model: payload.model,
|
|
21
|
+
messages: payload.messages,
|
|
22
|
+
temperature: typeof payload.temperature === 'number' ? payload.temperature : 0.0,
|
|
23
|
+
max_tokens: payload.max_tokens || 1024,
|
|
24
|
+
...(payload.tools && { tools: payload.tools }),
|
|
25
|
+
...(payload.tool_choice && { tool_choice: payload.tool_choice }),
|
|
26
|
+
...(payload.response_format && { response_format: payload.response_format })
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const start = Date.now();
|
|
30
|
+
const response = await client.chat.completions.create(request);
|
|
31
|
+
const latencyMs = Date.now() - start;
|
|
32
|
+
|
|
33
|
+
const choice = response.choices[0];
|
|
34
|
+
const inputTokens = response.usage.prompt_tokens;
|
|
35
|
+
const outputTokens = response.usage.completion_tokens;
|
|
36
|
+
const costUsd = estimateCost(payload.model, inputTokens, outputTokens);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
provider: 'openai',
|
|
40
|
+
model: payload.model,
|
|
41
|
+
latency_ms: latencyMs,
|
|
42
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens },
|
|
43
|
+
cost_usd: costUsd,
|
|
44
|
+
content: choice.message.content || '',
|
|
45
|
+
tool_calls: choice.message.tool_calls,
|
|
46
|
+
raw: response
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Pure state-accumulation for one OpenAI streaming chunk - factored out
|
|
52
|
+
* from chatStream() so the usage/cost extraction is directly
|
|
53
|
+
* unit-testable with canned chunks, no live API needed. Mutates `state`
|
|
54
|
+
* ({content, inputTokens, outputTokens}) and calls onDelta() with each
|
|
55
|
+
* new piece of assistant text.
|
|
56
|
+
*
|
|
57
|
+
* OpenAI only includes `usage` on a final, choice-less chunk, and only
|
|
58
|
+
* when the request explicitly asked for it (`stream_options:
|
|
59
|
+
* {include_usage: true}`, set in chatStream() below) - without that
|
|
60
|
+
* flag a streamed OpenAI response has NO usage data at all, which would
|
|
61
|
+
* silently make cost_usd wrong (stuck at 0) for every streamed OpenAI
|
|
62
|
+
* call. Requesting it explicitly is required, not optional, for the
|
|
63
|
+
* cost tracking this whole project is built around to stay honest.
|
|
64
|
+
*/
|
|
65
|
+
function applyStreamChunk(state, chunk, onDelta) {
|
|
66
|
+
const choice = chunk.choices && chunk.choices[0];
|
|
67
|
+
if (choice && choice.delta && choice.delta.content) {
|
|
68
|
+
state.content += choice.delta.content;
|
|
69
|
+
onDelta(choice.delta.content);
|
|
70
|
+
}
|
|
71
|
+
if (chunk.usage) {
|
|
72
|
+
state.inputTokens = chunk.usage.prompt_tokens;
|
|
73
|
+
state.outputTokens = chunk.usage.completion_tokens;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Streaming counterpart to chat(). Scope: plain text content only - no
|
|
79
|
+
* tools/tool_choice forwarded (server.js rejects stream:true + tools
|
|
80
|
+
* before this is ever called; see streaming.js for why).
|
|
81
|
+
*/
|
|
82
|
+
async function chatStream(client, payload, { onDelta, signal } = {}) {
|
|
83
|
+
const request = {
|
|
84
|
+
model: payload.model,
|
|
85
|
+
messages: payload.messages,
|
|
86
|
+
temperature: typeof payload.temperature === 'number' ? payload.temperature : 0.0,
|
|
87
|
+
max_tokens: payload.max_tokens || 1024,
|
|
88
|
+
stream: true,
|
|
89
|
+
stream_options: { include_usage: true }
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const start = Date.now();
|
|
93
|
+
const stream = await client.chat.completions.create(request, signal ? { signal } : undefined);
|
|
94
|
+
|
|
95
|
+
const state = { content: '', inputTokens: 0, outputTokens: 0 };
|
|
96
|
+
for await (const chunk of stream) {
|
|
97
|
+
applyStreamChunk(state, chunk, onDelta || (() => {}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const latencyMs = Date.now() - start;
|
|
101
|
+
const costUsd = estimateCost(payload.model, state.inputTokens, state.outputTokens);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
provider: 'openai',
|
|
105
|
+
model: payload.model,
|
|
106
|
+
latency_ms: latencyMs,
|
|
107
|
+
usage: { input_tokens: state.inputTokens, output_tokens: state.outputTokens },
|
|
108
|
+
cost_usd: costUsd,
|
|
109
|
+
content: state.content,
|
|
110
|
+
tool_calls: undefined
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { buildClient, chat, chatStream, applyStreamChunk, estimateCost };
|