@johpaz/hive-sdk 0.4.9 → 0.5.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/CHANGELOG.md +85 -1
- package/README.md +9 -4
- package/docs/API-AGENTS.md +70 -0
- package/docs/INDEX.md +5 -1
- package/docs/UPGRADING.md +20 -0
- package/package.json +1 -1
- package/packages/core/src/agent/agent-loop.ts +82 -5
- package/packages/core/src/agent/context-compiler.ts +122 -23
- package/packages/core/src/agent/index.ts +2 -0
- package/packages/core/src/agent/jev-decisions.ts +197 -0
- package/packages/core/src/agent/jev-planner.ts +298 -0
- package/packages/core/src/agent/llm-client.ts +2 -2
- package/packages/core/src/agent/llm-providers/anthropic.ts +2 -1
- package/packages/core/src/agent/llm-providers/gemini.ts +3 -1
- package/packages/core/src/agent/realtime-providers/gemini-live.ts +2 -1
- package/packages/core/src/agent/tool-selector.ts +1 -0
- package/packages/core/src/canvas/emitter.ts +19 -0
- package/packages/core/src/events/channel-narration.ts +3 -0
- package/packages/core/src/multimodal/vision-service.ts +7 -7
- package/packages/core/src/services/swarms.ts +4 -0
- package/packages/core/src/storage/collections.ts +9 -2
- package/packages/core/src/storage/crypto.ts +51 -17
- package/packages/core/src/storage/index.ts +1 -0
- package/packages/core/src/storage/seed.ts +4 -0
- package/packages/core/src/storage/usage.ts +55 -2
- package/packages/core/src/swarm/RoleSwarm.ts +6 -0
- package/packages/core/src/tool-runtime/index.ts +20 -1
- package/packages/core/src/tools/agents/get-available-models.ts +3 -1
- package/packages/core/src/tools/core/index.ts +33 -2
- package/packages/core/src/tools/web/computer-use.ts +2 -2
- package/packages/core/src/voice/index.ts +13 -13
|
@@ -117,6 +117,7 @@ export const SEED_DATA: SeedData = {
|
|
|
117
117
|
{ id: "notify", name: "notify", category: "core", description: "Enviar notificación al usuario. Sinónimos: notificar, enviar notificación, alertar, aviso" },
|
|
118
118
|
{ id: "save_note", name: "save_note", category: "core", description: "Guardar nota persistente en el scratchpad. Sinónimos: guardar nota, escribir nota, recordatorio rápido, apuntar" },
|
|
119
119
|
{ id: "report_progress", name: "report_progress", category: "core", description: "Reportar progreso actual al usuario. Sinónimos: reportar progreso, informar estado, actualizar progreso, porcentaje" },
|
|
120
|
+
{ id: "conversation_read", name: "conversation_read", category: "core", description: "Recuperar mensajes anteriores de esta conversación por ID o búsqueda de texto. Sinónimos: leer conversación, recuperar contexto, historial omitido" },
|
|
120
121
|
|
|
121
122
|
// ─────────────────────────────────────────
|
|
122
123
|
// 10. OFFICE — Archivos Office (PDF, DOCX, XLSX, PPTX)
|
|
@@ -235,6 +236,9 @@ export const SEED_DATA: SeedData = {
|
|
|
235
236
|
{ id: "kimi-k2.6", providerId: "kimi", name: "Kimi K2.6", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code"]), inputPer1M: 0.6, outputPer1M: 3.41 },
|
|
236
237
|
|
|
237
238
|
// ── OpenRouter (fuente: GET https://openrouter.ai/api/v1/models) ──
|
|
239
|
+
// Jev: modelo de decisiones (API Decisions, nunca chat). `modelType:
|
|
240
|
+
// "decision"` lo deja fuera de getDefaultLLM y de get_available_models.
|
|
241
|
+
{ id: "typesafe/jev-1.13", providerId: "openrouter", name: "Jev 1.13 (Decisions)", modelType: "decision", contextWindow: 32000, capabilities: JSON.stringify(["choice", "noul", "score"]), inputPer1M: 0.042, outputPer1M: 0 },
|
|
238
242
|
// Solo modelos vivos con `tools` en supported_parameters y publicados desde
|
|
239
243
|
// 2025-07. contextWindow = context_length reportado por el propio catálogo,
|
|
240
244
|
// y los precios también salen de ahí — son los de la ruta de OpenRouter, no
|
|
@@ -154,6 +154,15 @@ export interface UsageRecord {
|
|
|
154
154
|
created_at: number;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
export interface JevUsageSummary {
|
|
158
|
+
decisions: number;
|
|
159
|
+
costUsd: number;
|
|
160
|
+
/** Estimated main-model input tokens avoided; negative when Jev added more than it removed. */
|
|
161
|
+
savedTokens: number;
|
|
162
|
+
savedCostUsd: number;
|
|
163
|
+
byAgent: Record<string, { decisions: number; costUsd: number; savedTokens: number; savedCostUsd: number }>;
|
|
164
|
+
}
|
|
165
|
+
|
|
157
166
|
export interface UsageSummary {
|
|
158
167
|
totalTokens: number;
|
|
159
168
|
totalInputTokens: number;
|
|
@@ -169,6 +178,7 @@ export interface UsageSummary {
|
|
|
169
178
|
byProvider: Record<string, { tokens: number; costUsd: number; inputTokens: number; outputTokens: number }>;
|
|
170
179
|
byModel: Record<string, { tokens: number; costUsd: number; provider: string; inputTokens: number; outputTokens: number }>;
|
|
171
180
|
recentRecords: UsageRecord[];
|
|
181
|
+
jev: JevUsageSummary;
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
export function recordUsage(options: {
|
|
@@ -233,13 +243,15 @@ export async function getUsageStats(hours: number = 24): Promise<UsageSummary> {
|
|
|
233
243
|
const rollupsCol = await col<UsageRollupDoc>("usageRollups");
|
|
234
244
|
const buckets = hourBucketsSince(hours);
|
|
235
245
|
const rollups = (await Promise.all(buckets.map((id) => rollupsCol.get(id))))
|
|
236
|
-
|
|
246
|
+
// A bucket touched only by recordJevDecision lacks the token fields.
|
|
247
|
+
.map((e) => ({ ...emptyRollup(), ...e?.doc }));
|
|
237
248
|
|
|
238
249
|
const providerMap: UsageSummary["byProvider"] = {};
|
|
239
250
|
const modelMap: UsageSummary["byModel"] = {};
|
|
240
251
|
let totalInput = 0, totalOutput = 0, totalCost = 0;
|
|
241
252
|
let toonSavedTokens = 0, toonSavedCost = 0, toonSavedBytes = 0;
|
|
242
253
|
let toonJsonTokens = 0, toonToonTokens = 0, toonJsonBytes = 0;
|
|
254
|
+
const jev: JevUsageSummary = { decisions: 0, costUsd: 0, savedTokens: 0, savedCostUsd: 0, byAgent: {} };
|
|
243
255
|
|
|
244
256
|
for (const r of rollups) {
|
|
245
257
|
totalInput += r.inputTokens;
|
|
@@ -251,6 +263,18 @@ export async function getUsageStats(hours: number = 24): Promise<UsageSummary> {
|
|
|
251
263
|
toonJsonTokens += r.toonJsonTokens;
|
|
252
264
|
toonToonTokens += r.toonToonTokens;
|
|
253
265
|
toonJsonBytes += r.toonJsonBytes;
|
|
266
|
+
jev.decisions += r.jevDecisions ?? 0;
|
|
267
|
+
jev.costUsd += r.jevCostUsd ?? 0;
|
|
268
|
+
jev.savedTokens += r.jevSavedTokens ?? 0;
|
|
269
|
+
jev.savedCostUsd += r.jevSavedCostUsd ?? 0;
|
|
270
|
+
for (const [agentId, a] of Object.entries(r.jevByAgent ?? {})) {
|
|
271
|
+
const cur = jev.byAgent[agentId] ?? { decisions: 0, costUsd: 0, savedTokens: 0, savedCostUsd: 0 };
|
|
272
|
+
cur.decisions += a.jevDecisions ?? 0;
|
|
273
|
+
cur.costUsd += a.jevCostUsd ?? 0;
|
|
274
|
+
cur.savedTokens += a.jevSavedTokens ?? 0;
|
|
275
|
+
cur.savedCostUsd += a.jevSavedCostUsd ?? 0;
|
|
276
|
+
jev.byAgent[agentId] = cur;
|
|
277
|
+
}
|
|
254
278
|
|
|
255
279
|
for (const [provider, p] of Object.entries(r.byProvider ?? {})) {
|
|
256
280
|
const cur = providerMap[provider] ?? { tokens: 0, costUsd: 0, inputTokens: 0, outputTokens: 0 };
|
|
@@ -301,10 +325,39 @@ export async function getUsageStats(hours: number = 24): Promise<UsageSummary> {
|
|
|
301
325
|
toonSavingsPercent,
|
|
302
326
|
byProvider: providerMap,
|
|
303
327
|
byModel: modelMap,
|
|
304
|
-
recentRecords
|
|
328
|
+
recentRecords,
|
|
329
|
+
jev,
|
|
305
330
|
};
|
|
306
331
|
}
|
|
307
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Persists one Jev decision into the hourly rollup. The avoided tokens are
|
|
335
|
+
* priced with the advised agent's own model (input rate), through the same
|
|
336
|
+
* catalog lookup as real usage, so "saved" and "spent" are comparable.
|
|
337
|
+
*/
|
|
338
|
+
export function recordJevDecision(options: {
|
|
339
|
+
agentId: string;
|
|
340
|
+
provider: string;
|
|
341
|
+
model: string;
|
|
342
|
+
savedTokens: number;
|
|
343
|
+
costUsd: number;
|
|
344
|
+
}): void {
|
|
345
|
+
Promise.resolve().then(async () => {
|
|
346
|
+
try {
|
|
347
|
+
const unitCost = await calculateCost(options.provider, options.model, Math.abs(options.savedTokens), 0);
|
|
348
|
+
const savedCostUsd = Math.sign(options.savedTokens) * unitCost;
|
|
349
|
+
await bumpRollup("usageRollups", hourBucket(Date.now()), {
|
|
350
|
+
jevDecisions: 1,
|
|
351
|
+
jevCostUsd: options.costUsd,
|
|
352
|
+
jevSavedTokens: options.savedTokens,
|
|
353
|
+
jevSavedCostUsd: savedCostUsd,
|
|
354
|
+
}, { field: "jevByAgent", key: options.agentId });
|
|
355
|
+
} catch (error) {
|
|
356
|
+
log.warn(`[JEV] Failed to record decision:`, error);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
308
361
|
/**
|
|
309
362
|
* Record TOON savings for metrics tracking
|
|
310
363
|
* This updates the usage_records table with complete TOON compression metrics
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { runAgentIsolated } from "../agent/agent-loop.ts"
|
|
24
|
+
import type { JevOption } from "../agent/jev-decisions.ts"
|
|
24
25
|
import type { ProviderCredentials } from "../agent/llm-client.ts"
|
|
25
26
|
import { logger } from "../utils/logger.ts"
|
|
26
27
|
|
|
@@ -50,6 +51,7 @@ export type AgentInvoker = (input: {
|
|
|
50
51
|
threadId: string
|
|
51
52
|
channel?: string
|
|
52
53
|
credentials?: ProviderCredentials
|
|
54
|
+
jev?: JevOption
|
|
53
55
|
signal?: AbortSignal
|
|
54
56
|
}) => Promise<string>
|
|
55
57
|
|
|
@@ -60,6 +62,7 @@ export const defaultInvoker: AgentInvoker = async (input) =>
|
|
|
60
62
|
threadId: input.threadId,
|
|
61
63
|
channel: input.channel,
|
|
62
64
|
credentials: input.credentials,
|
|
65
|
+
jev: input.jev,
|
|
63
66
|
signal: input.signal,
|
|
64
67
|
})
|
|
65
68
|
|
|
@@ -83,6 +86,8 @@ export interface RoleSwarmOptions {
|
|
|
83
86
|
maxDelegations?: number
|
|
84
87
|
/** Credenciales del inquilino, propagadas a cada agente. */
|
|
85
88
|
credentials?: ProviderCredentials
|
|
89
|
+
/** Jev del inquilino, propagado a cada agente igual que `credentials`. */
|
|
90
|
+
jev?: JevOption
|
|
86
91
|
signal?: AbortSignal
|
|
87
92
|
/** Se llama en cada paso; acá persiste el consumidor si quiere. */
|
|
88
93
|
onMessage?: (message: SwarmMessage) => void | Promise<void>
|
|
@@ -129,6 +134,7 @@ export async function runRoleSwarm(opts: RoleSwarmOptions): Promise<RoleSwarmRes
|
|
|
129
134
|
threadId,
|
|
130
135
|
channel: opts.channel,
|
|
131
136
|
credentials: opts.credentials,
|
|
137
|
+
jev: opts.jev,
|
|
132
138
|
signal: opts.signal,
|
|
133
139
|
})
|
|
134
140
|
}
|
|
@@ -47,6 +47,8 @@ export type ExecuteToolBatchOptions = {
|
|
|
47
47
|
}
|
|
48
48
|
hiveConfig?: Config
|
|
49
49
|
workerPool?: ToolRuntimeConfig
|
|
50
|
+
/** Per-batch decision (Jev). Undefined preserves the configured behavior. */
|
|
51
|
+
parallelToolCalls?: boolean
|
|
50
52
|
mainThreadToolNames?: string[]
|
|
51
53
|
signal?: AbortSignal
|
|
52
54
|
}
|
|
@@ -199,6 +201,7 @@ const DEFAULT_MAIN_THREAD_TOOL_NAMES = new Set([
|
|
|
199
201
|
// These tools depend on process-local singleton state (HiveDB handle, live
|
|
200
202
|
// channel senders, schedulers, browser sessions, or in-memory services).
|
|
201
203
|
"search_knowledge",
|
|
204
|
+
"conversation_read",
|
|
202
205
|
"save_note",
|
|
203
206
|
"memory_write",
|
|
204
207
|
"memory_read",
|
|
@@ -670,6 +673,7 @@ export async function executeToolBatch(options: ExecuteToolBatchOptions): Promis
|
|
|
670
673
|
|
|
671
674
|
async function executeToolBatchInner(options: ExecuteToolBatchOptions): Promise<ToolBatchResult[]> {
|
|
672
675
|
const runtimeConfig = resolveRuntimeConfig(options.workerPool)
|
|
676
|
+
const parallelToolCalls = options.parallelToolCalls ?? runtimeConfig.parallelToolCalls
|
|
673
677
|
const hiveConfig = options.hiveConfig ?? loadConfig()
|
|
674
678
|
const mainThreadToolNames = [
|
|
675
679
|
...DEFAULT_MAIN_THREAD_TOOL_NAMES,
|
|
@@ -690,11 +694,26 @@ async function executeToolBatchInner(options: ExecuteToolBatchOptions): Promise<
|
|
|
690
694
|
|
|
691
695
|
// A null pool means workers are disabled, unnecessary (single call), or
|
|
692
696
|
// unavailable in this build — all three degrade to the main thread.
|
|
693
|
-
const pool = runtimeConfig.enabled &&
|
|
697
|
+
const pool = runtimeConfig.enabled && parallelToolCalls && options.toolCalls.length > 1
|
|
694
698
|
? getPool(runtimeConfig.maxWorkers)
|
|
695
699
|
: null
|
|
696
700
|
|
|
697
701
|
if (!pool) {
|
|
702
|
+
// Jev judged the batch independent: run it concurrently even without workers.
|
|
703
|
+
if (options.parallelToolCalls === true && options.toolCalls.length > 1) {
|
|
704
|
+
return Promise.all(options.toolCalls.map(async (toolCall) => {
|
|
705
|
+
const startedAt = performance.now()
|
|
706
|
+
const effectiveTimeout = resolveToolTimeout(toolCall.function.name, options.allTools, hiveConfig, runtimeConfig.toolTimeoutMs)
|
|
707
|
+
const result = await executeInMainThreadWithTimeout({
|
|
708
|
+
toolCall, allTools: options.allTools, toolConfig: options.toolConfig, signal: options.signal,
|
|
709
|
+
}, effectiveTimeout)
|
|
710
|
+
return {
|
|
711
|
+
toolCall, toolName: toolCall.function.name, result,
|
|
712
|
+
ok: !(result && typeof result === "object" && (result as any).error === true),
|
|
713
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
714
|
+
}
|
|
715
|
+
}))
|
|
716
|
+
}
|
|
698
717
|
const results: ToolBatchResult[] = []
|
|
699
718
|
for (const toolCall of options.toolCalls) {
|
|
700
719
|
const startedAt = performance.now()
|
|
@@ -60,7 +60,9 @@ export const getAvailableModelsTool: Tool = {
|
|
|
60
60
|
.filter(m => m.enabled && providersById.has(m.provider_id));
|
|
61
61
|
|
|
62
62
|
if (providerId) models = models.filter(m => m.provider_id === providerId);
|
|
63
|
-
|
|
63
|
+
// Decision models (Jev) only answer the Decisions API, never chat: an
|
|
64
|
+
// agent assigned one would fail every turn.
|
|
65
|
+
models = modelType ? models.filter(m => m.model_type === modelType) : models.filter(m => m.model_type !== "decision");
|
|
64
66
|
if (capabilities) models = models.filter(m => (m.capabilities ?? "").includes(capabilities));
|
|
65
67
|
|
|
66
68
|
// Transformar a formato amigable
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type CapabilityType,
|
|
15
15
|
} from "../../agent/capability-search.ts";
|
|
16
16
|
import { CORE_TOOL_CATALOG } from "../../agent/tool-selector.ts";
|
|
17
|
-
import { saveScratchpadNote } from "../../agent/conversation-store.ts";
|
|
17
|
+
import { saveScratchpadNote, getRecentMessages, getScratchpad } from "../../agent/conversation-store.ts";
|
|
18
18
|
|
|
19
19
|
const log = logger.child("core");
|
|
20
20
|
|
|
@@ -503,6 +503,37 @@ export const reportProgressTool: Tool = {
|
|
|
503
503
|
},
|
|
504
504
|
};
|
|
505
505
|
|
|
506
|
+
/** Recover a small, scoped slice of conversation omitted by Jev's context plan. */
|
|
507
|
+
export const conversationReadTool: Tool = {
|
|
508
|
+
name: "conversation_read",
|
|
509
|
+
description: "Read earlier messages from the current conversation by message IDs or text query when selected context is insufficient.",
|
|
510
|
+
parameters: {
|
|
511
|
+
type: "object",
|
|
512
|
+
properties: {
|
|
513
|
+
message_ids: { type: "array", description: "Message IDs shown in the context plan", items: { type: "number" } },
|
|
514
|
+
note_keys: { type: "array", description: "Scratchpad note keys shown in the context plan", items: { type: "string" } },
|
|
515
|
+
query: { type: "string", description: "Optional case-insensitive text to search in earlier messages" },
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
execute: async (params, config) => {
|
|
519
|
+
const threadId = config?.configurable?.thread_id as string | undefined
|
|
520
|
+
if (!threadId) return { ok: false, error: "Conversation scope unavailable" }
|
|
521
|
+
const ids = new Set(Array.isArray(params.message_ids) ? params.message_ids.map(Number).filter(Number.isFinite) : [])
|
|
522
|
+
const noteKeys = new Set(Array.isArray(params.note_keys) ? params.note_keys.map(String) : [])
|
|
523
|
+
const query = String(params.query ?? "").trim().toLocaleLowerCase()
|
|
524
|
+
if (!ids.size && !noteKeys.size && !query) return { ok: false, error: "Provide message_ids, note_keys or query" }
|
|
525
|
+
const rows = await getRecentMessages(threadId, 200)
|
|
526
|
+
const notes = await getScratchpad(threadId)
|
|
527
|
+
return {
|
|
528
|
+
ok: true,
|
|
529
|
+
messages: rows.filter(row => (ids.size && ids.has(row.id)) || (query && row.content.toLocaleLowerCase().includes(query)))
|
|
530
|
+
.slice(-8).map(row => ({ id: row.id, role: row.role, source: row.source, content: row.content.slice(0, 2000) })),
|
|
531
|
+
notes: notes.filter(note => noteKeys.has(note.key) || (query && `${note.key} ${note.value}`.toLocaleLowerCase().includes(query)))
|
|
532
|
+
.slice(0, 8).map(note => ({ key: note.key, value: note.value.slice(0, 2000) })),
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
|
|
506
537
|
export function createTools(): Tool[] {
|
|
507
|
-
return [searchKnowledgeTool, notifyTool, saveNoteTool, reportProgressTool];
|
|
538
|
+
return [searchKnowledgeTool, notifyTool, saveNoteTool, reportProgressTool, conversationReadTool];
|
|
508
539
|
}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import type { Tool } from "../types.ts";
|
|
29
29
|
import { logger } from "../../utils/logger.ts";
|
|
30
30
|
import { getBrowserService } from "./browser-service.ts";
|
|
31
|
-
import { loadProviderApiKey } from "../../storage/crypto.ts";
|
|
31
|
+
import { envSecret, loadProviderApiKey } from "../../storage/crypto.ts";
|
|
32
32
|
|
|
33
33
|
const log = logger.child("computer-use");
|
|
34
34
|
|
|
@@ -417,7 +417,7 @@ export const computerUseTaskTool: Tool = {
|
|
|
417
417
|
const maxPasos = Math.max(1, Math.min(30, Number(params.max_pasos) || MAX_PASOS));
|
|
418
418
|
const confirmado = params.confirmado === true;
|
|
419
419
|
|
|
420
|
-
const apiKey = (await loadProviderApiKey("gemini")) ||
|
|
420
|
+
const apiKey = (await loadProviderApiKey("gemini")) || envSecret("GEMINI_API_KEY");
|
|
421
421
|
if (!apiKey) {
|
|
422
422
|
return { ok: false, error: "Falta la API key de Gemini (Ajustes → Proveedores)." };
|
|
423
423
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolvePort } from "../utils/port.ts";
|
|
2
2
|
import { col } from "../storage/hive.ts";
|
|
3
3
|
import type { ChannelDoc, ModelDoc } from "../storage/collections.ts";
|
|
4
|
-
import { loadProviderApiKey } from "../storage/crypto.ts";
|
|
4
|
+
import { envSecret, loadProviderApiKey } from "../storage/crypto.ts";
|
|
5
5
|
import { logger } from "../utils/logger.ts";
|
|
6
6
|
|
|
7
7
|
export interface VoiceConfig {
|
|
@@ -160,7 +160,7 @@ class VoiceService {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
private async transcribeWithGroq(audio: AudioInput, modelId: string): Promise<string> {
|
|
163
|
-
const key = await this.getProviderApiKey("groq") ||
|
|
163
|
+
const key = await this.getProviderApiKey("groq") || envSecret("GROQ_API_KEY");
|
|
164
164
|
if (!key) {
|
|
165
165
|
throw new Error("GROQ_API_KEY not configured. Configúrala en Proveedores o en las variables de entorno.");
|
|
166
166
|
}
|
|
@@ -212,7 +212,7 @@ class VoiceService {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
private async transcribeWithOpenAIWhisper(audio: AudioInput): Promise<string> {
|
|
215
|
-
const key = await this.getProviderApiKey("openai") ||
|
|
215
|
+
const key = await this.getProviderApiKey("openai") || envSecret("OPENAI_API_KEY");
|
|
216
216
|
if (!key) {
|
|
217
217
|
throw new Error("OPENAI_API_KEY not configured. Configúrala en Proveedores o en las variables de entorno.");
|
|
218
218
|
}
|
|
@@ -302,7 +302,7 @@ class VoiceService {
|
|
|
302
302
|
|
|
303
303
|
private async speakWithElevenLabs(text: string, modelId: string, voiceId?: string): Promise<AudioOutput> {
|
|
304
304
|
const apiKey = await this.getProviderApiKey("elevenlabs");
|
|
305
|
-
const key = apiKey ||
|
|
305
|
+
const key = apiKey || envSecret("ELEVENLABS_API_KEY");
|
|
306
306
|
|
|
307
307
|
if (!key) {
|
|
308
308
|
throw new Error("ELEVENLABS_API_KEY not configured");
|
|
@@ -341,7 +341,7 @@ class VoiceService {
|
|
|
341
341
|
|
|
342
342
|
private async speakWithOpenAI(text: string, modelId: string = "gpt-4o-mini-tts", voiceId?: string): Promise<AudioOutput> {
|
|
343
343
|
const apiKey = await this.getProviderApiKey("openai-tts");
|
|
344
|
-
const key = apiKey ||
|
|
344
|
+
const key = apiKey || envSecret("OPENAI_API_KEY");
|
|
345
345
|
|
|
346
346
|
if (!key) {
|
|
347
347
|
throw new Error("OPENAI_API_KEY not configured");
|
|
@@ -377,7 +377,7 @@ class VoiceService {
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
private async speakWithGemini(text: string, modelId: string, voiceId?: string): Promise<AudioOutput> {
|
|
380
|
-
const key =
|
|
380
|
+
const key = envSecret("GEMINI_API_KEY");
|
|
381
381
|
|
|
382
382
|
if (!key) {
|
|
383
383
|
throw new Error("GEMINI_API_KEY not configured");
|
|
@@ -431,7 +431,7 @@ class VoiceService {
|
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
private async speakWithQwen(text: string, modelId: string, voiceId?: string): Promise<AudioOutput> {
|
|
434
|
-
const key =
|
|
434
|
+
const key = envSecret("DASHSCOPE_API_KEY");
|
|
435
435
|
|
|
436
436
|
if (!key) {
|
|
437
437
|
throw new Error("DASHSCOPE_API_KEY not configured");
|
|
@@ -485,11 +485,11 @@ class VoiceService {
|
|
|
485
485
|
]);
|
|
486
486
|
|
|
487
487
|
return {
|
|
488
|
-
groq: groq || !!(
|
|
489
|
-
elevenlabs: elevenlabs || !!(
|
|
490
|
-
openai: openai || !!(
|
|
491
|
-
gemini: gemini || !!(
|
|
492
|
-
qwen: qwen || !!(
|
|
488
|
+
groq: groq || !!(envSecret("GROQ_API_KEY")),
|
|
489
|
+
elevenlabs: elevenlabs || !!(envSecret("ELEVENLABS_API_KEY")),
|
|
490
|
+
openai: openai || !!(envSecret("OPENAI_API_KEY")),
|
|
491
|
+
gemini: gemini || !!(envSecret("GEMINI_API_KEY")),
|
|
492
|
+
qwen: qwen || !!(envSecret("DASHSCOPE_API_KEY")),
|
|
493
493
|
};
|
|
494
494
|
}
|
|
495
495
|
|
|
@@ -561,7 +561,7 @@ class VoiceService {
|
|
|
561
561
|
|
|
562
562
|
async getElevenLabsVoices(): Promise<Array<{ id: string; name: string; category: string }>> {
|
|
563
563
|
const apiKey = await this.getProviderApiKey("elevenlabs");
|
|
564
|
-
const key = apiKey ||
|
|
564
|
+
const key = apiKey || envSecret("ELEVENLABS_API_KEY");
|
|
565
565
|
|
|
566
566
|
if (!key) {
|
|
567
567
|
throw new Error("ELEVENLABS_API_KEY not configured");
|