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