@johpaz/hive-sdk 0.1.4 → 0.1.6

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