@johpaz/hive-sdk 0.1.4 → 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.
Files changed (272) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +78 -23
  3. package/bunfig.toml +4 -2
  4. package/docs/API-AGENTS.md +78 -27
  5. package/docs/API-CONTEXT-COMPILER.md +31 -34
  6. package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
  7. package/docs/HIVE-HARNESS.md +1 -1
  8. package/docs/INDEX.md +4 -4
  9. package/docs/TEMPLATE-HIVE-APP.md +10 -10
  10. package/package.json +9 -4
  11. package/packages/cli/package.json +2 -2
  12. package/packages/cli/src/commands/create-app.test.ts +36 -7
  13. package/packages/cli/src/commands/init.ts +3 -3
  14. package/packages/cli/src/commands/run.ts +1 -1
  15. package/packages/cli/src/commands/test.ts +37 -25
  16. package/packages/cli/src/commands/trace.ts +30 -28
  17. package/packages/cli/templates/hive-app/.env.example +10 -2
  18. package/packages/cli/templates/hive-app/README.md +103 -0
  19. package/packages/cli/templates/hive-app/hive.config.ts +9 -3
  20. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
  21. package/packages/cli/templates/hive-app/src/main.ts +12 -19
  22. package/packages/core/package.json +5 -4
  23. package/packages/core/src/agent/acceptance-checks.ts +166 -0
  24. package/packages/core/src/agent/agent-catalog.ts +348 -0
  25. package/packages/core/src/agent/agent-loop.ts +1373 -0
  26. package/packages/core/src/agent/capability-search.ts +186 -0
  27. package/packages/core/src/agent/catalog-selector.ts +103 -0
  28. package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
  29. package/packages/core/src/agent/context-compiler.ts +689 -0
  30. package/packages/core/src/agent/conversation-store.ts +381 -0
  31. package/packages/core/src/agent/curator.ts +276 -0
  32. package/packages/core/src/agent/delegation-runtime.ts +241 -0
  33. package/packages/core/src/agent/goal-runner.ts +323 -0
  34. package/packages/core/src/agent/index.ts +17 -12
  35. package/packages/core/src/agent/llm-client.ts +266 -0
  36. package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
  37. package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
  38. package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
  39. package/packages/core/src/agent/llm-providers/groq.ts +5 -0
  40. package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
  41. package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
  42. package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
  43. package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
  44. package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
  45. package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
  46. package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
  47. package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
  48. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
  49. package/packages/core/src/agent/llm-providers/openai.ts +5 -0
  50. package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
  51. package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
  52. package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
  53. package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
  54. package/packages/core/src/agent/minimal-loadout.ts +47 -0
  55. package/packages/core/src/agent/playbook-selector.ts +119 -0
  56. package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
  57. package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
  58. package/packages/core/src/agent/providers/index.ts +35 -16
  59. package/packages/core/src/agent/reflector.ts +320 -0
  60. package/packages/core/src/agent/routing-intent.ts +22 -0
  61. package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
  62. package/packages/core/src/{harness → agent}/run-store.ts +142 -81
  63. package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
  64. package/packages/core/src/agent/skill-selector.ts +374 -0
  65. package/packages/core/src/agent/stuck-loop.ts +209 -0
  66. package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
  67. package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
  68. package/packages/core/src/api/createAgent.test.ts +139 -27
  69. package/packages/core/src/api/createAgent.ts +232 -44
  70. package/packages/core/src/artifacts/store.ts +162 -0
  71. package/packages/core/src/canvas/canvas-manager.ts +161 -0
  72. package/packages/core/src/canvas/canvas.test.ts +8 -4
  73. package/packages/core/src/canvas/emitter.ts +131 -80
  74. package/packages/core/src/canvas/index.ts +1 -3
  75. package/packages/core/src/channels/base.ts +9 -1
  76. package/packages/core/src/channels/discord.ts +5 -4
  77. package/packages/core/src/channels/manager.ts +122 -30
  78. package/packages/core/src/channels/slack.ts +5 -4
  79. package/packages/core/src/channels/telegram.ts +36 -6
  80. package/packages/core/src/channels/webchat.ts +11 -10
  81. package/packages/core/src/channels/whatsapp.ts +23 -7
  82. package/packages/core/src/config/index.ts +13 -2
  83. package/packages/core/src/config/loader.ts +71 -29
  84. package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
  85. package/packages/core/src/ethics/EthicsGuard.ts +51 -47
  86. package/packages/core/src/events/agent-bus.ts +44 -68
  87. package/packages/core/src/events/channel-narration.ts +150 -0
  88. package/packages/core/src/events/narration.ts +82 -0
  89. package/packages/core/src/events/tool-narration.ts +62 -0
  90. package/packages/core/src/gateway/delegation-groups.ts +258 -0
  91. package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
  92. package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
  93. package/packages/core/src/gateway/lane-queue.ts +173 -0
  94. package/packages/core/src/gateway/notification-inbox.ts +57 -0
  95. package/packages/core/src/gateway/server.ts +1 -1
  96. package/packages/core/src/harness/index.ts +46 -27
  97. package/packages/core/src/index.ts +33 -27
  98. package/packages/core/src/mcp/hot-reload.ts +32 -23
  99. package/packages/core/src/mcp/index.ts +6 -3
  100. package/packages/core/src/mcp/singleton.ts +1 -4
  101. package/packages/core/src/mcp/tool-sync.ts +138 -0
  102. package/packages/core/src/memory/Scratchpad.test.ts +39 -20
  103. package/packages/core/src/memory/Scratchpad.ts +27 -34
  104. package/packages/core/src/multimodal/vision-service.ts +44 -38
  105. package/packages/core/src/resilience/retry.ts +95 -0
  106. package/packages/core/src/scheduler/CronScheduler.ts +334 -287
  107. package/packages/core/src/scheduler/index.ts +9 -7
  108. package/packages/core/src/scheduler/integration.ts +46 -26
  109. package/packages/core/src/scheduler/scheduler.test.ts +9 -13
  110. package/packages/core/src/scheduler/types.ts +7 -2
  111. package/packages/core/src/security/Pairing.ts +1 -1
  112. package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
  113. package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
  114. package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
  115. package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
  116. package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
  117. package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
  118. package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
  119. package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
  120. package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
  121. package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
  122. package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
  123. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
  124. package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
  125. package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
  126. package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
  127. package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
  128. package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
  129. package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
  130. package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
  131. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
  132. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
  133. package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
  134. package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
  135. package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
  136. package/packages/core/src/skills/skills.test.ts +52 -11
  137. package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
  138. package/packages/core/src/storage/bootstrap.ts +151 -0
  139. package/packages/core/src/storage/causal-events.ts +84 -0
  140. package/packages/core/src/storage/collections.ts +680 -0
  141. package/packages/core/src/storage/crypto.ts +205 -74
  142. package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
  143. package/packages/core/src/storage/hivedb.ts +61 -0
  144. package/packages/core/src/storage/index.ts +111 -18
  145. package/packages/core/src/storage/model-id.ts +53 -0
  146. package/packages/core/src/storage/onboarding.ts +540 -972
  147. package/packages/core/src/storage/reconcile.ts +238 -0
  148. package/packages/core/src/storage/seed.ts +572 -406
  149. package/packages/core/src/storage/usage.ts +285 -225
  150. package/packages/core/src/storage/user-email.ts +11 -0
  151. package/packages/core/src/swarm/AgentExecutor.ts +1 -1
  152. package/packages/core/src/swarm/EventBridge.ts +1 -1
  153. package/packages/core/src/swarm/index.ts +12 -9
  154. package/packages/core/src/tool-runtime/index.ts +146 -23
  155. package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
  156. package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
  157. package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
  158. package/packages/core/src/tools/agents/get-available-models.ts +36 -54
  159. package/packages/core/src/tools/agents/index.ts +784 -292
  160. package/packages/core/src/tools/api/api-request.test.ts +164 -0
  161. package/packages/core/src/tools/api/api-request.ts +174 -0
  162. package/packages/core/src/tools/api/index.ts +16 -0
  163. package/packages/core/src/tools/cli/index.ts +4 -0
  164. package/packages/core/src/tools/core/index.ts +281 -112
  165. package/packages/core/src/tools/cron/index.ts +121 -124
  166. package/packages/core/src/tools/index.ts +63 -78
  167. package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
  168. package/packages/core/src/tools/types.ts +3 -1
  169. package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
  170. package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
  171. package/packages/core/src/tools/web/browser-service.ts +5 -0
  172. package/packages/core/src/tools/web/browser-type.ts +3 -8
  173. package/packages/core/src/tools/web/index.ts +4 -4
  174. package/packages/core/src/voice/index.ts +89 -63
  175. package/packages/core/src/workers/agent.worker.ts +2 -2
  176. package/packages/core/src/workers/workers.test.ts +3 -10
  177. package/scripts/bump-version.ts +248 -0
  178. package/scripts/generate-skill-bundle.ts +108 -0
  179. package/test/agent-loop-terminal-synthesis.test.ts +32 -0
  180. package/test/catalog-agents-stay-enabled.test.ts +117 -0
  181. package/test/causal-events.test.ts +117 -0
  182. package/test/compaction.test.ts +105 -0
  183. package/test/context-compiler.test.ts +269 -0
  184. package/test/curator.test.ts +130 -0
  185. package/test/durable-queue.test.ts +114 -0
  186. package/test/harness-barrel.test.ts +64 -0
  187. package/test/hive-helpers.test.ts +130 -0
  188. package/test/hivedb-search.test.ts +189 -0
  189. package/test/internal-turns.test.ts +166 -0
  190. package/test/job-idempotency.test.ts +68 -0
  191. package/test/job-retry-backoff.test.ts +184 -0
  192. package/test/job-store.test.ts +381 -0
  193. package/test/llm-retry.test.ts +97 -0
  194. package/test/memory-perf.test.ts +774 -0
  195. package/test/minimal-loadout.test.ts +78 -0
  196. package/test/model-catalog.test.ts +105 -0
  197. package/test/preload.ts +12 -0
  198. package/test/reflector.test.ts +320 -0
  199. package/test/retention-cap.test.ts +91 -0
  200. package/test/retired-capabilities-pruned.test.ts +192 -0
  201. package/test/run-store.test.ts +355 -0
  202. package/test/scratchpad.test.ts +74 -0
  203. package/test/secrets-durability.test.ts +119 -0
  204. package/test/seed-model-reseed.test.ts +155 -0
  205. package/test/setup-agent-seed.test.ts +264 -0
  206. package/test/tool-inventory.test.ts +65 -0
  207. package/test/tool-runtime.test.ts +258 -0
  208. package/test/toon.test.ts +429 -0
  209. package/tsconfig.json +2 -0
  210. package/packages/core/src/ace/Curator.ts +0 -158
  211. package/packages/core/src/ace/Reflector.ts +0 -200
  212. package/packages/core/src/ace/index.ts +0 -4
  213. package/packages/core/src/agent/AgentRunner.ts +0 -711
  214. package/packages/core/src/agent/ContextCompiler.ts +0 -567
  215. package/packages/core/src/agent/ContextGuard.ts +0 -91
  216. package/packages/core/src/agent/ConversationStore.ts +0 -254
  217. package/packages/core/src/agent/Hooks.ts +0 -166
  218. package/packages/core/src/agent/StuckLoop.ts +0 -133
  219. package/packages/core/src/agent/providers/LLMClient.ts +0 -149
  220. package/packages/core/src/agent/providers/anthropic.ts +0 -212
  221. package/packages/core/src/agent/providers/openai-compat.ts +0 -231
  222. package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
  223. package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
  224. package/packages/core/src/agent/selectors/index.ts +0 -6
  225. package/packages/core/src/auth/auth.ts +0 -121
  226. package/packages/core/src/auth/index.ts +0 -1
  227. package/packages/core/src/canvas/CanvasManager.ts +0 -390
  228. package/packages/core/src/canvas/canvas-tools.ts +0 -448
  229. package/packages/core/src/harness/collections.ts +0 -98
  230. package/packages/core/src/harness/goal-verifier.ts +0 -141
  231. package/packages/core/src/harness/harness.test.ts +0 -236
  232. package/packages/core/src/harness/reconcile.ts +0 -149
  233. package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
  234. package/packages/core/src/multimodal/VisionService.ts +0 -293
  235. package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
  236. package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
  237. package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
  238. package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
  239. package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
  240. package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
  241. package/packages/core/src/scheduler/dag/errors.ts +0 -37
  242. package/packages/core/src/scheduler/dag/index.ts +0 -26
  243. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
  244. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
  245. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
  246. package/packages/core/src/storage/HiveDBStorage.ts +0 -64
  247. package/packages/core/src/storage/SQLiteStorage.ts +0 -414
  248. package/packages/core/src/storage/hiveSeed.ts +0 -308
  249. package/packages/core/src/storage/hiveStorage.test.ts +0 -38
  250. package/packages/core/src/storage/schema.ts +0 -689
  251. package/packages/core/src/storage/storage.test.ts +0 -37
  252. package/packages/core/src/swarm/AgentBus.ts +0 -460
  253. package/packages/core/src/swarm/EventBus.ts +0 -169
  254. package/packages/core/src/swarm/WorkerPool.ts +0 -236
  255. package/packages/core/src/tools/bridge-events.ts +0 -26
  256. package/packages/core/src/tools/canvas/index.ts +0 -375
  257. package/packages/core/src/tools/codebridge/index.ts +0 -342
  258. package/packages/core/src/tools/meeting/index.ts +0 -353
  259. package/packages/core/src/tools/projects/index.ts +0 -37
  260. package/packages/core/src/tools/projects/project-create.ts +0 -94
  261. package/packages/core/src/tools/projects/project-done.ts +0 -66
  262. package/packages/core/src/tools/projects/project-fail.ts +0 -66
  263. package/packages/core/src/tools/projects/project-list.ts +0 -96
  264. package/packages/core/src/tools/projects/project-update.ts +0 -72
  265. package/packages/core/src/tools/projects/task-create.ts +0 -68
  266. package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
  267. package/packages/core/src/tools/projects/task-update.ts +0 -93
  268. package/packages/core/src/tools/voice/index.ts +0 -104
  269. package/packages/core/src/tools/web/api-request.test.ts +0 -170
  270. package/packages/core/src/tools/web/api-request.ts +0 -239
  271. package/test/setup-db.ts +0 -216
  272. /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
@@ -1,10 +1,73 @@
1
- import { logger } from "../../utils/logger.ts"
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 "./LLMClient.ts"
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
- if (options.maxTokens) config.maxOutputTokens = options.maxTokens
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
- const response = await ai.models.generateContent({ model: options.model, contents, config })
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,5 @@
1
+ import { OpenAICompatBase } from "./openai-compat-base"
2
+
3
+ export class GroqProvider extends OpenAICompatBase {
4
+ constructor() { super("groq") }
5
+ }
@@ -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 "./LLMClient.ts"
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.ts"
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
- export const FIXED_TEMPERATURE_1_MODELS = new Set(["kimi-k2.5", "kimi-k2", "kimi-k2-5"])
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
- export const PROVIDER_PROFILES: Record<string, ProviderProfile> = {
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
- mistral: { ...DEFAULT_PROFILE, normalizeToolNames: true, toolChoiceAuto: "any", stripAdditionalProperties: true },
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
- "local-llama": { ...DEFAULT_PROFILE },
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
- export const NO_TOOL_MODELS = new Set([
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
- if (!profile.stripAdditionalProperties) return schema
110
- return deepStripSchema(schema)
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
+ }
@@ -0,0 +1,5 @@
1
+ import { OpenAICompatBase } from "./openai-compat-base"
2
+
3
+ export class MistralProvider extends OpenAICompatBase {
4
+ constructor() { super("mistral") }
5
+ }
@@ -0,0 +1,5 @@
1
+ import { OpenAICompatBase } from "./openai-compat-base"
2
+
3
+ export class ModelScopeProvider extends OpenAICompatBase {
4
+ constructor() { super("modelscope") }
5
+ }
@@ -0,0 +1,5 @@
1
+ import { OpenAICompatBase } from "./openai-compat-base"
2
+
3
+ export class NvidiaProvider extends OpenAICompatBase {
4
+ constructor() { super("nvidia") }
5
+ }