cachegate 1.0.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/.dockerignore +11 -0
- package/.env.example +112 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +25 -0
- package/.github/workflows/test.yml +63 -0
- package/CODE_OF_CONDUCT.md +66 -0
- package/CONTRIBUTING.md +94 -0
- package/Dockerfile +16 -0
- package/LICENSE +21 -0
- package/OPEN_SOURCE_ROADMAP.md +554 -0
- package/README.md +428 -0
- package/ROADMAP.md +281 -0
- package/SECURITY.md +39 -0
- package/cache.js +51 -0
- package/embeddings.js +32 -0
- package/failover.js +76 -0
- package/metrics.js +556 -0
- package/package.json +26 -0
- package/providers/anthropic.js +122 -0
- package/providers/openai.js +114 -0
- package/public/dashboard.html +1119 -0
- package/redisClient.js +45 -0
- package/router.js +218 -0
- package/semanticCache.js +154 -0
- package/server.js +668 -0
- package/streaming.js +77 -0
- package/sync-oss-release.sh +160 -0
- package/test/auth-config.test.js +27 -0
- package/test/cache.test.js +33 -0
- package/test/embeddings.test.js +24 -0
- package/test/failover.test.js +99 -0
- package/test/metrics-postgres.test.js +183 -0
- package/test/metrics.test.js +282 -0
- package/test/router.test.js +195 -0
- package/test/semanticCache.test.js +167 -0
- package/test/server.test.js +357 -0
- package/test/streaming.test.js +248 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// model-router/providers/anthropic.js
|
|
2
|
+
const { Anthropic } = require('@anthropic-ai/sdk');
|
|
3
|
+
|
|
4
|
+
function buildClient(apiKey) {
|
|
5
|
+
return new Anthropic({ apiKey });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function estimateCost(model, inputTokens, outputTokens) {
|
|
9
|
+
// Approximate pricing per 1M tokens — update as Anthropic changes rates
|
|
10
|
+
const rates = {
|
|
11
|
+
'claude-sonnet-4-5-20250929': { input: 3.0, output: 15.0 },
|
|
12
|
+
'claude-haiku-4-5-20251001': { input: 0.8, output: 4.0 },
|
|
13
|
+
'claude-3-5-sonnet-20241022': { input: 3.0, output: 15.0 },
|
|
14
|
+
'claude-3-5-sonnet-20240620': { input: 3.0, output: 15.0 }
|
|
15
|
+
};
|
|
16
|
+
const rate = rates[model] || { input: 3.0, output: 15.0 };
|
|
17
|
+
return ((inputTokens * rate.input) + (outputTokens * rate.output)) / 1_000_000;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function chat(client, payload) {
|
|
21
|
+
const systemMessage = payload.messages.find(m => m.role === 'system');
|
|
22
|
+
const userMessages = payload.messages.filter(m => m.role !== 'system');
|
|
23
|
+
|
|
24
|
+
const request = {
|
|
25
|
+
model: payload.model,
|
|
26
|
+
max_tokens: payload.max_tokens || 1024,
|
|
27
|
+
temperature: typeof payload.temperature === 'number' ? payload.temperature : 0.0,
|
|
28
|
+
messages: userMessages,
|
|
29
|
+
...(systemMessage && { system: systemMessage.content }),
|
|
30
|
+
...(payload.tools && { tools: payload.tools }),
|
|
31
|
+
...(payload.tool_choice && { tool_choice: payload.tool_choice })
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const start = Date.now();
|
|
35
|
+
const response = await client.messages.create(request);
|
|
36
|
+
const latencyMs = Date.now() - start;
|
|
37
|
+
|
|
38
|
+
const inputTokens = response.usage.input_tokens;
|
|
39
|
+
const outputTokens = response.usage.output_tokens;
|
|
40
|
+
const costUsd = estimateCost(payload.model, inputTokens, outputTokens);
|
|
41
|
+
|
|
42
|
+
const toolCall = response.content.find(c => c.type === 'tool_use');
|
|
43
|
+
const textContent = response.content
|
|
44
|
+
.filter(c => c.type === 'text')
|
|
45
|
+
.map(c => c.text)
|
|
46
|
+
.join('');
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
provider: 'anthropic',
|
|
50
|
+
model: payload.model,
|
|
51
|
+
latency_ms: latencyMs,
|
|
52
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens },
|
|
53
|
+
cost_usd: costUsd,
|
|
54
|
+
content: textContent,
|
|
55
|
+
tool_calls: toolCall ? [toolCall] : undefined,
|
|
56
|
+
raw: response
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pure state-accumulation for one Anthropic streaming event - factored
|
|
62
|
+
* out from chatStream() so the trickiest part (pulling usage/cost data
|
|
63
|
+
* out of a stream instead of one final response object) is directly
|
|
64
|
+
* unit-testable with canned events, no live API needed. Mutates
|
|
65
|
+
* `state` ({content, inputTokens, outputTokens}) and calls onDelta()
|
|
66
|
+
* with each new piece of assistant text.
|
|
67
|
+
*/
|
|
68
|
+
function applyStreamEvent(state, event, onDelta) {
|
|
69
|
+
if (event.type === 'message_start') {
|
|
70
|
+
state.inputTokens = event.message.usage.input_tokens;
|
|
71
|
+
state.outputTokens = event.message.usage.output_tokens || 0;
|
|
72
|
+
} else if (event.type === 'content_block_delta' && event.delta && event.delta.type === 'text_delta') {
|
|
73
|
+
state.content += event.delta.text;
|
|
74
|
+
onDelta(event.delta.text);
|
|
75
|
+
} else if (event.type === 'message_delta' && event.usage) {
|
|
76
|
+
// Anthropic reports output_tokens progressively here; the last one
|
|
77
|
+
// received before message_stop is the final total.
|
|
78
|
+
state.outputTokens = event.usage.output_tokens;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Streaming counterpart to chat(). Scope: plain text content only - no
|
|
84
|
+
* tools/tool_choice forwarded (server.js rejects stream:true + tools
|
|
85
|
+
* before this is ever called; see streaming.js for why).
|
|
86
|
+
*/
|
|
87
|
+
async function chatStream(client, payload, { onDelta, signal } = {}) {
|
|
88
|
+
const systemMessage = payload.messages.find(m => m.role === 'system');
|
|
89
|
+
const userMessages = payload.messages.filter(m => m.role !== 'system');
|
|
90
|
+
|
|
91
|
+
const request = {
|
|
92
|
+
model: payload.model,
|
|
93
|
+
max_tokens: payload.max_tokens || 1024,
|
|
94
|
+
temperature: typeof payload.temperature === 'number' ? payload.temperature : 0.0,
|
|
95
|
+
messages: userMessages,
|
|
96
|
+
...(systemMessage && { system: systemMessage.content }),
|
|
97
|
+
stream: true
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const start = Date.now();
|
|
101
|
+
const stream = await client.messages.create(request, signal ? { signal } : undefined);
|
|
102
|
+
|
|
103
|
+
const state = { content: '', inputTokens: 0, outputTokens: 0 };
|
|
104
|
+
for await (const event of stream) {
|
|
105
|
+
applyStreamEvent(state, event, onDelta || (() => {}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const latencyMs = Date.now() - start;
|
|
109
|
+
const costUsd = estimateCost(payload.model, state.inputTokens, state.outputTokens);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
provider: 'anthropic',
|
|
113
|
+
model: payload.model,
|
|
114
|
+
latency_ms: latencyMs,
|
|
115
|
+
usage: { input_tokens: state.inputTokens, output_tokens: state.outputTokens },
|
|
116
|
+
cost_usd: costUsd,
|
|
117
|
+
content: state.content,
|
|
118
|
+
tool_calls: undefined
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { buildClient, chat, chatStream, applyStreamEvent, estimateCost };
|
|
@@ -0,0 +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 };
|