@johpaz/hive-sdk 0.0.18 → 0.1.3
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/bun.lock +291 -1
- package/docs/HIVE-HARNESS.md +113 -0
- package/package.json +36 -2
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +13 -2
- package/packages/core/src/ace/Tracer.ts +1 -1
- package/packages/core/src/agent/AgentRunner.ts +12 -0
- package/packages/core/src/agent/ContextCompiler.ts +4 -4
- package/packages/core/src/agent/ConversationStore.ts +30 -20
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +50 -76
- package/packages/core/src/agent/selectors/SkillSelector.ts +106 -262
- package/packages/core/src/agent/selectors/ToolSelector.ts +53 -89
- package/packages/core/src/auth/auth.ts +36 -23
- package/packages/core/src/harness/boot-id.ts +20 -0
- package/packages/core/src/harness/collections.ts +98 -0
- package/packages/core/src/harness/db-helpers.ts +87 -0
- package/packages/core/src/harness/durable-queue.ts +337 -0
- package/packages/core/src/harness/goal-verifier.ts +141 -0
- package/packages/core/src/harness/harness.test.ts +236 -0
- package/packages/core/src/harness/index.ts +34 -0
- package/packages/core/src/harness/job-store.ts +399 -0
- package/packages/core/src/harness/proof-packet.ts +69 -0
- package/packages/core/src/harness/reconcile.ts +149 -0
- package/packages/core/src/harness/run-epoch.ts +32 -0
- package/packages/core/src/harness/run-store.ts +334 -0
- package/packages/core/src/index.ts +6 -0
- package/packages/core/src/memory/Scratchpad.test.ts +23 -21
- package/packages/core/src/memory/Scratchpad.ts +41 -24
- package/packages/core/src/storage/HiveDBStorage.ts +64 -0
- package/packages/core/src/storage/SQLiteStorage.ts +7 -0
- package/packages/core/src/storage/hiveSeed.ts +308 -0
- package/packages/core/src/storage/hiveStorage.test.ts +38 -0
- package/packages/core/src/storage/index.ts +10 -0
- package/packages/core/src/storage/seed.ts +5 -1
- package/packages/core/src/storage/usage.ts +106 -167
- package/packages/core/src/tool-runtime/tool-runtime.test.ts +11 -3
- package/packages/core/src/tools/agents/get-available-models.ts +52 -56
- package/packages/core/src/tools/agents/index.ts +77 -60
- package/packages/core/src/tools/core/index.ts +106 -291
- package/packages/core/src/tools/meeting/index.ts +83 -93
- package/packages/core/src/utils/toon.ts +4 -4
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getHiveDB } from "./HiveDBStorage.ts";
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import { logger } from "../utils/logger.ts";
|
|
4
4
|
|
|
5
5
|
const log = logger.child("usage");
|
|
6
6
|
|
|
7
|
-
// Precios en USD por millón de tokens (input / output)
|
|
8
|
-
// Fuentes: docs.anthropic.com, openrouter.ai/api/v1/models, api-docs.deepseek.com, console.groq.com
|
|
9
7
|
const MODEL_PRICING: Record<string, { inputPer1M: number; outputPer1M: number }> = {
|
|
10
|
-
// ── Anthropic (fuente: docs.anthropic.com) ──
|
|
11
8
|
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25 },
|
|
12
9
|
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15 },
|
|
13
10
|
"claude-haiku-4-5-20251001": { inputPer1M: 1, outputPer1M: 5 },
|
|
14
11
|
"anthropic/claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25 },
|
|
15
12
|
"anthropic/claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15 },
|
|
16
|
-
|
|
17
|
-
// ── OpenAI (fuente: openrouter.ai/api/v1/models) ──
|
|
18
13
|
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10 },
|
|
19
14
|
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6 },
|
|
20
15
|
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15 },
|
|
@@ -25,11 +20,8 @@ const MODEL_PRICING: Record<string, { inputPer1M: number; outputPer1M: number }>
|
|
|
25
20
|
"openai/gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15 },
|
|
26
21
|
"openai/gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180 },
|
|
27
22
|
"openai/gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14 },
|
|
28
|
-
// Groq OSS (fuente: console.groq.com)
|
|
29
23
|
"openai/gpt-oss-120b": { inputPer1M: 0.15, outputPer1M: 0.6 },
|
|
30
24
|
"openai/gpt-oss-20b": { inputPer1M: 0.075, outputPer1M: 0.3 },
|
|
31
|
-
|
|
32
|
-
// ── Google Gemini (fuente: openrouter.ai/api/v1/models) ──
|
|
33
25
|
"gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12 },
|
|
34
26
|
"gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5 },
|
|
35
27
|
"gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3 },
|
|
@@ -41,8 +33,6 @@ const MODEL_PRICING: Record<string, { inputPer1M: number; outputPer1M: number }>
|
|
|
41
33
|
"google/gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5 },
|
|
42
34
|
"google/gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3 },
|
|
43
35
|
"google/gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6 },
|
|
44
|
-
|
|
45
|
-
// ── Mistral (fuente: openrouter.ai/api/v1/models) ──
|
|
46
36
|
"mistral-large-2512": { inputPer1M: 0.5, outputPer1M: 1.5 },
|
|
47
37
|
"devstral-2512": { inputPer1M: 0.4, outputPer1M: 2 },
|
|
48
38
|
"ministral-14b-2512": { inputPer1M: 0.2, outputPer1M: 0.2 },
|
|
@@ -51,14 +41,10 @@ const MODEL_PRICING: Record<string, { inputPer1M: number; outputPer1M: number }>
|
|
|
51
41
|
"mistral-small-3.2-24b-instruct": { inputPer1M: 0.1, outputPer1M: 0.3 },
|
|
52
42
|
"mistral-large-latest": { inputPer1M: 0.5, outputPer1M: 1.5 },
|
|
53
43
|
"codestral-latest": { inputPer1M: 0.2, outputPer1M: 0.6 },
|
|
54
|
-
|
|
55
|
-
// ── DeepSeek (fuente: api-docs.deepseek.com/quick_start/pricing) ──
|
|
56
44
|
"deepseek-chat": { inputPer1M: 0.28, outputPer1M: 0.42 },
|
|
57
45
|
"deepseek-reasoner": { inputPer1M: 0.28, outputPer1M: 0.42 },
|
|
58
46
|
"deepseek/deepseek-v3.2": { inputPer1M: 0.25, outputPer1M: 0.4 },
|
|
59
47
|
"deepseek/deepseek-r1:free": { inputPer1M: 0, outputPer1M: 0 },
|
|
60
|
-
|
|
61
|
-
// ── Kimi / Moonshot (fuente: openrouter.ai/moonshotai) ──
|
|
62
48
|
"kimi-k2.5": { inputPer1M: 0.45, outputPer1M: 2.2 },
|
|
63
49
|
"kimi-k2": { inputPer1M: 0.45, outputPer1M: 2.2 },
|
|
64
50
|
"moonshot-v1-8k": { inputPer1M: 1.67, outputPer1M: 1.67 },
|
|
@@ -66,23 +52,15 @@ const MODEL_PRICING: Record<string, { inputPer1M: number; outputPer1M: number }>
|
|
|
66
52
|
"moonshot-v1-128k": { inputPer1M: 8.33, outputPer1M: 8.33 },
|
|
67
53
|
"moonshotai/kimi-k2.5": { inputPer1M: 0.45, outputPer1M: 2.2 },
|
|
68
54
|
"moonshotai/kimi-k2-instruct-0905": { inputPer1M: 0.45, outputPer1M: 2.2 },
|
|
69
|
-
|
|
70
|
-
// ── Meta Llama (vía OpenRouter) ──
|
|
71
55
|
"meta-llama/llama-3.3-70b-instruct": { inputPer1M: 0.88, outputPer1M: 0.88 },
|
|
72
56
|
"meta-llama/llama-4-maverick": { inputPer1M: 0.2, outputPer1M: 0.8 },
|
|
73
|
-
|
|
74
|
-
// ── Qwen (vía OpenRouter) ──
|
|
75
57
|
"qwen/qwen3.5-plus-02-15": { inputPer1M: 0.26, outputPer1M: 1.56 },
|
|
76
58
|
"qwen/qwen3.5-flash-02-23": { inputPer1M: 0.1, outputPer1M: 0.4 },
|
|
77
59
|
"qwen/qwen3-32b": { inputPer1M: 0, outputPer1M: 0 },
|
|
78
|
-
|
|
79
|
-
// ── Groq (fuente: console.groq.com/docs/models) ──
|
|
80
60
|
"llama-3.3-70b-versatile": { inputPer1M: 0.59, outputPer1M: 0.79 },
|
|
81
61
|
"llama-3.1-8b-instant": { inputPer1M: 0.05, outputPer1M: 0.08 },
|
|
82
62
|
"groq/compound": { inputPer1M: 0, outputPer1M: 0 },
|
|
83
63
|
"groq/compound-mini": { inputPer1M: 0, outputPer1M: 0 },
|
|
84
|
-
|
|
85
|
-
// ── Ollama local = siempre free ──
|
|
86
64
|
"qwen3:4b": { inputPer1M: 0, outputPer1M: 0 },
|
|
87
65
|
"qwen3:8b": { inputPer1M: 0, outputPer1M: 0 },
|
|
88
66
|
"qwen3:14b": { inputPer1M: 0, outputPer1M: 0 },
|
|
@@ -134,117 +112,101 @@ export interface UsageSummary {
|
|
|
134
112
|
recentRecords: UsageRecord[];
|
|
135
113
|
}
|
|
136
114
|
|
|
137
|
-
export function recordUsage(options: {
|
|
115
|
+
export async function recordUsage(options: {
|
|
138
116
|
provider: string;
|
|
139
117
|
model: string;
|
|
140
118
|
inputTokens: number;
|
|
141
119
|
outputTokens: number;
|
|
142
120
|
latencyMs?: number;
|
|
143
|
-
}): void {
|
|
121
|
+
}): Promise<void> {
|
|
144
122
|
try {
|
|
145
|
-
const db =
|
|
123
|
+
const db = await getHiveDB();
|
|
124
|
+
const col = db.collection<UsageRecord>("usage_records");
|
|
146
125
|
const costUsd = calculateCost(options.model, options.inputTokens, options.outputTokens);
|
|
147
126
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
options.
|
|
154
|
-
|
|
155
|
-
options.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
127
|
+
await col.put(randomUUID(), {
|
|
128
|
+
id: randomUUID(),
|
|
129
|
+
provider: options.provider,
|
|
130
|
+
model: options.model,
|
|
131
|
+
input_tokens: options.inputTokens,
|
|
132
|
+
output_tokens: options.outputTokens,
|
|
133
|
+
cost_usd: costUsd,
|
|
134
|
+
latency_ms: options.latencyMs ?? null,
|
|
135
|
+
toon_saved_tokens: 0,
|
|
136
|
+
toon_saved_cost: 0,
|
|
137
|
+
toon_json_bytes: 0,
|
|
138
|
+
toon_toon_bytes: 0,
|
|
139
|
+
toon_saved_bytes: 0,
|
|
140
|
+
toon_saved_percent: 0,
|
|
141
|
+
toon_json_tokens: 0,
|
|
142
|
+
toon_toon_tokens: 0,
|
|
143
|
+
toon_saved_tokens_pct: 0,
|
|
144
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
145
|
+
});
|
|
146
|
+
|
|
161
147
|
log.info(`[USAGE RECORDED] provider=${options.provider} model=${options.model} input=${options.inputTokens} output=${options.outputTokens} cost=$${costUsd.toFixed(4)}`);
|
|
162
148
|
} catch (error) {
|
|
163
149
|
console.error("Failed to record usage:", error);
|
|
164
150
|
}
|
|
165
151
|
}
|
|
166
152
|
|
|
167
|
-
export function getUsageStats(hours: number = 24): UsageSummary {
|
|
153
|
+
export async function getUsageStats(hours: number = 24): Promise<UsageSummary> {
|
|
168
154
|
log.info(`[USAGE STATS] Fetching stats for last ${hours} hours`);
|
|
169
|
-
const db =
|
|
155
|
+
const db = await getHiveDB();
|
|
156
|
+
const col = db.collection<UsageRecord>("usage_records");
|
|
170
157
|
const since = Math.floor(Date.now() / 1000) - (hours * 3600);
|
|
171
158
|
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
total_input:
|
|
187
|
-
total_output:
|
|
188
|
-
total_cost:
|
|
189
|
-
toon_saved_tokens:
|
|
190
|
-
toon_saved_cost:
|
|
191
|
-
toon_saved_bytes:
|
|
192
|
-
toon_saved_percent:
|
|
193
|
-
toon_json_tokens:
|
|
194
|
-
toon_toon_tokens:
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const byProvider = db.prepare(`
|
|
198
|
-
SELECT
|
|
199
|
-
provider,
|
|
200
|
-
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
201
|
-
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
202
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
203
|
-
FROM usage_records
|
|
204
|
-
WHERE created_at >= ? AND provider != 'toon'
|
|
205
|
-
GROUP BY provider
|
|
206
|
-
`).all(since) as Array<{ provider: string; input_tokens: number; output_tokens: number; cost_usd: number }>;
|
|
159
|
+
const entries = await col.scan();
|
|
160
|
+
const records = entries.map(e => e.doc).filter(r => r.created_at >= since);
|
|
161
|
+
|
|
162
|
+
const totals = records.reduce((acc, r) => ({
|
|
163
|
+
total_input: acc.total_input + r.input_tokens,
|
|
164
|
+
total_output: acc.total_output + r.output_tokens,
|
|
165
|
+
total_cost: acc.total_cost + r.cost_usd,
|
|
166
|
+
toon_saved_tokens: acc.toon_saved_tokens + r.toon_saved_tokens,
|
|
167
|
+
toon_saved_cost: acc.toon_saved_cost + r.toon_saved_cost,
|
|
168
|
+
toon_saved_bytes: acc.toon_saved_bytes + r.toon_saved_bytes,
|
|
169
|
+
toon_saved_percent: acc.toon_saved_percent + r.toon_saved_percent,
|
|
170
|
+
toon_json_tokens: acc.toon_json_tokens + r.toon_json_tokens,
|
|
171
|
+
toon_toon_tokens: acc.toon_toon_tokens + r.toon_toon_tokens,
|
|
172
|
+
}), {
|
|
173
|
+
total_input: 0,
|
|
174
|
+
total_output: 0,
|
|
175
|
+
total_cost: 0,
|
|
176
|
+
toon_saved_tokens: 0,
|
|
177
|
+
toon_saved_cost: 0,
|
|
178
|
+
toon_saved_bytes: 0,
|
|
179
|
+
toon_saved_percent: 0,
|
|
180
|
+
toon_json_tokens: 0,
|
|
181
|
+
toon_toon_tokens: 0,
|
|
182
|
+
});
|
|
207
183
|
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
model,
|
|
211
|
-
provider,
|
|
212
|
-
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
213
|
-
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
214
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
215
|
-
FROM usage_records
|
|
216
|
-
WHERE created_at >= ? AND provider != 'toon'
|
|
217
|
-
GROUP BY model
|
|
218
|
-
ORDER BY cost_usd DESC
|
|
219
|
-
`).all(since) as Array<{ model: string; provider: string; input_tokens: number; output_tokens: number; cost_usd: number }>;
|
|
184
|
+
const providerMap: UsageSummary["byProvider"] = {};
|
|
185
|
+
const modelMap: UsageSummary["byModel"] = {};
|
|
220
186
|
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
187
|
+
for (const r of records) {
|
|
188
|
+
if (r.provider === "toon") continue;
|
|
189
|
+
if (!providerMap[r.provider]) {
|
|
190
|
+
providerMap[r.provider] = { inputTokens: 0, outputTokens: 0, tokens: 0, costUsd: 0 };
|
|
191
|
+
}
|
|
192
|
+
providerMap[r.provider].inputTokens += r.input_tokens;
|
|
193
|
+
providerMap[r.provider].outputTokens += r.output_tokens;
|
|
194
|
+
providerMap[r.provider].tokens += r.input_tokens + r.output_tokens;
|
|
195
|
+
providerMap[r.provider].costUsd += r.cost_usd;
|
|
227
196
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
};
|
|
197
|
+
if (!modelMap[r.model]) {
|
|
198
|
+
modelMap[r.model] = { provider: r.provider, inputTokens: 0, outputTokens: 0, tokens: 0, costUsd: 0 };
|
|
199
|
+
}
|
|
200
|
+
modelMap[r.model].inputTokens += r.input_tokens;
|
|
201
|
+
modelMap[r.model].outputTokens += r.output_tokens;
|
|
202
|
+
modelMap[r.model].tokens += r.input_tokens + r.output_tokens;
|
|
203
|
+
modelMap[r.model].costUsd += r.cost_usd;
|
|
236
204
|
}
|
|
237
205
|
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
inputTokens: m.input_tokens,
|
|
243
|
-
outputTokens: m.output_tokens,
|
|
244
|
-
tokens: m.input_tokens + m.output_tokens,
|
|
245
|
-
costUsd: m.cost_usd
|
|
246
|
-
};
|
|
247
|
-
}
|
|
206
|
+
const recentRecords = records
|
|
207
|
+
.filter(r => r.created_at >= since)
|
|
208
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
209
|
+
.slice(0, 20);
|
|
248
210
|
|
|
249
211
|
const totalTokens = totals.total_input + totals.total_output;
|
|
250
212
|
const totalIncludingSaved = totalTokens + totals.toon_saved_tokens;
|
|
@@ -252,7 +214,6 @@ export function getUsageStats(hours: number = 24): UsageSummary {
|
|
|
252
214
|
? (totals.toon_saved_tokens / totalIncludingSaved) * 100
|
|
253
215
|
: 0;
|
|
254
216
|
|
|
255
|
-
// Calculate average bytes savings percent
|
|
256
217
|
const toonSavedBytesPercent = totals.toon_toon_tokens > 0
|
|
257
218
|
? (totals.toon_saved_bytes / totals.toon_toon_tokens) * 100
|
|
258
219
|
: 0;
|
|
@@ -271,7 +232,7 @@ export function getUsageStats(hours: number = 24): UsageSummary {
|
|
|
271
232
|
toonSavingsPercent,
|
|
272
233
|
byProvider: providerMap,
|
|
273
234
|
byModel: modelMap,
|
|
274
|
-
recentRecords
|
|
235
|
+
recentRecords,
|
|
275
236
|
};
|
|
276
237
|
}
|
|
277
238
|
|
|
@@ -284,14 +245,9 @@ export function estimateCostForTokens(model: string, tokens: number): number {
|
|
|
284
245
|
return (tokens / 1_000_000) * pricing.inputPer1M;
|
|
285
246
|
}
|
|
286
247
|
|
|
287
|
-
/**
|
|
288
|
-
* Get average cost per token for a model (input + output average)
|
|
289
|
-
*/
|
|
290
248
|
export function getAverageTokenCost(model: string): number {
|
|
291
|
-
// 1. Exact match
|
|
292
249
|
let pricing = MODEL_PRICING[model];
|
|
293
250
|
|
|
294
|
-
// 2. Try stripping a single provider prefix (e.g. "openrouter/moonshotai/kimi" → "moonshotai/kimi")
|
|
295
251
|
if (!pricing) {
|
|
296
252
|
const slashIdx = model.indexOf('/');
|
|
297
253
|
if (slashIdx !== -1) {
|
|
@@ -299,7 +255,6 @@ export function getAverageTokenCost(model: string): number {
|
|
|
299
255
|
}
|
|
300
256
|
}
|
|
301
257
|
|
|
302
|
-
// 3. Partial match — find any key whose name is contained in the model string
|
|
303
258
|
if (!pricing) {
|
|
304
259
|
for (const [key, p] of Object.entries(MODEL_PRICING)) {
|
|
305
260
|
if (model.includes(key) || key.includes(model)) {
|
|
@@ -310,15 +265,10 @@ export function getAverageTokenCost(model: string): number {
|
|
|
310
265
|
}
|
|
311
266
|
|
|
312
267
|
if (!pricing) return 0;
|
|
313
|
-
// Average of input and output cost per token
|
|
314
268
|
return (pricing.inputPer1M + pricing.outputPer1M) / 2 / 1_000_000;
|
|
315
269
|
}
|
|
316
270
|
|
|
317
|
-
|
|
318
|
-
* Record TOON savings for metrics tracking
|
|
319
|
-
* This updates the usage_records table with complete TOON compression metrics
|
|
320
|
-
*/
|
|
321
|
-
export function recordToonSavings(
|
|
271
|
+
export async function recordToonSavings(
|
|
322
272
|
analysis: {
|
|
323
273
|
jsonBytes: number;
|
|
324
274
|
toonBytes: number;
|
|
@@ -329,46 +279,35 @@ export function recordToonSavings(
|
|
|
329
279
|
savedTokens: number;
|
|
330
280
|
savedTokensPercent: number;
|
|
331
281
|
},
|
|
332
|
-
costSaved: number,
|
|
282
|
+
costSaved: number,
|
|
333
283
|
category: string
|
|
334
|
-
): void {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
analysis.jsonTokens,
|
|
364
|
-
analysis.toonTokens,
|
|
365
|
-
Math.max(0, analysis.savedTokensPercent),
|
|
366
|
-
Math.floor(Date.now() / 1000),
|
|
367
|
-
)
|
|
368
|
-
|
|
369
|
-
log.debug(`[TOON] Recorded ${analysis.savedTokens} tokens ($${costSaved.toFixed(6)}) saved for ${category}`)
|
|
370
|
-
} catch (error) {
|
|
371
|
-
log.warn(`[TOON] Failed to record savings:`, error)
|
|
372
|
-
}
|
|
373
|
-
})
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
try {
|
|
286
|
+
const db = await getHiveDB();
|
|
287
|
+
const col = db.collection<UsageRecord>("usage_records");
|
|
288
|
+
|
|
289
|
+
await col.put(`toon_${category}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, {
|
|
290
|
+
id: `toon_${category}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
291
|
+
provider: "toon",
|
|
292
|
+
model: category,
|
|
293
|
+
input_tokens: 0,
|
|
294
|
+
output_tokens: 0,
|
|
295
|
+
cost_usd: 0,
|
|
296
|
+
latency_ms: null,
|
|
297
|
+
toon_saved_tokens: Math.max(0, analysis.savedTokens),
|
|
298
|
+
toon_saved_cost: costSaved,
|
|
299
|
+
toon_json_bytes: analysis.jsonBytes,
|
|
300
|
+
toon_toon_bytes: analysis.toonBytes,
|
|
301
|
+
toon_saved_bytes: analysis.savedBytes,
|
|
302
|
+
toon_saved_percent: Math.max(0, analysis.savedPercent),
|
|
303
|
+
toon_json_tokens: analysis.jsonTokens,
|
|
304
|
+
toon_toon_tokens: analysis.toonTokens,
|
|
305
|
+
toon_saved_tokens_pct: Math.max(0, analysis.savedTokensPercent),
|
|
306
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
log.debug(`[TOON] Recorded ${analysis.savedTokens} tokens ($${costSaved.toFixed(6)}) saved for ${category}`);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
log.warn(`[TOON] Failed to record savings:`, error);
|
|
312
|
+
}
|
|
374
313
|
}
|
|
@@ -20,10 +20,18 @@ describe("tool runtime worker pool", () => {
|
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it("runs multiple tools in parallel through worker scheduling", async () => {
|
|
23
|
+
// Per-tool delay + threshold sized for CI headroom: worker spawn/scheduling
|
|
24
|
+
// overhead is roughly fixed, so a larger delay keeps that overhead a small
|
|
25
|
+
// fraction of the budget instead of dominating it on a loaded runner.
|
|
26
|
+
// Serial execution would take ~3x the delay; the threshold stays well
|
|
27
|
+
// under that so the assertion still proves parallelism, not just patience.
|
|
28
|
+
const TOOL_DELAY_MS = 200;
|
|
29
|
+
const PARALLEL_THRESHOLD_MS = 450;
|
|
30
|
+
|
|
23
31
|
const tools: RuntimeTool[] = ["slow_a", "slow_b", "slow_c"].map((name) => ({
|
|
24
32
|
name,
|
|
25
33
|
execute: async () => {
|
|
26
|
-
await delay(
|
|
34
|
+
await delay(TOOL_DELAY_MS);
|
|
27
35
|
return { name };
|
|
28
36
|
},
|
|
29
37
|
}));
|
|
@@ -38,12 +46,12 @@ describe("tool runtime worker pool", () => {
|
|
|
38
46
|
allTools: tools,
|
|
39
47
|
toolConfig: {},
|
|
40
48
|
hiveConfig: loadConfig(),
|
|
41
|
-
workerPool: { enabled: true, maxWorkers: 3, toolTimeoutMs:
|
|
49
|
+
workerPool: { enabled: true, maxWorkers: 3, toolTimeoutMs: 5000, parallelToolCalls: true },
|
|
42
50
|
});
|
|
43
51
|
const elapsed = performance.now() - startedAt;
|
|
44
52
|
|
|
45
53
|
expect(results.map((result) => (result.result as any).name)).toEqual(["slow_a", "slow_b", "slow_c"]);
|
|
46
|
-
expect(elapsed).toBeLessThan(
|
|
54
|
+
expect(elapsed).toBeLessThan(PARALLEL_THRESHOLD_MS);
|
|
47
55
|
});
|
|
48
56
|
|
|
49
57
|
it("preserves input order when tools complete out of order", async () => {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { Tool } from "../types.ts";
|
|
11
|
-
import {
|
|
11
|
+
import { getHiveDB } from "../../storage/HiveDBStorage.ts";
|
|
12
|
+
import type { HiveProviderDoc, HiveModelDoc } from "../../storage/hiveSeed.ts";
|
|
12
13
|
|
|
13
14
|
export const getAvailableModelsTool: Tool = {
|
|
14
15
|
name: "get_available_models",
|
|
@@ -31,7 +32,6 @@ export const getAvailableModelsTool: Tool = {
|
|
|
31
32
|
},
|
|
32
33
|
},
|
|
33
34
|
execute: async (params: Record<string, unknown>) => {
|
|
34
|
-
const db = getDb();
|
|
35
35
|
const { providerId, modelType, capabilities } = params as {
|
|
36
36
|
providerId?: string;
|
|
37
37
|
modelType?: string;
|
|
@@ -39,74 +39,70 @@ export const getAvailableModelsTool: Tool = {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
p.id as provider_id,
|
|
46
|
-
p.name as provider_name,
|
|
47
|
-
p.category as provider_category,
|
|
48
|
-
m.id as model_id,
|
|
49
|
-
m.name as model_name,
|
|
50
|
-
m.model_type,
|
|
51
|
-
m.context_window,
|
|
52
|
-
m.capabilities
|
|
53
|
-
FROM models m
|
|
54
|
-
INNER JOIN providers p ON m.provider_id = p.id
|
|
55
|
-
WHERE m.enabled = 1 AND m.active = 1 AND p.enabled = 1 AND p.active = 1
|
|
56
|
-
`;
|
|
42
|
+
const db = await getHiveDB();
|
|
43
|
+
const providersCol = db.collection<HiveProviderDoc>("providers");
|
|
44
|
+
const modelsCol = db.collection<HiveModelDoc>("models");
|
|
57
45
|
|
|
58
|
-
const
|
|
59
|
-
|
|
46
|
+
const [providers, models] = await Promise.all([
|
|
47
|
+
providersCol.scan(),
|
|
48
|
+
modelsCol.scan(),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const activeProviders = new Map(
|
|
52
|
+
providers
|
|
53
|
+
.filter(p => p.doc.enabled && p.doc.active)
|
|
54
|
+
.map(p => [p.id, p.doc])
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
let rows = models
|
|
58
|
+
.filter(m => m.doc.enabled && m.doc.active)
|
|
59
|
+
.map(m => {
|
|
60
|
+
const provider = activeProviders.get(m.doc.providerId);
|
|
61
|
+
if (!provider) return null;
|
|
62
|
+
return {
|
|
63
|
+
providerId: provider.id,
|
|
64
|
+
providerName: provider.name,
|
|
65
|
+
providerCategory: provider.category,
|
|
66
|
+
modelId: m.doc.id,
|
|
67
|
+
modelName: m.doc.name,
|
|
68
|
+
modelType: m.doc.modelType,
|
|
69
|
+
contextWindow: m.doc.contextWindow ?? null,
|
|
70
|
+
capabilities: m.doc.capabilities ?? null,
|
|
71
|
+
};
|
|
72
|
+
})
|
|
73
|
+
.filter(Boolean) as Array<{
|
|
74
|
+
providerId: string;
|
|
75
|
+
providerName: string;
|
|
76
|
+
providerCategory: string;
|
|
77
|
+
modelId: string;
|
|
78
|
+
modelName: string;
|
|
79
|
+
modelType: string;
|
|
80
|
+
contextWindow: number | null;
|
|
81
|
+
capabilities: string[] | null;
|
|
82
|
+
}>;
|
|
60
83
|
|
|
61
84
|
if (providerId) {
|
|
62
|
-
|
|
63
|
-
queryParams.push(providerId as string);
|
|
85
|
+
rows = rows.filter(r => r.providerId === providerId);
|
|
64
86
|
}
|
|
65
87
|
|
|
66
88
|
if (modelType) {
|
|
67
|
-
|
|
68
|
-
queryParams.push(modelType as string);
|
|
89
|
+
rows = rows.filter(r => r.modelType === modelType);
|
|
69
90
|
}
|
|
70
91
|
|
|
71
92
|
if (capabilities) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (whereClauses.length > 0) {
|
|
77
|
-
query += " AND " + whereClauses.join(" AND ");
|
|
93
|
+
const cap = capabilities.toLowerCase();
|
|
94
|
+
rows = rows.filter(r => r.capabilities?.some(c => c.toLowerCase().includes(cap)));
|
|
78
95
|
}
|
|
79
96
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
provider_id: string;
|
|
85
|
-
provider_name: string;
|
|
86
|
-
provider_category: string;
|
|
87
|
-
model_id: string;
|
|
88
|
-
model_name: string;
|
|
89
|
-
model_type: string;
|
|
90
|
-
context_window: number | null;
|
|
91
|
-
capabilities: string | null;
|
|
92
|
-
}>;
|
|
93
|
-
|
|
94
|
-
// Transformar a formato amigable
|
|
95
|
-
const result = rows.map(row => ({
|
|
96
|
-
providerId: row.provider_id,
|
|
97
|
-
providerName: row.provider_name,
|
|
98
|
-
providerCategory: row.provider_category,
|
|
99
|
-
modelId: row.model_id,
|
|
100
|
-
modelName: row.model_name,
|
|
101
|
-
modelType: row.model_type,
|
|
102
|
-
contextWindow: row.context_window,
|
|
103
|
-
capabilities: row.capabilities ? JSON.parse(row.capabilities) : null,
|
|
104
|
-
}));
|
|
97
|
+
rows.sort((a, b) => {
|
|
98
|
+
if (a.providerName !== b.providerName) return a.providerName.localeCompare(b.providerName);
|
|
99
|
+
return a.modelName.localeCompare(b.modelName);
|
|
100
|
+
});
|
|
105
101
|
|
|
106
102
|
return {
|
|
107
103
|
ok: true,
|
|
108
|
-
count:
|
|
109
|
-
models:
|
|
104
|
+
count: rows.length,
|
|
105
|
+
models: rows,
|
|
110
106
|
};
|
|
111
107
|
} catch (error) {
|
|
112
108
|
return {
|