@johpaz/hive-sdk 0.1.3 → 0.1.5
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 +97 -0
- package/README.md +78 -23
- package/bunfig.toml +4 -2
- package/docs/API-AGENTS.md +78 -27
- package/docs/API-CONTEXT-COMPILER.md +31 -34
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
- package/docs/HIVE-HARNESS.md +1 -1
- package/docs/INDEX.md +4 -4
- package/docs/TEMPLATE-HIVE-APP.md +10 -10
- package/package.json +9 -4
- package/packages/cli/package.json +2 -2
- package/packages/cli/src/commands/create-app.test.ts +36 -7
- package/packages/cli/src/commands/init.ts +3 -3
- package/packages/cli/src/commands/run.ts +1 -1
- package/packages/cli/src/commands/test.ts +37 -25
- package/packages/cli/src/commands/trace.ts +30 -28
- package/packages/cli/templates/hive-app/.env.example +10 -2
- package/packages/cli/templates/hive-app/README.md +103 -0
- package/packages/cli/templates/hive-app/hive.config.ts +9 -3
- package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
- package/packages/cli/templates/hive-app/src/main.ts +12 -19
- package/packages/core/package.json +5 -4
- package/packages/core/src/agent/acceptance-checks.ts +166 -0
- package/packages/core/src/agent/agent-catalog.ts +348 -0
- package/packages/core/src/agent/agent-loop.ts +1373 -0
- package/packages/core/src/agent/capability-search.ts +186 -0
- package/packages/core/src/agent/catalog-selector.ts +103 -0
- package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
- package/packages/core/src/agent/context-compiler.ts +689 -0
- package/packages/core/src/agent/conversation-store.ts +381 -0
- package/packages/core/src/agent/curator.ts +276 -0
- package/packages/core/src/agent/delegation-runtime.ts +241 -0
- package/packages/core/src/agent/goal-runner.ts +323 -0
- package/packages/core/src/agent/index.ts +17 -12
- package/packages/core/src/agent/llm-client.ts +266 -0
- package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
- package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
- package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
- package/packages/core/src/agent/llm-providers/groq.ts +5 -0
- package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
- package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
- package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
- package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
- package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
- package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
- package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
- package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
- package/packages/core/src/agent/llm-providers/openai.ts +5 -0
- package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
- package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
- package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
- package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
- package/packages/core/src/agent/minimal-loadout.ts +47 -0
- package/packages/core/src/agent/playbook-selector.ts +119 -0
- package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
- package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
- package/packages/core/src/agent/providers/index.ts +35 -16
- package/packages/core/src/agent/reflector.ts +320 -0
- package/packages/core/src/agent/routing-intent.ts +22 -0
- package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
- package/packages/core/src/{harness → agent}/run-store.ts +142 -81
- package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
- package/packages/core/src/agent/skill-selector.ts +374 -0
- package/packages/core/src/agent/stuck-loop.ts +209 -0
- package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
- package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
- package/packages/core/src/api/createAgent.test.ts +139 -27
- package/packages/core/src/api/createAgent.ts +232 -44
- package/packages/core/src/artifacts/store.ts +162 -0
- package/packages/core/src/canvas/canvas-manager.ts +161 -0
- package/packages/core/src/canvas/canvas.test.ts +8 -4
- package/packages/core/src/canvas/emitter.ts +131 -80
- package/packages/core/src/canvas/index.ts +1 -3
- package/packages/core/src/channels/base.ts +9 -1
- package/packages/core/src/channels/discord.ts +5 -4
- package/packages/core/src/channels/manager.ts +122 -30
- package/packages/core/src/channels/slack.ts +5 -4
- package/packages/core/src/channels/telegram.ts +36 -6
- package/packages/core/src/channels/webchat.ts +11 -10
- package/packages/core/src/channels/whatsapp.ts +23 -7
- package/packages/core/src/config/index.ts +13 -2
- package/packages/core/src/config/loader.ts +71 -29
- package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
- package/packages/core/src/ethics/EthicsGuard.ts +51 -47
- package/packages/core/src/events/agent-bus.ts +44 -68
- package/packages/core/src/events/channel-narration.ts +150 -0
- package/packages/core/src/events/narration.ts +82 -0
- package/packages/core/src/events/tool-narration.ts +62 -0
- package/packages/core/src/gateway/delegation-groups.ts +258 -0
- package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
- package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
- package/packages/core/src/gateway/lane-queue.ts +173 -0
- package/packages/core/src/gateway/notification-inbox.ts +57 -0
- package/packages/core/src/gateway/server.ts +1 -1
- package/packages/core/src/harness/index.ts +46 -27
- package/packages/core/src/index.ts +33 -20
- package/packages/core/src/mcp/hot-reload.ts +32 -23
- package/packages/core/src/mcp/index.ts +6 -3
- package/packages/core/src/mcp/singleton.ts +1 -4
- package/packages/core/src/mcp/tool-sync.ts +138 -0
- package/packages/core/src/memory/Scratchpad.test.ts +39 -20
- package/packages/core/src/memory/Scratchpad.ts +27 -34
- package/packages/core/src/multimodal/vision-service.ts +44 -38
- package/packages/core/src/resilience/retry.ts +95 -0
- package/packages/core/src/scheduler/CronScheduler.ts +334 -287
- package/packages/core/src/scheduler/index.ts +9 -7
- package/packages/core/src/scheduler/integration.ts +46 -26
- package/packages/core/src/scheduler/scheduler.test.ts +9 -13
- package/packages/core/src/scheduler/types.ts +7 -2
- package/packages/core/src/security/Pairing.ts +1 -1
- package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
- package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
- package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
- package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
- package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
- package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
- package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
- package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
- package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
- package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
- package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
- package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
- package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
- package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
- package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
- package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
- package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
- package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
- package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
- package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
- package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
- package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
- package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
- package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
- package/packages/core/src/skills/skills.test.ts +52 -11
- package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
- package/packages/core/src/storage/bootstrap.ts +151 -0
- package/packages/core/src/storage/causal-events.ts +84 -0
- package/packages/core/src/storage/collections.ts +680 -0
- package/packages/core/src/storage/crypto.ts +205 -74
- package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
- package/packages/core/src/storage/hivedb.ts +61 -0
- package/packages/core/src/storage/index.ts +111 -17
- package/packages/core/src/storage/model-id.ts +53 -0
- package/packages/core/src/storage/onboarding.ts +540 -972
- package/packages/core/src/storage/reconcile.ts +238 -0
- package/packages/core/src/storage/seed.ts +572 -406
- package/packages/core/src/storage/usage.ts +285 -225
- package/packages/core/src/storage/user-email.ts +11 -0
- package/packages/core/src/swarm/AgentExecutor.ts +1 -1
- package/packages/core/src/swarm/EventBridge.ts +1 -1
- package/packages/core/src/swarm/index.ts +12 -9
- package/packages/core/src/tool-runtime/index.ts +146 -23
- package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
- package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
- package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
- package/packages/core/src/tools/agents/get-available-models.ts +36 -54
- package/packages/core/src/tools/agents/index.ts +784 -292
- package/packages/core/src/tools/api/api-request.test.ts +164 -0
- package/packages/core/src/tools/api/api-request.ts +174 -0
- package/packages/core/src/tools/api/index.ts +16 -0
- package/packages/core/src/tools/cli/index.ts +4 -0
- package/packages/core/src/tools/core/index.ts +281 -112
- package/packages/core/src/tools/cron/index.ts +121 -124
- package/packages/core/src/tools/index.ts +63 -78
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
- package/packages/core/src/tools/types.ts +3 -1
- package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
- package/packages/core/src/tools/web/browser-service.ts +5 -0
- package/packages/core/src/tools/web/browser-type.ts +3 -8
- package/packages/core/src/tools/web/index.ts +4 -4
- package/packages/core/src/voice/index.ts +89 -63
- package/packages/core/src/workers/agent.worker.ts +2 -2
- package/packages/core/src/workers/workers.test.ts +3 -10
- package/scripts/bump-version.ts +248 -0
- package/scripts/generate-skill-bundle.ts +108 -0
- package/test/agent-loop-terminal-synthesis.test.ts +32 -0
- package/test/catalog-agents-stay-enabled.test.ts +117 -0
- package/test/causal-events.test.ts +117 -0
- package/test/compaction.test.ts +105 -0
- package/test/context-compiler.test.ts +269 -0
- package/test/curator.test.ts +130 -0
- package/test/durable-queue.test.ts +114 -0
- package/test/harness-barrel.test.ts +64 -0
- package/test/hive-helpers.test.ts +130 -0
- package/test/hivedb-search.test.ts +189 -0
- package/test/internal-turns.test.ts +166 -0
- package/test/job-idempotency.test.ts +68 -0
- package/test/job-retry-backoff.test.ts +184 -0
- package/test/job-store.test.ts +381 -0
- package/test/llm-retry.test.ts +97 -0
- package/test/memory-perf.test.ts +774 -0
- package/test/minimal-loadout.test.ts +78 -0
- package/test/model-catalog.test.ts +105 -0
- package/test/preload.ts +12 -0
- package/test/reflector.test.ts +320 -0
- package/test/retention-cap.test.ts +91 -0
- package/test/retired-capabilities-pruned.test.ts +192 -0
- package/test/run-store.test.ts +355 -0
- package/test/scratchpad.test.ts +74 -0
- package/test/secrets-durability.test.ts +119 -0
- package/test/seed-model-reseed.test.ts +155 -0
- package/test/setup-agent-seed.test.ts +264 -0
- package/test/tool-inventory.test.ts +65 -0
- package/test/tool-runtime.test.ts +258 -0
- package/test/toon.test.ts +429 -0
- package/tsconfig.json +2 -0
- package/packages/core/src/ace/Curator.ts +0 -158
- package/packages/core/src/ace/Reflector.ts +0 -200
- package/packages/core/src/ace/index.ts +0 -4
- package/packages/core/src/agent/AgentRunner.ts +0 -711
- package/packages/core/src/agent/ContextCompiler.ts +0 -567
- package/packages/core/src/agent/ContextGuard.ts +0 -91
- package/packages/core/src/agent/ConversationStore.ts +0 -254
- package/packages/core/src/agent/Hooks.ts +0 -166
- package/packages/core/src/agent/StuckLoop.ts +0 -133
- package/packages/core/src/agent/providers/LLMClient.ts +0 -149
- package/packages/core/src/agent/providers/anthropic.ts +0 -212
- package/packages/core/src/agent/providers/openai-compat.ts +0 -231
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
- package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
- package/packages/core/src/agent/selectors/index.ts +0 -6
- package/packages/core/src/auth/auth.ts +0 -121
- package/packages/core/src/auth/index.ts +0 -1
- package/packages/core/src/canvas/CanvasManager.ts +0 -390
- package/packages/core/src/canvas/canvas-tools.ts +0 -448
- package/packages/core/src/harness/collections.ts +0 -98
- package/packages/core/src/harness/goal-verifier.ts +0 -141
- package/packages/core/src/harness/harness.test.ts +0 -236
- package/packages/core/src/harness/reconcile.ts +0 -149
- package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
- package/packages/core/src/multimodal/VisionService.ts +0 -293
- package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
- package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
- package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
- package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
- package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
- package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
- package/packages/core/src/scheduler/dag/errors.ts +0 -37
- package/packages/core/src/scheduler/dag/index.ts +0 -26
- package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
- package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
- package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
- package/packages/core/src/storage/HiveDBStorage.ts +0 -64
- package/packages/core/src/storage/SQLiteStorage.ts +0 -414
- package/packages/core/src/storage/hiveSeed.ts +0 -308
- package/packages/core/src/storage/hiveStorage.test.ts +0 -38
- package/packages/core/src/storage/schema.ts +0 -689
- package/packages/core/src/storage/storage.test.ts +0 -37
- package/packages/core/src/swarm/AgentBus.ts +0 -460
- package/packages/core/src/swarm/EventBus.ts +0 -169
- package/packages/core/src/swarm/WorkerPool.ts +0 -236
- package/packages/core/src/tools/bridge-events.ts +0 -26
- package/packages/core/src/tools/canvas/index.ts +0 -375
- package/packages/core/src/tools/codebridge/index.ts +0 -342
- package/packages/core/src/tools/meeting/index.ts +0 -353
- package/packages/core/src/tools/projects/index.ts +0 -37
- package/packages/core/src/tools/projects/project-create.ts +0 -94
- package/packages/core/src/tools/projects/project-done.ts +0 -66
- package/packages/core/src/tools/projects/project-fail.ts +0 -66
- package/packages/core/src/tools/projects/project-list.ts +0 -96
- package/packages/core/src/tools/projects/project-update.ts +0 -72
- package/packages/core/src/tools/projects/task-create.ts +0 -68
- package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
- package/packages/core/src/tools/projects/task-update.ts +0 -93
- package/packages/core/src/tools/voice/index.ts +0 -104
- package/packages/core/src/tools/web/api-request.test.ts +0 -170
- package/packages/core/src/tools/web/api-request.ts +0 -239
- package/test/setup-db.ts +0 -216
- /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
|
@@ -1,10 +1,73 @@
|
|
|
1
|
-
import { logger } from "../../utils/logger
|
|
2
|
-
import { sanitizeMessages } from "./interface"
|
|
1
|
+
import { logger } from "../../utils/logger"
|
|
2
|
+
import { sanitizeMessages, resolveMaxTokens, ensureArrayItems } from "./interface"
|
|
3
3
|
import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
|
|
4
|
-
import type { ContentPart, LLMMessage } from "
|
|
4
|
+
import type { ContentPart, LLMMessage } from "../llm-client"
|
|
5
5
|
|
|
6
6
|
const log = logger.child("llm-client")
|
|
7
7
|
|
|
8
|
+
/** Re-applies Gemini's structural invariants (INV-1/2/3) in place — needed both on the initial build and after compacting `contents` for a context-overflow retry. */
|
|
9
|
+
function enforceGeminiConstraints(contents: any[]): void {
|
|
10
|
+
while (contents.length > 0 && contents[0].role === "model") {
|
|
11
|
+
log.warn(`[llm-client] Gemini: removed leading model turn (no preceding user turn)`)
|
|
12
|
+
contents.shift()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let changed = true
|
|
16
|
+
let safetyLimit = 10
|
|
17
|
+
while (changed && safetyLimit-- > 0) {
|
|
18
|
+
changed = false
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < contents.length; i++) {
|
|
21
|
+
const turn = contents[i]
|
|
22
|
+
const prev = i > 0 ? contents[i - 1] : null
|
|
23
|
+
|
|
24
|
+
// INV-3: merge consecutive model turns
|
|
25
|
+
if (turn.role === "model" && prev?.role === "model") {
|
|
26
|
+
prev.parts.push(...(turn.parts ?? []))
|
|
27
|
+
contents.splice(i, 1)
|
|
28
|
+
i--
|
|
29
|
+
changed = true
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// INV-1: model(fc) must come after user
|
|
34
|
+
if (turn.role === "model") {
|
|
35
|
+
const hasFc = turn.parts?.some((p: any) => p.functionCall)
|
|
36
|
+
if (hasFc && prev?.role !== "user") {
|
|
37
|
+
turn.parts = (turn.parts ?? []).filter((p: any) => !p.functionCall)
|
|
38
|
+
log.warn(`[llm-client] Gemini: stripped functionCall not after user turn (i=${i})`)
|
|
39
|
+
if (turn.parts.length === 0) { contents.splice(i, 1); i-- }
|
|
40
|
+
changed = true
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// INV-2: user(fr) must come after model(fc)
|
|
46
|
+
if (turn.role === "user") {
|
|
47
|
+
const hasFr = turn.parts?.some((p: any) => p.functionResponse)
|
|
48
|
+
const prevHasFc = prev?.role === "model" && prev?.parts?.some((p: any) => p.functionCall)
|
|
49
|
+
if (hasFr && !prevHasFc) {
|
|
50
|
+
turn.parts = (turn.parts ?? []).filter((p: any) => !p.functionResponse)
|
|
51
|
+
log.warn(`[llm-client] Gemini: stripped orphaned functionResponse (i=${i})`)
|
|
52
|
+
if (turn.parts.length === 0) { contents.splice(i, 1); i-- }
|
|
53
|
+
changed = true
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (safetyLimit <= 0) {
|
|
61
|
+
log.error(`[llm-client] Gemini: constraint enforcement loop exhausted — message history may still violate Gemini constraints`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Best-effort heuristic — Gemini has no structured error code for context overflow like llama.cpp's n_ctx. */
|
|
66
|
+
function isGeminiContextOverflowError(err: any): boolean {
|
|
67
|
+
const msg = (err?.message ?? String(err) ?? "").toLowerCase()
|
|
68
|
+
return msg.includes("token") && (msg.includes("exceed") || msg.includes("too long") || msg.includes("maximum"))
|
|
69
|
+
}
|
|
70
|
+
|
|
8
71
|
export class GeminiProvider implements LLMProvider {
|
|
9
72
|
private _convertContentPart(part: ContentPart): any {
|
|
10
73
|
switch (part.type) {
|
|
@@ -90,78 +153,43 @@ export class GeminiProvider implements LLMProvider {
|
|
|
90
153
|
|
|
91
154
|
// Gemini constraint enforcement
|
|
92
155
|
const contents: any[] = rawContents
|
|
93
|
-
|
|
94
|
-
while (contents.length > 0 && contents[0].role === "model") {
|
|
95
|
-
log.warn(`[llm-client] Gemini: removed leading model turn (no preceding user turn)`)
|
|
96
|
-
contents.shift()
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let changed = true
|
|
100
|
-
let safetyLimit = 10
|
|
101
|
-
while (changed && safetyLimit-- > 0) {
|
|
102
|
-
changed = false
|
|
103
|
-
|
|
104
|
-
for (let i = 0; i < contents.length; i++) {
|
|
105
|
-
const turn = contents[i]
|
|
106
|
-
const prev = i > 0 ? contents[i - 1] : null
|
|
107
|
-
|
|
108
|
-
// INV-3: merge consecutive model turns
|
|
109
|
-
if (turn.role === "model" && prev?.role === "model") {
|
|
110
|
-
prev.parts.push(...(turn.parts ?? []))
|
|
111
|
-
contents.splice(i, 1)
|
|
112
|
-
i--
|
|
113
|
-
changed = true
|
|
114
|
-
continue
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// INV-1: model(fc) must come after user
|
|
118
|
-
if (turn.role === "model") {
|
|
119
|
-
const hasFc = turn.parts?.some((p: any) => p.functionCall)
|
|
120
|
-
if (hasFc && prev?.role !== "user") {
|
|
121
|
-
turn.parts = (turn.parts ?? []).filter((p: any) => !p.functionCall)
|
|
122
|
-
log.warn(`[llm-client] Gemini: stripped functionCall not after user turn (i=${i})`)
|
|
123
|
-
if (turn.parts.length === 0) { contents.splice(i, 1); i-- }
|
|
124
|
-
changed = true
|
|
125
|
-
continue
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// INV-2: user(fr) must come after model(fc)
|
|
130
|
-
if (turn.role === "user") {
|
|
131
|
-
const hasFr = turn.parts?.some((p: any) => p.functionResponse)
|
|
132
|
-
const prevHasFc = prev?.role === "model" && prev?.parts?.some((p: any) => p.functionCall)
|
|
133
|
-
if (hasFr && !prevHasFc) {
|
|
134
|
-
turn.parts = (turn.parts ?? []).filter((p: any) => !p.functionResponse)
|
|
135
|
-
log.warn(`[llm-client] Gemini: stripped orphaned functionResponse (i=${i})`)
|
|
136
|
-
if (turn.parts.length === 0) { contents.splice(i, 1); i-- }
|
|
137
|
-
changed = true
|
|
138
|
-
continue
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (safetyLimit <= 0) {
|
|
145
|
-
log.error(`[llm-client] Gemini: constraint enforcement loop exhausted — message history may still violate Gemini constraints`)
|
|
146
|
-
}
|
|
156
|
+
enforceGeminiConstraints(contents)
|
|
147
157
|
|
|
148
158
|
const config: any = {}
|
|
149
159
|
if (systemText) config.systemInstruction = systemText
|
|
150
|
-
|
|
160
|
+
const maxTokens = resolveMaxTokens(options.maxTokens, options.contextWindow)
|
|
161
|
+
if (maxTokens) config.maxOutputTokens = maxTokens
|
|
151
162
|
if (options.temperature !== undefined) config.temperature = options.temperature
|
|
163
|
+
if (options.thinking?.enabled) config.thinkingConfig = { includeThoughts: true }
|
|
164
|
+
if (options.signal) config.abortSignal = options.signal
|
|
152
165
|
if (options.tools?.length) {
|
|
153
166
|
config.tools = [{
|
|
154
167
|
functionDeclarations: options.tools.map((t) => ({
|
|
155
168
|
name: t.function.name,
|
|
156
169
|
description: t.function.description,
|
|
157
|
-
parameters: t.function.parameters,
|
|
170
|
+
parameters: ensureArrayItems(t.function.parameters),
|
|
158
171
|
})),
|
|
159
172
|
}]
|
|
160
173
|
}
|
|
161
174
|
|
|
162
175
|
log.info(`[llm-client] gemini/${options.model} — ${contents.length} turns, ${options.tools?.length ?? 0} tools`)
|
|
163
176
|
|
|
164
|
-
|
|
177
|
+
let response
|
|
178
|
+
try {
|
|
179
|
+
response = await ai.models.generateContent({ model: options.model, contents, config })
|
|
180
|
+
} catch (err: any) {
|
|
181
|
+
if (!isGeminiContextOverflowError(err)) throw err
|
|
182
|
+
log.warn(`[llm-client] gemini: context overflow — compacting turns and retrying`)
|
|
183
|
+
const originalCount = contents.length
|
|
184
|
+
// Keep only the last ~33% of turns, then re-apply the structural invariants
|
|
185
|
+
// (compacting can orphan a functionCall/functionResponse pairing).
|
|
186
|
+
const keepRatio = Math.max(1, Math.floor(contents.length / 3))
|
|
187
|
+
contents.splice(0, contents.length - keepRatio)
|
|
188
|
+
enforceGeminiConstraints(contents)
|
|
189
|
+
if (config.maxOutputTokens) config.maxOutputTokens = Math.min(config.maxOutputTokens, 4096)
|
|
190
|
+
log.info(`[llm-client] gemini: compacted ${originalCount} turns → ${contents.length} turns, maxOutputTokens=${config.maxOutputTokens}`)
|
|
191
|
+
response = await ai.models.generateContent({ model: options.model, contents, config })
|
|
192
|
+
}
|
|
165
193
|
|
|
166
194
|
const candidate = response.candidates?.[0]
|
|
167
195
|
|
|
@@ -181,9 +209,14 @@ export class GeminiProvider implements LLMProvider {
|
|
|
181
209
|
const parts: any[] = candidate?.content?.parts ?? []
|
|
182
210
|
|
|
183
211
|
let content = ""
|
|
212
|
+
let reasoning_content = ""
|
|
184
213
|
const tool_calls: LLMToolCall[] = []
|
|
185
214
|
|
|
186
215
|
for (const part of parts) {
|
|
216
|
+
if (part.thought && part.text) {
|
|
217
|
+
reasoning_content += part.text
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
187
220
|
if (part.text) content += part.text
|
|
188
221
|
if (part.functionCall) {
|
|
189
222
|
tool_calls.push({
|
|
@@ -200,9 +233,14 @@ export class GeminiProvider implements LLMProvider {
|
|
|
200
233
|
: candidate?.finishReason === "MAX_TOKENS" ? "max_tokens"
|
|
201
234
|
: "stop"
|
|
202
235
|
|
|
236
|
+
// Gemini has no streaming path (generateContent, not generateContentStream),
|
|
237
|
+
// so any thought content only arrives here — emit it as a single chunk.
|
|
238
|
+
if (reasoning_content) options.onReasoningToken?.(reasoning_content)
|
|
239
|
+
|
|
203
240
|
const usageMeta = response.usageMetadata
|
|
204
241
|
return {
|
|
205
242
|
content,
|
|
243
|
+
reasoning_content: reasoning_content || undefined,
|
|
206
244
|
tool_calls: tool_calls.length ? tool_calls : undefined,
|
|
207
245
|
stop_reason,
|
|
208
246
|
usage: usageMeta ? {
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { logger } from "../../utils/logger"
|
|
2
|
+
import { OpenAICompatBase } from "./openai-compat-base"
|
|
3
|
+
import type { LLMCallOptions, LLMResponse } from "./interface"
|
|
4
|
+
|
|
5
|
+
const log = logger.child("llm-client")
|
|
6
|
+
|
|
7
|
+
const DEFAULT_BASE = "https://llm.hiveagents.io"
|
|
8
|
+
|
|
9
|
+
/** Contexto por defecto que se solicita al backend de HiveAgents al cargar un modelo. */
|
|
10
|
+
const HIVEAGENTS_DEFAULT_LOAD_CTX = 50000
|
|
11
|
+
|
|
12
|
+
// Cloudflare blocks requests with the OpenAI SDK User-Agent and x-stainless-* fingerprint headers.
|
|
13
|
+
const BLOCKED_HEADERS = [
|
|
14
|
+
"user-agent",
|
|
15
|
+
"x-stainless-lang",
|
|
16
|
+
"x-stainless-package-version",
|
|
17
|
+
"x-stainless-runtime",
|
|
18
|
+
"x-stainless-runtime-version",
|
|
19
|
+
"x-stainless-arch",
|
|
20
|
+
"x-stainless-os",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
export interface HiveAgentsLoadResult {
|
|
24
|
+
success: boolean
|
|
25
|
+
loading?: boolean
|
|
26
|
+
error?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface HiveAgentsStatusResult {
|
|
30
|
+
loaded: boolean
|
|
31
|
+
model?: { name?: string; ctx?: number; n_ctx?: number }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getApiBase(baseUrl?: string): string {
|
|
35
|
+
return (baseUrl?.replace(/\/v1\/?$/, "") || DEFAULT_BASE)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getAuthHeaders(apiKey: string): Record<string, string> {
|
|
39
|
+
return {
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
Authorization: `Bearer ${apiKey}`,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Solicita la carga de un modelo GGUF en el backend de HiveAgents.
|
|
47
|
+
* Siempre pide ctx=HIVEAGENTS_LOAD_CTX para maximizar la ventana de contexto disponible.
|
|
48
|
+
*/
|
|
49
|
+
/** Timeout para la petición de carga. Menor al límite de Cloudflare (100s) para evitar 524. */
|
|
50
|
+
const HIVEAGENTS_LOAD_FETCH_TIMEOUT_MS = 90000
|
|
51
|
+
|
|
52
|
+
export async function loadHiveAgentsModel(
|
|
53
|
+
modelId: string,
|
|
54
|
+
apiKey: string,
|
|
55
|
+
baseUrl?: string,
|
|
56
|
+
ctx = HIVEAGENTS_DEFAULT_LOAD_CTX
|
|
57
|
+
): Promise<HiveAgentsLoadResult> {
|
|
58
|
+
const apiBase = getApiBase(baseUrl)
|
|
59
|
+
const headers = getAuthHeaders(apiKey)
|
|
60
|
+
const loadBody = {
|
|
61
|
+
model: modelId,
|
|
62
|
+
config: { ctx },
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
log.info(`[hiveagents] → POST ${apiBase}/api/load`)
|
|
67
|
+
log.info(`[hiveagents] → Body: ${JSON.stringify(loadBody)}`)
|
|
68
|
+
const res = await fetch(`${apiBase}/api/load`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers,
|
|
71
|
+
body: JSON.stringify(loadBody),
|
|
72
|
+
signal: AbortSignal.timeout(HIVEAGENTS_LOAD_FETCH_TIMEOUT_MS),
|
|
73
|
+
})
|
|
74
|
+
const responseText = await res.text().catch(() => "")
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
// 524 = Cloudflare timeout. El backend puede seguir cargando, así que lo tratamos como "en progreso".
|
|
77
|
+
// 530 = Cloudflare Tunnel error (origen no resoluble); también puede ser transitorio.
|
|
78
|
+
const isTransientCloudflareError = [502, 503, 504, 524, 530].includes(res.status)
|
|
79
|
+
if (isTransientCloudflareError) {
|
|
80
|
+
log.warn(`[hiveagents] ← Load request hit transient error (HTTP ${res.status}); backend may still be loading`)
|
|
81
|
+
return { success: true, loading: true }
|
|
82
|
+
}
|
|
83
|
+
log.error(`[hiveagents] ← Load failed: HTTP ${res.status} ${res.statusText} — ${responseText}`)
|
|
84
|
+
return { success: false, error: `Load failed: HTTP ${res.status} — ${responseText || res.statusText}` }
|
|
85
|
+
}
|
|
86
|
+
log.info(`[hiveagents] ← Load accepted: ${responseText}`)
|
|
87
|
+
return { success: true }
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const msg = (err as Error).message || ""
|
|
90
|
+
// AbortError por timeout interno: el backend puede seguir cargando.
|
|
91
|
+
if (msg.includes("timed out") || msg.includes("abort") || msg.includes("AbortError")) {
|
|
92
|
+
log.warn(`[hiveagents] ← Load request timed out after ${HIVEAGENTS_LOAD_FETCH_TIMEOUT_MS}ms; backend may still be loading`)
|
|
93
|
+
return { success: true, loading: true }
|
|
94
|
+
}
|
|
95
|
+
return { success: false, error: msg }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Consulta el estado actual del backend de HiveAgents.
|
|
101
|
+
*/
|
|
102
|
+
export async function getHiveAgentsModelStatus(
|
|
103
|
+
apiKey: string,
|
|
104
|
+
baseUrl?: string
|
|
105
|
+
): Promise<HiveAgentsStatusResult> {
|
|
106
|
+
const apiBase = getApiBase(baseUrl)
|
|
107
|
+
const headers = getAuthHeaders(apiKey)
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch(`${apiBase}/api/status`, { headers })
|
|
111
|
+
if (!res.ok) return { loaded: false }
|
|
112
|
+
const data = await res.json() as any
|
|
113
|
+
return {
|
|
114
|
+
loaded: !!data.loaded,
|
|
115
|
+
model: data.model,
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
return { loaded: false }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class HiveAgentsProvider extends OpenAICompatBase {
|
|
123
|
+
private _currentModelId = ""
|
|
124
|
+
|
|
125
|
+
constructor() { super("hiveagents") }
|
|
126
|
+
|
|
127
|
+
private _isGemma4(modelId: string): boolean { return /^gemma-?4/i.test(modelId) }
|
|
128
|
+
private _isQwen3(modelId: string): boolean { return /^qwen3?/i.test(modelId) }
|
|
129
|
+
private _isAgentWorld(modelId: string): boolean { return /agentworld/i.test(modelId) }
|
|
130
|
+
|
|
131
|
+
// Cloudflare WAF blocks requests carrying x-stainless-* headers from the OpenAI SDK.
|
|
132
|
+
// Strip them via a custom fetch wrapper so they never reach the WAF.
|
|
133
|
+
protected async resolveOpenAIClient(apiKey: string, baseURL: string | undefined): Promise<any> {
|
|
134
|
+
const { default: OpenAI } = await import("openai")
|
|
135
|
+
return new OpenAI({
|
|
136
|
+
apiKey,
|
|
137
|
+
baseURL,
|
|
138
|
+
fetch: async (url: RequestInfo | URL, init?: RequestInit) => {
|
|
139
|
+
const headers = new Headers(init?.headers as HeadersInit | undefined)
|
|
140
|
+
for (const h of BLOCKED_HEADERS) headers.delete(h)
|
|
141
|
+
|
|
142
|
+
// Debug: log exact request so we can replicate with curl
|
|
143
|
+
const headersObj: Record<string, string> = {}
|
|
144
|
+
headers.forEach((v, k) => { headersObj[k] = k.toLowerCase() === "authorization" ? `Bearer ••••${v.slice(-6)}` : v })
|
|
145
|
+
log.info(`[hiveagents] → POST ${url}`)
|
|
146
|
+
log.info(`[hiveagents] → Headers: ${JSON.stringify(headersObj)}`)
|
|
147
|
+
if (init?.body) {
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(init.body as string)
|
|
150
|
+
const summary = { model: parsed.model, messages: parsed.messages?.length, tools: parsed.tools?.length, max_tokens: parsed.max_tokens, temperature: parsed.temperature, tool_choice: parsed.tool_choice, extra_body: parsed.extra_body }
|
|
151
|
+
log.info(`[hiveagents] → Body summary: ${JSON.stringify(summary)}`)
|
|
152
|
+
} catch { /* ignore */ }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const res = await fetch(url, { ...init, headers })
|
|
156
|
+
log.info(`[hiveagents] ← Response: ${res.status} ${res.statusText}`)
|
|
157
|
+
return res
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async call(options: LLMCallOptions): Promise<LLMResponse> {
|
|
163
|
+
const realModelId = options.model.replace(/^hiveagents\//i, "")
|
|
164
|
+
if (realModelId && realModelId !== "local") {
|
|
165
|
+
await this._ensureModelLoaded(realModelId, options)
|
|
166
|
+
}
|
|
167
|
+
this._currentModelId = realModelId
|
|
168
|
+
|
|
169
|
+
let callOptions = { ...options, model: "hiveagents/local" }
|
|
170
|
+
|
|
171
|
+
// Qwen3: inject /no_think when thinking is explicitly disabled
|
|
172
|
+
if (this._isQwen3(realModelId) && options.thinking?.enabled === false) {
|
|
173
|
+
const msgs = callOptions.messages.map(m => ({ ...m }))
|
|
174
|
+
const sysMsg = msgs.find(m => m.role === "system")
|
|
175
|
+
if (sysMsg && typeof sysMsg.content === "string") {
|
|
176
|
+
if (!sysMsg.content.startsWith("/no_think"))
|
|
177
|
+
sysMsg.content = "/no_think\n" + sysMsg.content
|
|
178
|
+
} else {
|
|
179
|
+
msgs.unshift({ role: "system", content: "/no_think" })
|
|
180
|
+
}
|
|
181
|
+
callOptions = { ...callOptions, messages: msgs }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return super.call(callOptions)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Gemma 4 and Qwen-AgentWorld: inject chat_template_kwargs.enable_thinking via extra_body.
|
|
188
|
+
// Default is true (thinking ON) when options.thinking is not set.
|
|
189
|
+
protected modifyRequestBody(body: any, options: LLMCallOptions): any {
|
|
190
|
+
if (this._isGemma4(this._currentModelId) || this._isAgentWorld(this._currentModelId)) {
|
|
191
|
+
const enableThinking = options.thinking?.enabled !== false
|
|
192
|
+
body.extra_body = {
|
|
193
|
+
...(body.extra_body ?? {}),
|
|
194
|
+
chat_template_kwargs: { enable_thinking: enableThinking },
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return body
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Fallback defensivo: si la UI no cargó el modelo previamente,
|
|
202
|
+
* lo intenta cargar justo antes de inferir.
|
|
203
|
+
*/
|
|
204
|
+
private async _ensureModelLoaded(modelId: string, options: LLMCallOptions): Promise<void> {
|
|
205
|
+
const status = await getHiveAgentsModelStatus(options.apiKey, options.baseUrl)
|
|
206
|
+
if (status.loaded && status.model?.name === modelId) {
|
|
207
|
+
log.info(`[hiveagents] Model ${modelId} already loaded`)
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
// The model's own context_window (BD, via resolveProviderConfig) is the source
|
|
211
|
+
// of truth for how much context to request when mounting it — only fall back
|
|
212
|
+
// to the generic default when it's genuinely unavailable.
|
|
213
|
+
const ctx = options.contextWindow || HIVEAGENTS_DEFAULT_LOAD_CTX
|
|
214
|
+
log.warn(`[hiveagents] Model ${modelId} not loaded. Triggering load with ctx=${ctx}`)
|
|
215
|
+
const result = await loadHiveAgentsModel(modelId, options.apiKey, options.baseUrl, ctx)
|
|
216
|
+
if (!result.success) {
|
|
217
|
+
log.warn(`[hiveagents] Auto-load failed for ${modelId}: ${result.error}`)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
protected injectToolsIntoPrompt(body: any, preparedTools: any[]): void {
|
|
222
|
+
// When the backend already receives native OpenAI-style tools, do not confuse
|
|
223
|
+
// the model with an alternate <tool_call> text format. HiveAgents supports
|
|
224
|
+
// native tool_calls when the model/chat-template supports them.
|
|
225
|
+
if (body.tools && body.tools.length > 0) {
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Fallback for models/backends that do not expose native tool calling:
|
|
230
|
+
// inject the tool descriptions as text and instruct the model to emit
|
|
231
|
+
// a single JSON block wrapped in <tool_call> tags.
|
|
232
|
+
const toolDescriptions = preparedTools.map(t => JSON.stringify(t.function)).join("\n")
|
|
233
|
+
const instruction = [
|
|
234
|
+
"You have access to the following tools.",
|
|
235
|
+
"When you need to use a tool, output EXACTLY one JSON block wrapped in <tool_call> tags and NOTHING ELSE in that turn:",
|
|
236
|
+
"",
|
|
237
|
+
"<tool_call>",
|
|
238
|
+
'{"name": "browser_navigate", "arguments": {"url": "https://example.com"}}',
|
|
239
|
+
"</tool_call>",
|
|
240
|
+
"",
|
|
241
|
+
"Use the exact tool name and argument names from the list below. Do not add extra text, markdown, or explanations inside the tool_call block.",
|
|
242
|
+
"",
|
|
243
|
+
"Tools:",
|
|
244
|
+
toolDescriptions,
|
|
245
|
+
].join("\n")
|
|
246
|
+
const sysMsg = body.messages.find((m: any) => m.role === "system")
|
|
247
|
+
if (sysMsg) {
|
|
248
|
+
sysMsg.content += "\n\n" + instruction
|
|
249
|
+
} else {
|
|
250
|
+
body.messages.unshift({ role: "system", content: instruction })
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Shared types and utilities for LLM providers.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { LLMCallOptions, LLMMessage, LLMResponse, LLMToolCall, ContentPart } from "
|
|
6
|
-
export type { LLMCallOptions, LLMMessage, LLMResponse, LLMToolCall, ContentPart }
|
|
5
|
+
import type { LLMCallOptions, LLMMessage, LLMResponse, LLMToolCall, ContentPart, ThinkingBlock } from "../llm-client"
|
|
6
|
+
export type { LLMCallOptions, LLMMessage, LLMResponse, LLMToolCall, ContentPart, ThinkingBlock }
|
|
7
7
|
|
|
8
|
-
import { logger } from "../../utils/logger
|
|
8
|
+
import { logger } from "../../utils/logger"
|
|
9
9
|
const log = logger.child("llm-client")
|
|
10
10
|
|
|
11
11
|
// ─── Provider interface ────────────────────────────────────────────────────────
|
|
@@ -17,7 +17,7 @@ export interface LLMProvider {
|
|
|
17
17
|
// ─── Shared constants ─────────────────────────────────────────────────────────
|
|
18
18
|
|
|
19
19
|
// Models that only accept temperature=1 (reasoning/thinking models).
|
|
20
|
-
|
|
20
|
+
const FIXED_TEMPERATURE_1_MODELS = new Set(["kimi-k2.5", "kimi-k2", "kimi-k2-5"])
|
|
21
21
|
|
|
22
22
|
export const OPENAI_COMPAT_BASE_URLS: Record<string, string> = {
|
|
23
23
|
groq: "https://api.groq.com/openai/v1",
|
|
@@ -25,9 +25,16 @@ export const OPENAI_COMPAT_BASE_URLS: Record<string, string> = {
|
|
|
25
25
|
openrouter: "https://openrouter.ai/api/v1",
|
|
26
26
|
deepseek: "https://api.deepseek.com/v1",
|
|
27
27
|
kimi: "https://api.moonshot.ai/v1",
|
|
28
|
-
"local-llama": "http://localhost:8080/v1",
|
|
29
28
|
nvidia: "https://integrate.api.nvidia.com/v1",
|
|
30
|
-
qwen: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
|
29
|
+
qwen: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
|
30
|
+
minimax: "https://api.minimaxi.com/v1",
|
|
31
|
+
"opencode-go": "https://opencode.ai/zen/go/v1",
|
|
32
|
+
hiveagents: "https://llm.hiveagents.io/v1",
|
|
33
|
+
"z-ai": "https://api.z.ai/api/paas/v4",
|
|
34
|
+
// OJO: `.ai`, no `.cn`. Son dos plataformas con cuentas y tokens distintos, y
|
|
35
|
+
// un token de la internacional da 401 contra el endpoint chino aunque el
|
|
36
|
+
// listado de modelos responda 200 en ambos (ese listado es público).
|
|
37
|
+
modelscope: "https://api-inference.modelscope.ai/v1",
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
// ─── Provider profiles ────────────────────────────────────────────────────────
|
|
@@ -56,16 +63,24 @@ const DEFAULT_PROFILE: ProviderProfile = {
|
|
|
56
63
|
retryWithoutToolsOnCodes: [],
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
const PROVIDER_PROFILES: Record<string, ProviderProfile> = {
|
|
60
67
|
openai: { ...DEFAULT_PROFILE, normalizeToolNames: true },
|
|
61
68
|
kimi: { ...DEFAULT_PROFILE, normalizeToolNames: true, disableParallelToolCalls: true, retryWithoutToolsOnCodes: [422] },
|
|
62
69
|
deepseek: { ...DEFAULT_PROFILE, normalizeToolNames: true },
|
|
63
70
|
groq: { ...DEFAULT_PROFILE, normalizeToolNames: true, retryWithoutToolsOnCodes: [400, 422] },
|
|
64
|
-
|
|
71
|
+
// tool_choice "any" *forces* a tool call on every request per docs.mistral.ai —
|
|
72
|
+
// it is not Mistral's spelling of "auto", which Mistral supports and defaults to.
|
|
73
|
+
// With "any" the agent could never answer in plain text: every turn had to end
|
|
74
|
+
// in a tool call, so the loop only stopped when it hit max iterations.
|
|
75
|
+
mistral: { ...DEFAULT_PROFILE, normalizeToolNames: true, stripAdditionalProperties: true },
|
|
65
76
|
openrouter: { ...DEFAULT_PROFILE, normalizeToolNames: true, retryWithoutToolsOnCodes: [400, 422] },
|
|
66
|
-
nvidia: { ...DEFAULT_PROFILE, normalizeToolNames: true },
|
|
77
|
+
nvidia: { ...DEFAULT_PROFILE, normalizeToolNames: true, retryWithoutToolsOnCodes: [400, 422] },
|
|
78
|
+
"z-ai": { ...DEFAULT_PROFILE, normalizeToolNames: true, retryWithoutToolsOnCodes: [400, 422] },
|
|
79
|
+
modelscope: { ...DEFAULT_PROFILE, normalizeToolNames: true, retryWithoutToolsOnCodes: [400, 422] },
|
|
67
80
|
qwen: { ...DEFAULT_PROFILE, normalizeToolNames: true, retryWithoutToolsOnCodes: [400, 422] },
|
|
68
|
-
|
|
81
|
+
minimax: { ...DEFAULT_PROFILE, normalizeToolNames: true, retryWithoutToolsOnCodes: [400, 422] },
|
|
82
|
+
"opencode-go": { ...DEFAULT_PROFILE, normalizeToolNames: true, retryWithoutToolsOnCodes: [400, 422] },
|
|
83
|
+
hiveagents: { ...DEFAULT_PROFILE, retryWithoutToolsOnCodes: [400, 422] },
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
export function getProviderProfile(provider: string): ProviderProfile {
|
|
@@ -74,7 +89,7 @@ export function getProviderProfile(provider: string): ProviderProfile {
|
|
|
74
89
|
|
|
75
90
|
// ─── Models that don't support tool calling ───────────────────────────────────
|
|
76
91
|
|
|
77
|
-
|
|
92
|
+
const NO_TOOL_MODELS = new Set([
|
|
78
93
|
"deepseek-reasoner",
|
|
79
94
|
"deepseek/deepseek-r1:free",
|
|
80
95
|
])
|
|
@@ -106,8 +121,34 @@ export function normalizeToolSchema(
|
|
|
106
121
|
schema: Record<string, unknown>,
|
|
107
122
|
profile: ProviderProfile
|
|
108
123
|
): Record<string, unknown> {
|
|
109
|
-
|
|
110
|
-
|
|
124
|
+
const withItems = ensureArrayItems(schema)
|
|
125
|
+
if (!profile.stripAdditionalProperties) return withItems
|
|
126
|
+
return deepStripSchema(withItems)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Recursively ensures every `{ type: "array" }` node in a JSON Schema has an
|
|
131
|
+
* `items` sub-schema. `items` is optional per the JSON Schema spec, but some
|
|
132
|
+
* providers' function-calling validators (Gemini in particular) reject arrays
|
|
133
|
+
* without one — found via a real Gemini call rejecting a tool with a bare
|
|
134
|
+
* `datos: { type: "array" }` field. An empty `{}` items schema (accept
|
|
135
|
+
* anything) is always a safe, permissive default, so this runs unconditionally
|
|
136
|
+
* for every provider, not just ones with a matching profile flag — the same
|
|
137
|
+
* schema bug in an MCP-provided tool (outside hive's control) would break the
|
|
138
|
+
* exact same way, so this has to protect the wire path, not just hive's own
|
|
139
|
+
* tool definitions.
|
|
140
|
+
*/
|
|
141
|
+
export function ensureArrayItems(obj: unknown): any {
|
|
142
|
+
if (typeof obj !== "object" || obj === null) return obj
|
|
143
|
+
if (Array.isArray(obj)) return obj.map(ensureArrayItems)
|
|
144
|
+
const result: any = {}
|
|
145
|
+
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
|
146
|
+
result[k] = ensureArrayItems(v)
|
|
147
|
+
}
|
|
148
|
+
if (result.type === "array" && !result.items) {
|
|
149
|
+
result.items = {}
|
|
150
|
+
}
|
|
151
|
+
return result
|
|
111
152
|
}
|
|
112
153
|
|
|
113
154
|
function deepStripSchema(obj: unknown): any {
|
|
@@ -121,6 +162,25 @@ function deepStripSchema(obj: unknown): any {
|
|
|
121
162
|
return result
|
|
122
163
|
}
|
|
123
164
|
|
|
165
|
+
// ─── Output token budget ───────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
const OUTPUT_RESERVE_RATIO = 0.15
|
|
168
|
+
const MAX_OUTPUT_TOKENS_CEILING = 32768
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Derives the output token budget from the model's context_window (BD), reserving
|
|
172
|
+
* a fraction of it for generation. An explicit maxTokens always wins. The ceiling
|
|
173
|
+
* only guards against a misconfigured/absurd context_window — it must stay well
|
|
174
|
+
* above what any real seeded model's 15% reserve would produce (200k window → 30k),
|
|
175
|
+
* or every large-context model silently gets clamped to the same fixed number
|
|
176
|
+
* regardless of what's actually configured.
|
|
177
|
+
*/
|
|
178
|
+
export function resolveMaxTokens(explicitMaxTokens?: number, contextWindow?: number): number | undefined {
|
|
179
|
+
if (explicitMaxTokens) return explicitMaxTokens
|
|
180
|
+
if (!contextWindow) return undefined
|
|
181
|
+
return Math.min(MAX_OUTPUT_TOKENS_CEILING, Math.floor(contextWindow * OUTPUT_RESERVE_RATIO))
|
|
182
|
+
}
|
|
183
|
+
|
|
124
184
|
// ─── Temperature constraints ──────────────────────────────────────────────────
|
|
125
185
|
|
|
126
186
|
/**
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { OpenAICompatBase } from "./openai-compat-base"
|
|
2
|
+
|
|
3
|
+
export class KimiProvider extends OpenAICompatBase {
|
|
4
|
+
constructor() { super("kimi") }
|
|
5
|
+
|
|
6
|
+
/** Kimi K2 thinking mode returns reasoning_content that must be round-tripped. */
|
|
7
|
+
protected needsReasoningRoundtrip(): boolean { return true }
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { OpenAICompatBase } from "./openai-compat-base"
|
|
2
|
+
|
|
3
|
+
export class MiniMaxProvider extends OpenAICompatBase {
|
|
4
|
+
static readonly secretKey = "MINIMAX_API_KEY"
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
super("minimax")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
protected needsReasoningRoundtrip(): boolean {
|
|
11
|
+
return true
|
|
12
|
+
}
|
|
13
|
+
}
|