@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.
- package/LICENSE +102 -0
- package/README.md +31 -2
- package/SECURITY.md +24 -0
- package/bin/causyne.js +13 -0
- package/docs/ARCHITECTURE.md +63 -0
- package/docs/PROVENANCE.md +29 -0
- package/docs/SECURITY_REVIEW.md +89 -0
- package/docs/THIRD_PARTY_NOTICES.md +49 -0
- package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
- package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
- package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
- package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
- package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
- package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
- package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
- package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
- package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
- package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
- package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
- package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
- package/native-skills/android/play-store-release/SKILL.md +43 -0
- package/native-skills/android/room-and-persistence/SKILL.md +40 -0
- package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
- package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
- package/native-skills/backend/caching-strategy/SKILL.md +44 -0
- package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
- package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
- package/native-skills/backend/rest-api-design/SKILL.md +40 -0
- package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
- package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
- package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
- package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
- package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
- package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
- package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
- package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
- package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
- package/native-skills/data/metric-definition/SKILL.md +42 -0
- package/native-skills/data/notebook-to-production/SKILL.md +43 -0
- package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
- package/native-skills/database/connection-pooling/SKILL.md +42 -0
- package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
- package/native-skills/database/query-optimisation/SKILL.md +38 -0
- package/native-skills/database/safe-migrations/SKILL.md +47 -0
- package/native-skills/database/schema-design/SKILL.md +38 -0
- package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
- package/native-skills/design/colour-system-construction/SKILL.md +38 -0
- package/native-skills/design/design-direction-brief/SKILL.md +41 -0
- package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
- package/native-skills/design/responsive-strategy/SKILL.md +39 -0
- package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
- package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
- package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
- package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
- package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
- package/native-skills/devops/incident-triage/SKILL.md +38 -0
- package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
- package/native-skills/devops/observability-baseline/SKILL.md +38 -0
- package/native-skills/devops/secret-management/SKILL.md +40 -0
- package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
- package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
- package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
- package/native-skills/git/branch-strategy/SKILL.md +39 -0
- package/native-skills/git/commit-hygiene/SKILL.md +41 -0
- package/native-skills/git/history-recovery/SKILL.md +44 -0
- package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
- package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
- package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
- package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
- package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
- package/native-skills/mobile/deep-linking/SKILL.md +39 -0
- package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
- package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
- package/native-skills/mobile/push-notifications/SKILL.md +38 -0
- package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
- package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
- package/native-skills/performance/load-testing/SKILL.md +42 -0
- package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
- package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
- package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
- package/native-skills/python/async-correctness/SKILL.md +40 -0
- package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
- package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
- package/native-skills/python/environment-isolation/SKILL.md +41 -0
- package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
- package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
- package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
- package/native-skills/security/authorization-model/SKILL.md +42 -0
- package/native-skills/security/crypto-usage/SKILL.md +48 -0
- package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
- package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
- package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
- package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
- package/native-skills/security/threat-modelling/SKILL.md +46 -0
- package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
- package/native-skills/testing/property-based-testing/SKILL.md +46 -0
- package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
- package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
- package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
- package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
- package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
- package/native-skills/web/accessible-components/SKILL.md +45 -0
- package/native-skills/web/api-client-resilience/SKILL.md +39 -0
- package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
- package/native-skills/web/core-web-vitals/SKILL.md +40 -0
- package/native-skills/web/css-layout-systems/SKILL.md +41 -0
- package/native-skills/web/form-validation-ux/SKILL.md +39 -0
- package/native-skills/web/react-state-architecture/SKILL.md +41 -0
- package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
- package/package.json +68 -4
- package/src/agentLoop.js +2458 -0
- package/src/appChannel.js +109 -0
- package/src/approvalGrants.js +66 -0
- package/src/auditLog.js +52 -0
- package/src/autoUpdate.js +12 -0
- package/src/capabilities.js +36 -0
- package/src/checkpoints.js +1199 -0
- package/src/cliMode.js +638 -0
- package/src/commands.js +805 -0
- package/src/compiledSkills.js +540 -0
- package/src/config.js +130 -0
- package/src/contextManager.js +498 -0
- package/src/cortex/autoCondenser.js +192 -0
- package/src/cortex/provisioner.js +27 -0
- package/src/cortex/vectorDb.js +447 -0
- package/src/designPalettes.js +99 -0
- package/src/devMode.js +6 -0
- package/src/embodimentSession.js +180 -0
- package/src/entrypoint.js +194 -0
- package/src/execSafe.js +21 -0
- package/src/gitSnapshot.js +378 -0
- package/src/mcp/jsonRpc.js +165 -0
- package/src/mcp/mcpClient.js +136 -0
- package/src/mcp/mcpRegistry.js +207 -0
- package/src/memory.js +62 -0
- package/src/memoryManager.js +124 -0
- package/src/modelCatalogue.js +132 -0
- package/src/modelIds.js +42 -0
- package/src/modelRouter.js +145 -0
- package/src/outputStore.js +246 -0
- package/src/perception.js +124 -0
- package/src/planCache.js +437 -0
- package/src/procSignal.js +32 -0
- package/src/providers/anthropic.js +408 -0
- package/src/providers/base.js +328 -0
- package/src/providers/dashscope-byok.js +86 -0
- package/src/providers/deepseek.js +80 -0
- package/src/providers/factory.js +37 -0
- package/src/providers/gemini.js +346 -0
- package/src/providers/moonshot.js +73 -0
- package/src/providers/muse.js +169 -0
- package/src/providers/ollama.js +104 -0
- package/src/providers/openai-compatible.js +84 -0
- package/src/providers/openai.js +20 -0
- package/src/providers/qwen-byok.js +76 -0
- package/src/providers/xai.js +73 -0
- package/src/provisioning/capabilityConsent.js +24 -0
- package/src/provisioning/capabilityProvisioner.js +271 -0
- package/src/provisioning/capabilityTools.js +29 -0
- package/src/reporting.js +373 -0
- package/src/sandbox.js +529 -0
- package/src/sessionStore.js +279 -0
- package/src/skillsManager.js +288 -0
- package/src/slopDetector.js +307 -0
- package/src/taskBuffer.js +45 -0
- package/src/taskShape.js +241 -0
- package/src/textDiff.js +150 -0
- package/src/tokenTracker.js +560 -0
- package/src/tool-scripts/ax_snapshot.applescript +39 -0
- package/src/tool-scripts/ax_snapshot.ps1 +10 -0
- package/src/tool-scripts/desktop_act.applescript +58 -0
- package/src/tool-scripts/resolve_automator.py +119 -0
- package/src/toolPacks.js +524 -0
- package/src/tools/archive.js +63 -0
- package/src/tools/astryxOracle.js +78 -0
- package/src/tools/axSnapshot.js +165 -0
- package/src/tools/blenderAutomator.js +349 -0
- package/src/tools/browserAutomator.js +252 -0
- package/src/tools/checkSystemSpecs.js +64 -0
- package/src/tools/clipboard.js +36 -0
- package/src/tools/codeSearchAst.js +101 -0
- package/src/tools/codeSearcher.js +230 -0
- package/src/tools/codeTools.js +262 -0
- package/src/tools/codebaseSearch.js +75 -0
- package/src/tools/createSkill.js +129 -0
- package/src/tools/deferredMcpSearch.js +246 -0
- package/src/tools/desktopAct.js +208 -0
- package/src/tools/desktopOpen.js +34 -0
- package/src/tools/dynamicLoader.js +44 -0
- package/src/tools/executeProceduralMedia.js +93 -0
- package/src/tools/fsDelete.js +51 -0
- package/src/tools/fsEdit.js +105 -0
- package/src/tools/fsFind.js +128 -0
- package/src/tools/fsList.js +77 -0
- package/src/tools/fsMove.js +57 -0
- package/src/tools/fsRead.js +87 -0
- package/src/tools/fsSearch.js +198 -0
- package/src/tools/fsWrite.js +41 -0
- package/src/tools/gameAiBehaviorTree.js +208 -0
- package/src/tools/gameAnimationRig.js +194 -0
- package/src/tools/gameAssetFetcher.js +372 -0
- package/src/tools/gameAssetHarvester.js +210 -0
- package/src/tools/gameAudioSynth.js +267 -0
- package/src/tools/gameBuilder.js +886 -0
- package/src/tools/gameCanvasAudit.js +101 -0
- package/src/tools/gameCinematicSequencer.js +151 -0
- package/src/tools/gameDataForge.js +124 -0
- package/src/tools/gameDialogueTreeGen.js +166 -0
- package/src/tools/gameEconomyBalancer.js +161 -0
- package/src/tools/gameEntitySpawner.js +207 -0
- package/src/tools/gameLoreForge.js +153 -0
- package/src/tools/gameQuestArcBuilder.js +152 -0
- package/src/tools/gameSceneBuilder.js +158 -0
- package/src/tools/gameShaderForge.js +181 -0
- package/src/tools/gameTilemapGenerator.js +164 -0
- package/src/tools/gameUiHudBuilder.js +292 -0
- package/src/tools/gameVfxEmitterGen.js +108 -0
- package/src/tools/gameWebRuntimeInspector.js +91 -0
- package/src/tools/generateMediaViaApi.js +358 -0
- package/src/tools/gitExec.js +54 -0
- package/src/tools/godotAutomator.js +437 -0
- package/src/tools/manageLocalAiDependencies.js +220 -0
- package/src/tools/memoryWrite.js +99 -0
- package/src/tools/multiEdit.js +107 -0
- package/src/tools/palmierAutomator.js +104 -0
- package/src/tools/pdfReader.js +112 -0
- package/src/tools/persistentShell.js +148 -0
- package/src/tools/printer.js +316 -0
- package/src/tools/procTerm.js +91 -0
- package/src/tools/processList.js +52 -0
- package/src/tools/readNativeSkill.js +39 -0
- package/src/tools/registry.js +277 -0
- package/src/tools/resolveAutomator.js +102 -0
- package/src/tools/screenshot.js +75 -0
- package/src/tools/subagentOrchestrator.js +305 -0
- package/src/tools/sysRun.js +125 -0
- package/src/tools/todoWrite.js +75 -0
- package/src/tools/toolDispatch.js +127 -0
- package/src/tools/toolForge.js +124 -0
- package/src/tools/updatePlan.js +88 -0
- package/src/tools/viewImage.js +82 -0
- package/src/tools/visionInspector.js +154 -0
- package/src/tools/webFetch.js +74 -0
- package/src/tools/webSearch.js +82 -0
- package/src/tui/ask.js +164 -0
- package/src/tui/banner.js +158 -0
- package/src/tui/block.js +152 -0
- package/src/tui/chatRenderer.js +953 -0
- package/src/tui/completion.js +424 -0
- package/src/tui/dialogs.js +606 -0
- package/src/tui/diff.js +296 -0
- package/src/tui/history.js +135 -0
- package/src/tui/home.js +217 -0
- package/src/tui/invocation.js +105 -0
- package/src/tui/keys.js +443 -0
- package/src/tui/lineEditor.js +256 -0
- package/src/tui/overlay.js +256 -0
- package/src/tui/prompt.js +1258 -0
- package/src/tui/selectList.js +335 -0
- package/src/tui/stage.js +330 -0
- package/src/tui/stageSurface.js +609 -0
- package/src/tui/statusLine.js +254 -0
- package/src/tui/text.js +345 -0
- package/src/tui/theme.js +393 -0
- package/src/tui/toolDisplay.js +176 -0
- package/src/tui/transcript.js +235 -0
- package/src/tui/viewport.js +337 -0
- package/src/uncertainty.js +271 -0
- package/src/userSkills.js +265 -0
- package/src/utils/cli_ui.js +0 -0
- package/src/utils/inquirer.js +12 -0
- package/src/utils/paths.js +37 -0
- package/src/version.js +38 -0
- 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
|
+
}
|