@causyne/agent 0.0.0-stage → 1.10.0

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 (274) hide show
  1. package/LICENSE +102 -0
  2. package/README.md +31 -2
  3. package/SECURITY.md +24 -0
  4. package/bin/causyne.js +13 -0
  5. package/docs/ARCHITECTURE.md +63 -0
  6. package/docs/PROVENANCE.md +29 -0
  7. package/docs/SECURITY_REVIEW.md +89 -0
  8. package/docs/THIRD_PARTY_NOTICES.md +49 -0
  9. package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
  10. package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
  11. package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
  12. package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
  13. package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
  14. package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
  15. package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
  16. package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
  17. package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
  18. package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
  19. package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
  20. package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
  21. package/native-skills/android/play-store-release/SKILL.md +43 -0
  22. package/native-skills/android/room-and-persistence/SKILL.md +40 -0
  23. package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
  24. package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
  25. package/native-skills/backend/caching-strategy/SKILL.md +44 -0
  26. package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
  27. package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
  28. package/native-skills/backend/rest-api-design/SKILL.md +40 -0
  29. package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
  30. package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
  31. package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
  32. package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
  33. package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
  34. package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
  35. package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
  36. package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
  37. package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
  38. package/native-skills/data/metric-definition/SKILL.md +42 -0
  39. package/native-skills/data/notebook-to-production/SKILL.md +43 -0
  40. package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
  41. package/native-skills/database/connection-pooling/SKILL.md +42 -0
  42. package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
  43. package/native-skills/database/query-optimisation/SKILL.md +38 -0
  44. package/native-skills/database/safe-migrations/SKILL.md +47 -0
  45. package/native-skills/database/schema-design/SKILL.md +38 -0
  46. package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
  47. package/native-skills/design/colour-system-construction/SKILL.md +38 -0
  48. package/native-skills/design/design-direction-brief/SKILL.md +41 -0
  49. package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
  50. package/native-skills/design/responsive-strategy/SKILL.md +39 -0
  51. package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
  52. package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
  53. package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
  54. package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
  55. package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
  56. package/native-skills/devops/incident-triage/SKILL.md +38 -0
  57. package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
  58. package/native-skills/devops/observability-baseline/SKILL.md +38 -0
  59. package/native-skills/devops/secret-management/SKILL.md +40 -0
  60. package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
  61. package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
  62. package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
  63. package/native-skills/git/branch-strategy/SKILL.md +39 -0
  64. package/native-skills/git/commit-hygiene/SKILL.md +41 -0
  65. package/native-skills/git/history-recovery/SKILL.md +44 -0
  66. package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
  67. package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
  68. package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
  69. package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
  70. package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
  71. package/native-skills/mobile/deep-linking/SKILL.md +39 -0
  72. package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
  73. package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
  74. package/native-skills/mobile/push-notifications/SKILL.md +38 -0
  75. package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
  76. package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
  77. package/native-skills/performance/load-testing/SKILL.md +42 -0
  78. package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
  79. package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
  80. package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
  81. package/native-skills/python/async-correctness/SKILL.md +40 -0
  82. package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
  83. package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
  84. package/native-skills/python/environment-isolation/SKILL.md +41 -0
  85. package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
  86. package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
  87. package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
  88. package/native-skills/security/authorization-model/SKILL.md +42 -0
  89. package/native-skills/security/crypto-usage/SKILL.md +48 -0
  90. package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
  91. package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
  92. package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
  93. package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
  94. package/native-skills/security/threat-modelling/SKILL.md +46 -0
  95. package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
  96. package/native-skills/testing/property-based-testing/SKILL.md +46 -0
  97. package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
  98. package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
  99. package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
  100. package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
  101. package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
  102. package/native-skills/web/accessible-components/SKILL.md +45 -0
  103. package/native-skills/web/api-client-resilience/SKILL.md +39 -0
  104. package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
  105. package/native-skills/web/core-web-vitals/SKILL.md +40 -0
  106. package/native-skills/web/css-layout-systems/SKILL.md +41 -0
  107. package/native-skills/web/form-validation-ux/SKILL.md +39 -0
  108. package/native-skills/web/react-state-architecture/SKILL.md +41 -0
  109. package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
  110. package/package.json +68 -4
  111. package/src/agentLoop.js +2458 -0
  112. package/src/appChannel.js +109 -0
  113. package/src/approvalGrants.js +66 -0
  114. package/src/auditLog.js +52 -0
  115. package/src/autoUpdate.js +12 -0
  116. package/src/capabilities.js +36 -0
  117. package/src/checkpoints.js +1199 -0
  118. package/src/cliMode.js +638 -0
  119. package/src/commands.js +805 -0
  120. package/src/compiledSkills.js +540 -0
  121. package/src/config.js +130 -0
  122. package/src/contextManager.js +498 -0
  123. package/src/cortex/autoCondenser.js +192 -0
  124. package/src/cortex/provisioner.js +27 -0
  125. package/src/cortex/vectorDb.js +447 -0
  126. package/src/designPalettes.js +99 -0
  127. package/src/devMode.js +6 -0
  128. package/src/embodimentSession.js +180 -0
  129. package/src/entrypoint.js +194 -0
  130. package/src/execSafe.js +21 -0
  131. package/src/gitSnapshot.js +378 -0
  132. package/src/mcp/jsonRpc.js +165 -0
  133. package/src/mcp/mcpClient.js +136 -0
  134. package/src/mcp/mcpRegistry.js +207 -0
  135. package/src/memory.js +62 -0
  136. package/src/memoryManager.js +124 -0
  137. package/src/modelCatalogue.js +132 -0
  138. package/src/modelIds.js +42 -0
  139. package/src/modelRouter.js +145 -0
  140. package/src/outputStore.js +246 -0
  141. package/src/perception.js +124 -0
  142. package/src/planCache.js +437 -0
  143. package/src/procSignal.js +32 -0
  144. package/src/providers/anthropic.js +408 -0
  145. package/src/providers/base.js +328 -0
  146. package/src/providers/dashscope-byok.js +86 -0
  147. package/src/providers/deepseek.js +80 -0
  148. package/src/providers/factory.js +37 -0
  149. package/src/providers/gemini.js +346 -0
  150. package/src/providers/moonshot.js +73 -0
  151. package/src/providers/muse.js +169 -0
  152. package/src/providers/ollama.js +104 -0
  153. package/src/providers/openai-compatible.js +84 -0
  154. package/src/providers/openai.js +20 -0
  155. package/src/providers/qwen-byok.js +76 -0
  156. package/src/providers/xai.js +73 -0
  157. package/src/provisioning/capabilityConsent.js +24 -0
  158. package/src/provisioning/capabilityProvisioner.js +271 -0
  159. package/src/provisioning/capabilityTools.js +29 -0
  160. package/src/reporting.js +373 -0
  161. package/src/sandbox.js +529 -0
  162. package/src/sessionStore.js +279 -0
  163. package/src/skillsManager.js +288 -0
  164. package/src/slopDetector.js +307 -0
  165. package/src/taskBuffer.js +45 -0
  166. package/src/taskShape.js +241 -0
  167. package/src/textDiff.js +150 -0
  168. package/src/tokenTracker.js +560 -0
  169. package/src/tool-scripts/ax_snapshot.applescript +39 -0
  170. package/src/tool-scripts/ax_snapshot.ps1 +10 -0
  171. package/src/tool-scripts/desktop_act.applescript +58 -0
  172. package/src/tool-scripts/resolve_automator.py +119 -0
  173. package/src/toolPacks.js +524 -0
  174. package/src/tools/archive.js +63 -0
  175. package/src/tools/astryxOracle.js +78 -0
  176. package/src/tools/axSnapshot.js +165 -0
  177. package/src/tools/blenderAutomator.js +349 -0
  178. package/src/tools/browserAutomator.js +252 -0
  179. package/src/tools/checkSystemSpecs.js +64 -0
  180. package/src/tools/clipboard.js +36 -0
  181. package/src/tools/codeSearchAst.js +101 -0
  182. package/src/tools/codeSearcher.js +230 -0
  183. package/src/tools/codeTools.js +262 -0
  184. package/src/tools/codebaseSearch.js +75 -0
  185. package/src/tools/createSkill.js +129 -0
  186. package/src/tools/deferredMcpSearch.js +246 -0
  187. package/src/tools/desktopAct.js +208 -0
  188. package/src/tools/desktopOpen.js +34 -0
  189. package/src/tools/dynamicLoader.js +44 -0
  190. package/src/tools/executeProceduralMedia.js +93 -0
  191. package/src/tools/fsDelete.js +51 -0
  192. package/src/tools/fsEdit.js +105 -0
  193. package/src/tools/fsFind.js +128 -0
  194. package/src/tools/fsList.js +77 -0
  195. package/src/tools/fsMove.js +57 -0
  196. package/src/tools/fsRead.js +87 -0
  197. package/src/tools/fsSearch.js +198 -0
  198. package/src/tools/fsWrite.js +41 -0
  199. package/src/tools/gameAiBehaviorTree.js +208 -0
  200. package/src/tools/gameAnimationRig.js +194 -0
  201. package/src/tools/gameAssetFetcher.js +372 -0
  202. package/src/tools/gameAssetHarvester.js +210 -0
  203. package/src/tools/gameAudioSynth.js +267 -0
  204. package/src/tools/gameBuilder.js +886 -0
  205. package/src/tools/gameCanvasAudit.js +101 -0
  206. package/src/tools/gameCinematicSequencer.js +151 -0
  207. package/src/tools/gameDataForge.js +124 -0
  208. package/src/tools/gameDialogueTreeGen.js +166 -0
  209. package/src/tools/gameEconomyBalancer.js +161 -0
  210. package/src/tools/gameEntitySpawner.js +207 -0
  211. package/src/tools/gameLoreForge.js +153 -0
  212. package/src/tools/gameQuestArcBuilder.js +152 -0
  213. package/src/tools/gameSceneBuilder.js +158 -0
  214. package/src/tools/gameShaderForge.js +181 -0
  215. package/src/tools/gameTilemapGenerator.js +164 -0
  216. package/src/tools/gameUiHudBuilder.js +292 -0
  217. package/src/tools/gameVfxEmitterGen.js +108 -0
  218. package/src/tools/gameWebRuntimeInspector.js +91 -0
  219. package/src/tools/generateMediaViaApi.js +358 -0
  220. package/src/tools/gitExec.js +54 -0
  221. package/src/tools/godotAutomator.js +437 -0
  222. package/src/tools/manageLocalAiDependencies.js +220 -0
  223. package/src/tools/memoryWrite.js +99 -0
  224. package/src/tools/multiEdit.js +107 -0
  225. package/src/tools/palmierAutomator.js +104 -0
  226. package/src/tools/pdfReader.js +112 -0
  227. package/src/tools/persistentShell.js +148 -0
  228. package/src/tools/printer.js +316 -0
  229. package/src/tools/procTerm.js +91 -0
  230. package/src/tools/processList.js +52 -0
  231. package/src/tools/readNativeSkill.js +39 -0
  232. package/src/tools/registry.js +277 -0
  233. package/src/tools/resolveAutomator.js +102 -0
  234. package/src/tools/screenshot.js +75 -0
  235. package/src/tools/subagentOrchestrator.js +305 -0
  236. package/src/tools/sysRun.js +125 -0
  237. package/src/tools/todoWrite.js +75 -0
  238. package/src/tools/toolDispatch.js +127 -0
  239. package/src/tools/toolForge.js +124 -0
  240. package/src/tools/updatePlan.js +88 -0
  241. package/src/tools/viewImage.js +82 -0
  242. package/src/tools/visionInspector.js +154 -0
  243. package/src/tools/webFetch.js +74 -0
  244. package/src/tools/webSearch.js +82 -0
  245. package/src/tui/ask.js +164 -0
  246. package/src/tui/banner.js +158 -0
  247. package/src/tui/block.js +152 -0
  248. package/src/tui/chatRenderer.js +953 -0
  249. package/src/tui/completion.js +424 -0
  250. package/src/tui/dialogs.js +606 -0
  251. package/src/tui/diff.js +296 -0
  252. package/src/tui/history.js +135 -0
  253. package/src/tui/home.js +217 -0
  254. package/src/tui/invocation.js +105 -0
  255. package/src/tui/keys.js +443 -0
  256. package/src/tui/lineEditor.js +256 -0
  257. package/src/tui/overlay.js +256 -0
  258. package/src/tui/prompt.js +1258 -0
  259. package/src/tui/selectList.js +335 -0
  260. package/src/tui/stage.js +330 -0
  261. package/src/tui/stageSurface.js +609 -0
  262. package/src/tui/statusLine.js +254 -0
  263. package/src/tui/text.js +345 -0
  264. package/src/tui/theme.js +393 -0
  265. package/src/tui/toolDisplay.js +176 -0
  266. package/src/tui/transcript.js +235 -0
  267. package/src/tui/viewport.js +337 -0
  268. package/src/uncertainty.js +271 -0
  269. package/src/userSkills.js +265 -0
  270. package/src/utils/cli_ui.js +0 -0
  271. package/src/utils/inquirer.js +12 -0
  272. package/src/utils/paths.js +37 -0
  273. package/src/version.js +38 -0
  274. package/src/vfeTracker.js +220 -0
@@ -0,0 +1,328 @@
1
+ import { canonicalModelId } from '../modelIds.js';
2
+
3
+ // Fields the agent loop attaches to messages for its own bookkeeping. They must
4
+ // never reach a provider API, which rejects unknown message properties.
5
+ const INTERNAL_MESSAGE_FIELDS = ['cacheAnchor', 'spillPath'];
6
+
7
+ // Container formats each vendor actually accepts for audio input. Kept here so
8
+
9
+ // instead of three drifting copies.
10
+ const AUDIO_MIME_BY_FORMAT = {
11
+ wav: 'audio/wav',
12
+ mp3: 'audio/mpeg',
13
+ m4a: 'audio/mp4',
14
+ aac: 'audio/aac',
15
+ ogg: 'audio/ogg',
16
+ flac: 'audio/flac',
17
+ webm: 'audio/webm'
18
+ };
19
+
20
+ /** Map a container format token to the MIME type a provider expects. */
21
+ export function mimeTypeForAudioFormat(format) {
22
+ return AUDIO_MIME_BY_FORMAT[String(format || '').toLowerCase()] || 'audio/wav';
23
+ }
24
+
25
+ /**
26
+ * Map a MIME type to the short format token OpenAI's input_audio wants.
27
+ *
28
+ * OpenAI accepts only 'wav' and 'mp3'. Anything else is reported honestly as
29
+ * itself so the provider layer can reject it with a clear message rather than
30
+ * mislabelling an m4a as an mp3 and letting the vendor fail on decode — which
31
+ * is exactly what the previous hardcoded format:'mp3' did.
32
+ */
33
+ export function audioFormatForMimeType(mimeType) {
34
+ const mime = String(mimeType || '').toLowerCase().split(';')[0].trim();
35
+ for (const [format, candidate] of Object.entries(AUDIO_MIME_BY_FORMAT)) {
36
+ if (candidate === mime) return format;
37
+ }
38
+ if (mime === 'audio/mp3') return 'mp3';
39
+ if (mime === 'audio/x-wav' || mime === 'audio/wave') return 'wav';
40
+ if (mime === 'audio/x-m4a') return 'm4a';
41
+ return 'wav';
42
+ }
43
+
44
+ /** True for an audio content block in either the OpenAI or the Gemini shape. */
45
+ export function isAudioBlock(block) {
46
+ if (!block) return false;
47
+ if (block.type === 'input_audio') return true;
48
+ return Boolean(block.inlineData && String(block.inlineData.mimeType || '').startsWith('audio/'));
49
+ }
50
+
51
+ export class LLMProvider {
52
+ constructor(config) {
53
+ this.apiKey = config.apiKey;
54
+ // Normalised here rather than at each caller: this is the last hop before
55
+ // the request payload and every provider extends this class, so a bad id
56
+
57
+ // written before this existed) is corrected exactly once. A dotted
58
+ // Anthropic id used to match none of anthropic.js's capability lists,
59
+ // which silently disabled thinking and tool search and sent `temperature`
60
+ // to models that reject it. See src/modelIds.js.
61
+ this.model = canonicalModelId(config.model);
62
+ this.baseUrl = config.baseUrl; // For openai-compatible endpoints
63
+
64
+ // Model-id prefixes this provider can send images to. Vision is a property
65
+ // of the *model*, not the vendor, so each provider declares its own list —
66
+ // the same pattern AnthropicProvider uses for tool search. A single '*'
67
+ // means every model this provider serves accepts images.
68
+ //
69
+ // This replaced a flat `supportsVision = false` on the base class that only
70
+ // OpenAI and Anthropic ever overrode, which silently culled `screenshot`
71
+ // and `vision_inspect_ui` from the catalogue on seven of ten providers and
72
+ // made the mandatory visual-verification step of the anti-slop directive
73
+ // impossible to satisfy on any of them.
74
+ this.visionModels = [];
75
+
76
+
77
+ this.imagePayloadFormat = 'openai';
78
+
79
+ // An explicit config value always wins. Custom OpenAI-compatible endpoints
80
+ // serve models this list cannot possibly know about.
81
+ this.visionOverride = typeof config.supportsVision === 'boolean'
82
+ ? config.supportsVision
83
+ : null;
84
+
85
+ // Model-id prefixes this provider can send AUDIO to. Same shape and same
86
+ // reasoning as visionModels above — audio is a property of the model, not
87
+ // the vendor. It is deliberately NOT derived from visionModels: the two
88
+ // capabilities diverge sharply. OpenAI serves images on every model but
89
+ // accepts input_audio only on the gpt-4o-audio / gpt-audio family, and
90
+ // Anthropic reads images on every model while accepting no audio at all.
91
+ // Defaulting audio to the vision list would 400 on both.
92
+ this.audioModels = [];
93
+
94
+
95
+ this.audioPayloadFormat = 'openai';
96
+
97
+ // Hard ceiling on a single audio clip for this provider, applied as the
98
+
99
+
100
+
101
+ this.maxAudioSeconds = null;
102
+ this.maxAudioBytes = null;
103
+
104
+ this.audioOverride = typeof config.supportsAudio === 'boolean'
105
+ ? config.supportsAudio
106
+ : null;
107
+ }
108
+
109
+ get supportsVision() {
110
+ if (this.visionOverride !== null) return this.visionOverride;
111
+ const model = (this.model || '').toLowerCase();
112
+ return this.visionModels.some(prefix => prefix === '*' || model.includes(prefix));
113
+ }
114
+
115
+ /**
116
+ * A model from this same provider that CAN take audio, or null.
117
+ *
118
+ * Exists because "the selected model cannot process audio" is a dead end for
119
+ * the user unless something names the way out. Meta is the case that forced
120
+ * it: the model they recommend, Muse Spark 1.3, is the one that cannot hear.
121
+ */
122
+ audioAlternative() {
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * Turn a clip into text, or null when this provider has no way to.
128
+ *
129
+ * A provider whose chat model cannot hear may still ship a dedicated
130
+ * speech-to-text model on the same key — Meta does, and it is the whole answer
131
+ * to Muse Spark 1.3 being both the recommended model and the deaf one.
132
+ * Returning null means "no route", not "it failed"; a thrown error means the
133
+ * route existed and did not work, which the caller reports differently.
134
+ */
135
+ async transcribeAudio() {
136
+ return null;
137
+ }
138
+
139
+
140
+ get canTranscribe() {
141
+ return false;
142
+ }
143
+
144
+ get supportsAudio() {
145
+ if (this.audioOverride !== null) return this.audioOverride;
146
+ const model = (this.model || '').toLowerCase();
147
+ return this.audioModels.some(prefix => prefix === '*' || model.includes(prefix));
148
+ }
149
+
150
+ /**
151
+ * Send a chat request to the LLM.
152
+ * @param {Array} messages - [{ role: 'system'|'user'|'assistant', content: '...' }]
153
+ * @param {Object} options - { temperature, maxTokens, jsonMode, tools }
154
+ * @returns {Promise<{ content: string, usage: { promptTokens: number, completionTokens: number } }>}
155
+ */
156
+ async chat(messages, options = {}) {
157
+ throw new Error('chat() must be implemented by provider');
158
+ }
159
+
160
+ /**
161
+ * Prepare a message list for this specific model.
162
+ *
163
+ * Providers used to hardcode "strip every image block" because their whole
164
+ * vendor was assumed blind. Now that vision is decided per model, that blanket
165
+ * strip would delete images from models that can actually read them — so the
166
+ * decision is made here, once, against supportsVision.
167
+ */
168
+ prepareMessages(messages) {
169
+ const clean = this.stripInternalFields(messages);
170
+
171
+ // Vision and audio are decided separately. A model can read images and not
172
+ // hear audio (most of them), so the two strips cannot share one branch —
173
+ // an audio-blind vision model used to keep its input_audio block and 400.
174
+ const shaped = this.supportsVision
175
+ ? clean.map(msg => this.normalizeImageBlocks(msg))
176
+ : clean.map(msg => this.flattenToTextOnly(msg));
177
+
178
+ return this.supportsAudio
179
+ ? shaped.map(msg => this.normalizeAudioBlocks(msg))
180
+ : shaped.map(msg => this.stripAudioBlocks(msg));
181
+ }
182
+
183
+ /**
184
+ * Translate audio blocks into this provider's wire shape.
185
+ *
186
+ * The agent loop emits the OpenAI `input_audio` shape as the canonical form;
187
+ * Gemini needs `inlineData`. History replayed from an earlier session — or
188
+ * from before a provider switch — can carry either, so both are accepted.
189
+ */
190
+ normalizeAudioBlocks(msg) {
191
+ if (!Array.isArray(msg.content)) return msg;
192
+
193
+ const content = msg.content.map(block => {
194
+ if (!block) return block;
195
+
196
+ if (this.audioPayloadFormat === 'gemini') {
197
+ if (block.type === 'input_audio') {
198
+ return {
199
+ inlineData: {
200
+ mimeType: mimeTypeForAudioFormat(block.input_audio?.format),
201
+ data: block.input_audio?.data
202
+ }
203
+ };
204
+ }
205
+ return block;
206
+ }
207
+
208
+ // OpenAI shape. Convert a Gemini-shaped block back if one is replayed.
209
+ if (block.inlineData && String(block.inlineData.mimeType || '').startsWith('audio/')) {
210
+ return {
211
+ type: 'input_audio',
212
+ input_audio: {
213
+ data: block.inlineData.data,
214
+ format: audioFormatForMimeType(block.inlineData.mimeType)
215
+ }
216
+ };
217
+ }
218
+ return block;
219
+ });
220
+
221
+ return { ...msg, content };
222
+ }
223
+
224
+ /**
225
+ * Drop audio from a model that cannot hear, leaving a visible marker.
226
+ *
227
+ * Silently deleting it was the old behaviour and it was the worst possible
228
+ * one: the user recorded a clip, the model answered as though it had heard
229
+ * nothing, and nothing anywhere said why. Images already got a placeholder;
230
+ * audio now gets the same courtesy.
231
+ */
232
+ stripAudioBlocks(msg) {
233
+ if (!Array.isArray(msg.content)) return msg;
234
+ if (!msg.content.some(isAudioBlock)) return msg;
235
+
236
+ const content = msg.content
237
+ .filter(block => !isAudioBlock(block))
238
+ .concat([{
239
+ type: 'text',
240
+ text: '[Audio content removed — the selected model cannot process audio. Causyne transcribed the request on-device instead where possible.]'
241
+ }]);
242
+
243
+ return { ...msg, content };
244
+ }
245
+
246
+ /**
247
+ * Translate image blocks into this provider's wire shape. The agent loop emits
248
+ * whichever shape imagePayloadFormat asks for, but history replayed from an
249
+ * earlier session — or from before a provider switch — can carry the other.
250
+ */
251
+ normalizeImageBlocks(msg) {
252
+ if (!Array.isArray(msg.content)) return msg;
253
+ if (this.imagePayloadFormat !== 'openai') return msg;
254
+
255
+ const content = msg.content.map(block => {
256
+ if (block && block.type === 'image' && block.source?.type === 'base64') {
257
+ return {
258
+ type: 'image_url',
259
+ image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
260
+ };
261
+ }
262
+ return block;
263
+ });
264
+
265
+ return { ...msg, content };
266
+ }
267
+
268
+ /**
269
+ * Collapse multimodal content to plain text for a model that cannot see.
270
+ *
271
+ * Audio blocks are deliberately preserved here rather than filtered out:
272
+ * this runs before the audio strip in prepareMessages, and a model that is
273
+ * blind is not necessarily deaf. Flattening to a bare string at this point
274
+ * would destroy an audio block that the audio branch was about to keep.
275
+ */
276
+ flattenToTextOnly(msg) {
277
+ if (!Array.isArray(msg.content)) return msg;
278
+
279
+ const audioBlocks = this.supportsAudio ? msg.content.filter(isAudioBlock) : [];
280
+
281
+ const textParts = msg.content
282
+ .filter(block => block.type === 'text')
283
+ .map(block => block.text);
284
+
285
+ const text = textParts.join('\n') ||
286
+ '[Image content removed — not supported by this model]';
287
+
288
+ if (audioBlocks.length === 0) return { ...msg, content: text };
289
+
290
+ return { ...msg, content: [{ type: 'text', text }, ...audioBlocks] };
291
+ }
292
+
293
+ /** Remove internal bookkeeping fields before sending messages upstream. */
294
+ stripInternalFields(messages) {
295
+ return messages.map(msg => {
296
+ let needsCopy = false;
297
+ for (const field of INTERNAL_MESSAGE_FIELDS) {
298
+ if (field in msg) { needsCopy = true; break; }
299
+ }
300
+ if (!needsCopy) return msg;
301
+
302
+ const clean = { ...msg };
303
+ for (const field of INTERNAL_MESSAGE_FIELDS) delete clean[field];
304
+ return clean;
305
+ });
306
+ }
307
+
308
+ // Normalize usage across different provider response formats.
309
+ //
310
+ // Cached prompt tokens are surfaced separately because they are billed at a
311
+ // fraction of the base input rate: OpenAI-style APIs report them under
312
+ // prompt_tokens_details.cached_tokens, DeepSeek under prompt_cache_hit_tokens.
313
+
314
+ // disfavour once caching starts working.
315
+ normalizeUsage(rawUsage) {
316
+ const promptTokens = rawUsage?.prompt_tokens || 0;
317
+ const cachedPromptTokens =
318
+ rawUsage?.prompt_tokens_details?.cached_tokens ??
319
+ rawUsage?.prompt_cache_hit_tokens ??
320
+ 0;
321
+
322
+ return {
323
+ promptTokens,
324
+ completionTokens: rawUsage?.completion_tokens || 0,
325
+ cachedPromptTokens: Math.min(cachedPromptTokens, promptTokens)
326
+ };
327
+ }
328
+ }
@@ -0,0 +1,86 @@
1
+ import { canonicalModelId } from '../modelIds.js';
2
+ import { DEFAULT_MODEL_BY_PROVIDER } from '../modelCatalogue.js';
3
+ import { LLMProvider } from './base.js';
4
+
5
+ // Direct DashScope API connection for BYOK (unlike dashscope.js which is the proxy for Free Demo)
6
+ export class DashscopeByokProvider extends LLMProvider {
7
+ constructor(config) {
8
+ super(config);
9
+ // Use International/Singapore endpoint by default, allow override via config
10
+ let url = config.baseUrl || 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions';
11
+ if (url.endsWith('/v1')) url += '/chat/completions';
12
+ else if (url.endsWith('/v1/')) url += 'chat/completions';
13
+
14
+ this.baseUrl = url;
15
+ this.model = canonicalModelId(config.model || DEFAULT_MODEL_BY_PROVIDER['dashscope-byok']);
16
+ this.apiKey = config.apiKey;
17
+ this.visionModels = ['-vl-', '-vl', 'qwen3.6', 'qwen3.7', 'kimi-k2.5', 'kimi-k2.7'];
18
+ // Qwen exposes audio on the omni and dedicated audio families only; the
19
+ // -vl- vision models are image-in, text-out and reject an audio block.
20
+ this.audioModels = ['-omni', 'qwen-audio', 'qwen2-audio'];
21
+ }
22
+
23
+ async chat(messages, options = {}) {
24
+ const ALLOWED = [
25
+ "qwen3.7-max", "qwen3.7-plus", "kimi-k2.7-code", "kimi-k2.6",
26
+ "kimi-k2.5", "kimi-k2-thinking", "Moonshot-Kimi-K2-Instruct",
27
+ "deepseek-v4-flash", "deepseek-v4-pro"
28
+ ];
29
+ let activeModel = this.model;
30
+ if (!ALLOWED.includes(activeModel)) {
31
+ activeModel = 'qwen3.7-plus';
32
+ }
33
+
34
+ const sanitizedMessages = this.prepareMessages(messages);
35
+
36
+ const payload = {
37
+ model: activeModel,
38
+ messages: sanitizedMessages,
39
+ temperature: options.temperature || 0.1,
40
+ };
41
+
42
+ if (options.jsonMode) {
43
+ payload.response_format = { type: 'json_object' };
44
+ }
45
+
46
+ if (options.tools) {
47
+ payload.tools = options.tools.map(tool => ({
48
+ type: 'function',
49
+ function: tool.function ? tool.function : tool
50
+ }));
51
+ payload.tool_choice = "auto";
52
+ }
53
+
54
+ const response = await fetch(this.baseUrl, {
55
+ method: 'POST',
56
+ headers: {
57
+ 'Content-Type': 'application/json',
58
+ 'Authorization': `Bearer ${this.apiKey}`
59
+ },
60
+ body: JSON.stringify(payload)
61
+ });
62
+
63
+ if (!response.ok) {
64
+ const errorData = await response.text();
65
+
66
+ // Enterprise Error Handling: Detect Workspace-bound API Key configuration errors
67
+ if (response.status === 401 && this.apiKey && this.apiKey.startsWith('sk-ws-')) {
68
+ if (!this.baseUrl.includes('ws-')) {
69
+ throw new Error(`\n❌ QWEN WORKSPACE CONFIGURATION ERROR ❌\n\nYou are using a Workspace-bound API Key (starts with sk-ws-), but you haven't configured your Workspace Base URL!\n\nAlibaba requires a specific URL for these keys.\n\n🛠️ HOW TO FIX THIS:\n1. Open your computer terminal.\n2. Run command: causyne-agent config\n3. When asked for 'Custom Base URL', paste your Workspace URL from Alibaba (e.g. https://{Your-Workspace-ID}.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/chat/completions).\n\nAlternatively, use a Standard API Key (starts with sk-) which does not require a custom URL.`);
70
+ }
71
+ }
72
+
73
+ throw new Error(`Qwen DashScope API Error (${response.status}): ${errorData}`);
74
+ }
75
+
76
+ const data = await response.json();
77
+ const content = data.choices[0].message.content;
78
+ const toolCalls = data.choices[0].message.tool_calls;
79
+
80
+ return {
81
+ content: content || '',
82
+ toolCalls: (toolCalls && toolCalls.length > 0) ? toolCalls : undefined,
83
+ usage: this.normalizeUsage(data.usage)
84
+ };
85
+ }
86
+ }
@@ -0,0 +1,80 @@
1
+ import { canonicalModelId } from '../modelIds.js';
2
+ import { DEFAULT_MODEL_BY_PROVIDER } from '../modelCatalogue.js';
3
+ import { LLMProvider } from './base.js';
4
+
5
+ export class DeepSeekProvider extends LLMProvider {
6
+ constructor(config) {
7
+ super(config);
8
+ this.baseUrl = 'https://api.deepseek.com/v1/chat/completions';
9
+ this.model = canonicalModelId(config.model || DEFAULT_MODEL_BY_PROVIDER['deepseek']);
10
+ // The deepseek-chat line is text-only; only the VL builds accept images.
11
+ this.visionModels = ['-vl'];
12
+ // DeepSeek exposes no audio input endpoint.
13
+ this.audioModels = [];
14
+ }
15
+
16
+ async chat(messages, options = {}) {
17
+ const sanitizedMessages = this.prepareMessages(messages);
18
+
19
+ const payload = {
20
+ model: this.model,
21
+ messages: sanitizedMessages,
22
+ temperature: options.temperature || 0.1,
23
+ };
24
+
25
+ if (options.jsonMode) {
26
+ payload.response_format = { type: 'json_object' };
27
+ }
28
+
29
+ let targetUrl = this.baseUrl;
30
+ let headers = {
31
+ 'Content-Type': 'application/json'
32
+ };
33
+
34
+ if (this.apiKey) {
35
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
36
+ } else {
37
+ throw new Error("API key is required.");
38
+ }
39
+
40
+ if (options.tools) {
41
+ payload.tools = options.tools.map(tool => ({
42
+ type: 'function',
43
+ function: tool.function ? tool.function : tool
44
+ }));
45
+ payload.tool_choice = "auto";
46
+ }
47
+
48
+ const response = await fetch(targetUrl, {
49
+ method: 'POST',
50
+ headers: headers,
51
+ body: JSON.stringify(payload)
52
+ });
53
+
54
+ if (!response.ok) {
55
+ const errorData = await response.text();
56
+ throw new Error(`DeepSeek API Error (${response.status}): ${errorData}`);
57
+ }
58
+
59
+ const data = await response.json();
60
+ let content = '';
61
+ let toolCalls = [];
62
+
63
+ // Support both direct DeepSeek API format and our Cloudflare Proxy format
64
+ if (data.reply !== undefined) {
65
+ // Proxy Format
66
+ content = data.reply || '';
67
+ toolCalls = data.tool_calls || [];
68
+ } else if (data.choices && data.choices[0] && data.choices[0].message) {
69
+ // Direct API Format
70
+ content = data.choices[0].message.content || '';
71
+ toolCalls = data.choices[0].message.tool_calls || [];
72
+ }
73
+
74
+ return {
75
+ content: content,
76
+ toolCalls: (toolCalls && toolCalls.length > 0) ? toolCalls : undefined,
77
+ usage: this.normalizeUsage(data.usage || { prompt_tokens: 0, completion_tokens: 0 })
78
+ };
79
+ }
80
+ }
@@ -0,0 +1,37 @@
1
+ // Provider construction, in one place.
2
+ //
3
+
4
+
5
+ // ("can the selected model hear?"), and a second copy would drift the moment a
6
+
7
+
8
+ import { OpenAIProvider } from './openai.js';
9
+ import { AnthropicProvider } from './anthropic.js';
10
+ import { GeminiProvider } from './gemini.js';
11
+ import { OllamaProvider } from './ollama.js';
12
+ import { OpenAICompatibleProvider } from './openai-compatible.js';
13
+ import { DeepSeekProvider } from './deepseek.js';
14
+ import { QwenByokProvider } from './qwen-byok.js';
15
+ import { DashscopeByokProvider } from './dashscope-byok.js';
16
+ import { XAIProvider } from './xai.js';
17
+ import { MoonshotProvider } from './moonshot.js';
18
+ import { MuseProvider } from './muse.js';
19
+
20
+ export function createProvider(config) {
21
+ const p = config.provider?.toLowerCase();
22
+ if (p === 'openai') return new OpenAIProvider(config);
23
+ if (p === 'anthropic') return new AnthropicProvider(config);
24
+ if (p === 'gemini') return new GeminiProvider(config);
25
+ if (p === 'ollama') return new OllamaProvider(config);
26
+ if (p === 'deepseek') return new DeepSeekProvider(config);
27
+ if (p === 'dashscope-byok' || p === 'dashscope') return new DashscopeByokProvider(config);
28
+ if (p === 'qwen') return new QwenByokProvider(config);
29
+ if (p === 'openai-compatible') return new OpenAICompatibleProvider(config);
30
+ if (p === 'xai' || p === 'grok') return new XAIProvider(config);
31
+ if (p === 'moonshot' || p === 'kimi') return new MoonshotProvider(config);
32
+ // Meta Model API. 'meta' and 'muse' both reach it: the vendor is Meta, the
33
+ // model family is Muse, and users say either.
34
+ if (p === 'muse' || p === 'meta') return new MuseProvider(config);
35
+
36
+ throw new Error("API Key Required. Causyne is a 100% Sovereign BYOK Engine. Please configure your API Provider and Key in Settings.");
37
+ }