nemoris 0.1.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/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/SECURITY.md +119 -0
- package/bin/nemoris +46 -0
- package/config/agents/agent.toml.example +28 -0
- package/config/agents/default.toml +22 -0
- package/config/agents/orchestrator.toml +18 -0
- package/config/delivery.toml +73 -0
- package/config/embeddings.toml +5 -0
- package/config/identity/default-purpose.md +1 -0
- package/config/identity/default-soul.md +3 -0
- package/config/identity/orchestrator-purpose.md +1 -0
- package/config/identity/orchestrator-soul.md +1 -0
- package/config/improvement-targets.toml +15 -0
- package/config/jobs/heartbeat-check.toml +30 -0
- package/config/jobs/memory-rollup.toml +46 -0
- package/config/jobs/workspace-health.toml +63 -0
- package/config/mcp.toml +16 -0
- package/config/output-contracts.toml +17 -0
- package/config/peers.toml +32 -0
- package/config/peers.toml.example +32 -0
- package/config/policies/memory-default.toml +10 -0
- package/config/policies/memory-heartbeat.toml +5 -0
- package/config/policies/memory-ops.toml +10 -0
- package/config/policies/tools-heartbeat-minimal.toml +8 -0
- package/config/policies/tools-interactive-safe.toml +8 -0
- package/config/policies/tools-ops-bounded.toml +8 -0
- package/config/policies/tools-orchestrator.toml +7 -0
- package/config/providers/anthropic.toml +15 -0
- package/config/providers/ollama.toml +5 -0
- package/config/providers/openai-codex.toml +9 -0
- package/config/providers/openrouter.toml +5 -0
- package/config/router.toml +22 -0
- package/config/runtime.toml +114 -0
- package/config/skills/self-improvement.toml +15 -0
- package/config/skills/telegram-onboarding-spec.md +240 -0
- package/config/skills/workspace-monitor.toml +15 -0
- package/config/task-router.toml +42 -0
- package/install.sh +50 -0
- package/package.json +90 -0
- package/src/auth/auth-profiles.js +169 -0
- package/src/auth/openai-codex-oauth.js +285 -0
- package/src/battle.js +449 -0
- package/src/cli/help.js +265 -0
- package/src/cli/output-filter.js +49 -0
- package/src/cli/runtime-control.js +704 -0
- package/src/cli-main.js +2763 -0
- package/src/cli.js +78 -0
- package/src/config/loader.js +332 -0
- package/src/config/schema-validator.js +214 -0
- package/src/config/toml-lite.js +8 -0
- package/src/daemon/action-handlers.js +71 -0
- package/src/daemon/healing-tick.js +87 -0
- package/src/daemon/health-probes.js +90 -0
- package/src/daemon/notifier.js +57 -0
- package/src/daemon/nurse.js +218 -0
- package/src/daemon/repair-log.js +106 -0
- package/src/daemon/rule-staging.js +90 -0
- package/src/daemon/rules.js +29 -0
- package/src/daemon/telegram-commands.js +54 -0
- package/src/daemon/updater.js +85 -0
- package/src/jobs/job-runner.js +78 -0
- package/src/mcp/consumer.js +129 -0
- package/src/memory/active-recall.js +171 -0
- package/src/memory/backend-manager.js +97 -0
- package/src/memory/backends/file-backend.js +38 -0
- package/src/memory/backends/qmd-backend.js +219 -0
- package/src/memory/embedding-guards.js +24 -0
- package/src/memory/embedding-index.js +118 -0
- package/src/memory/embedding-service.js +179 -0
- package/src/memory/file-index.js +177 -0
- package/src/memory/memory-signature.js +5 -0
- package/src/memory/memory-store.js +648 -0
- package/src/memory/retrieval-planner.js +66 -0
- package/src/memory/scoring.js +145 -0
- package/src/memory/simhash.js +78 -0
- package/src/memory/sqlite-active-store.js +824 -0
- package/src/memory/write-policy.js +36 -0
- package/src/onboarding/aliases.js +33 -0
- package/src/onboarding/auth/api-key.js +224 -0
- package/src/onboarding/auth/ollama-detect.js +42 -0
- package/src/onboarding/clack-prompter.js +77 -0
- package/src/onboarding/doctor.js +530 -0
- package/src/onboarding/lock.js +42 -0
- package/src/onboarding/model-catalog.js +344 -0
- package/src/onboarding/phases/auth.js +589 -0
- package/src/onboarding/phases/build.js +130 -0
- package/src/onboarding/phases/choose.js +82 -0
- package/src/onboarding/phases/detect.js +98 -0
- package/src/onboarding/phases/hatch.js +216 -0
- package/src/onboarding/phases/identity.js +79 -0
- package/src/onboarding/phases/ollama.js +345 -0
- package/src/onboarding/phases/scaffold.js +99 -0
- package/src/onboarding/phases/telegram.js +377 -0
- package/src/onboarding/phases/validate.js +204 -0
- package/src/onboarding/phases/verify.js +206 -0
- package/src/onboarding/platform.js +482 -0
- package/src/onboarding/status-bar.js +95 -0
- package/src/onboarding/templates.js +794 -0
- package/src/onboarding/toml-writer.js +38 -0
- package/src/onboarding/tui.js +250 -0
- package/src/onboarding/uninstall.js +153 -0
- package/src/onboarding/wizard.js +499 -0
- package/src/providers/anthropic.js +168 -0
- package/src/providers/base.js +247 -0
- package/src/providers/circuit-breaker.js +136 -0
- package/src/providers/ollama.js +163 -0
- package/src/providers/openai-codex.js +149 -0
- package/src/providers/openrouter.js +136 -0
- package/src/providers/registry.js +36 -0
- package/src/providers/router.js +16 -0
- package/src/runtime/bootstrap-cache.js +47 -0
- package/src/runtime/capabilities-prompt.js +25 -0
- package/src/runtime/completion-ping.js +99 -0
- package/src/runtime/config-validator.js +121 -0
- package/src/runtime/context-ledger.js +360 -0
- package/src/runtime/cutover-readiness.js +42 -0
- package/src/runtime/daemon.js +729 -0
- package/src/runtime/delivery-ack.js +195 -0
- package/src/runtime/delivery-adapters/local-file.js +41 -0
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
- package/src/runtime/delivery-adapters/shadow.js +13 -0
- package/src/runtime/delivery-adapters/standalone-http.js +98 -0
- package/src/runtime/delivery-adapters/telegram.js +104 -0
- package/src/runtime/delivery-adapters/tui.js +128 -0
- package/src/runtime/delivery-manager.js +807 -0
- package/src/runtime/delivery-store.js +168 -0
- package/src/runtime/dependency-health.js +118 -0
- package/src/runtime/envelope.js +114 -0
- package/src/runtime/evaluation.js +1089 -0
- package/src/runtime/exec-approvals.js +216 -0
- package/src/runtime/executor.js +500 -0
- package/src/runtime/failure-ping.js +67 -0
- package/src/runtime/flows.js +83 -0
- package/src/runtime/guards.js +45 -0
- package/src/runtime/handoff.js +51 -0
- package/src/runtime/identity-cache.js +28 -0
- package/src/runtime/improvement-engine.js +109 -0
- package/src/runtime/improvement-harness.js +581 -0
- package/src/runtime/input-sanitiser.js +72 -0
- package/src/runtime/interaction-contract.js +347 -0
- package/src/runtime/lane-readiness.js +226 -0
- package/src/runtime/migration.js +323 -0
- package/src/runtime/model-resolution.js +78 -0
- package/src/runtime/network.js +64 -0
- package/src/runtime/notification-store.js +97 -0
- package/src/runtime/notifier.js +256 -0
- package/src/runtime/orchestrator.js +53 -0
- package/src/runtime/orphan-reaper.js +41 -0
- package/src/runtime/output-contract-schema.js +139 -0
- package/src/runtime/output-contract-validator.js +439 -0
- package/src/runtime/peer-readiness.js +69 -0
- package/src/runtime/peer-registry.js +133 -0
- package/src/runtime/pilot-status.js +108 -0
- package/src/runtime/prompt-builder.js +261 -0
- package/src/runtime/provider-attempt.js +582 -0
- package/src/runtime/report-fallback.js +71 -0
- package/src/runtime/result-normalizer.js +183 -0
- package/src/runtime/retention.js +74 -0
- package/src/runtime/review.js +244 -0
- package/src/runtime/route-job.js +15 -0
- package/src/runtime/run-store.js +38 -0
- package/src/runtime/schedule.js +88 -0
- package/src/runtime/scheduler-state.js +434 -0
- package/src/runtime/scheduler.js +656 -0
- package/src/runtime/session-compactor.js +182 -0
- package/src/runtime/session-search.js +155 -0
- package/src/runtime/slack-inbound.js +249 -0
- package/src/runtime/ssrf.js +102 -0
- package/src/runtime/status-aggregator.js +330 -0
- package/src/runtime/task-contract.js +140 -0
- package/src/runtime/task-packet.js +107 -0
- package/src/runtime/task-router.js +140 -0
- package/src/runtime/telegram-inbound.js +1565 -0
- package/src/runtime/token-counter.js +134 -0
- package/src/runtime/token-estimator.js +59 -0
- package/src/runtime/tool-loop.js +200 -0
- package/src/runtime/transport-server.js +311 -0
- package/src/runtime/tui-server.js +411 -0
- package/src/runtime/ulid.js +44 -0
- package/src/security/ssrf-check.js +197 -0
- package/src/setup.js +369 -0
- package/src/shadow/bridge.js +303 -0
- package/src/skills/loader.js +84 -0
- package/src/tools/catalog.json +49 -0
- package/src/tools/cli-delegate.js +44 -0
- package/src/tools/mcp-client.js +106 -0
- package/src/tools/micro/cancel-task.js +6 -0
- package/src/tools/micro/complete-task.js +6 -0
- package/src/tools/micro/fail-task.js +6 -0
- package/src/tools/micro/http-fetch.js +74 -0
- package/src/tools/micro/index.js +36 -0
- package/src/tools/micro/lcm-recall.js +60 -0
- package/src/tools/micro/list-dir.js +17 -0
- package/src/tools/micro/list-skills.js +46 -0
- package/src/tools/micro/load-skill.js +38 -0
- package/src/tools/micro/memory-search.js +45 -0
- package/src/tools/micro/read-file.js +11 -0
- package/src/tools/micro/session-search.js +54 -0
- package/src/tools/micro/shell-exec.js +43 -0
- package/src/tools/micro/trigger-job.js +79 -0
- package/src/tools/micro/web-search.js +58 -0
- package/src/tools/micro/workspace-paths.js +39 -0
- package/src/tools/micro/write-file.js +14 -0
- package/src/tools/micro/write-memory.js +41 -0
- package/src/tools/registry.js +348 -0
- package/src/tools/tool-result-contract.js +36 -0
- package/src/tui/chat.js +835 -0
- package/src/tui/renderer.js +175 -0
- package/src/tui/socket-client.js +217 -0
- package/src/utils/canonical-json.js +29 -0
- package/src/utils/compaction.js +30 -0
- package/src/utils/env-loader.js +5 -0
- package/src/utils/errors.js +80 -0
- package/src/utils/fs.js +101 -0
- package/src/utils/ids.js +5 -0
- package/src/utils/model-context-limits.js +30 -0
- package/src/utils/token-budget.js +74 -0
- package/src/utils/usage-cost.js +25 -0
- package/src/utils/usage-metrics.js +14 -0
- package/vendor/smol-toml-1.5.2.tgz +0 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { annotateError } from "../runtime/network.js";
|
|
2
|
+
import { fetchWithOutboundPolicy } from "../runtime/ssrf.js";
|
|
3
|
+
import { OUTBOUND_ADDRESS_POLICY } from "../security/ssrf-check.js";
|
|
4
|
+
import { getAuthProfile, resolveProfileSecret } from "../auth/auth-profiles.js";
|
|
5
|
+
|
|
6
|
+
export class ProviderAdapter {
|
|
7
|
+
constructor(config = {}, dependencies = {}) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.fetchImpl = dependencies.fetchImpl || globalThis.fetch;
|
|
10
|
+
this.lookupImpl = dependencies.lookupImpl;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
requireFetch() {
|
|
14
|
+
if (!this.fetchImpl) {
|
|
15
|
+
throw new Error("No fetch implementation available");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get authRef() {
|
|
20
|
+
return this.config.authRef || "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get authProfileId() {
|
|
24
|
+
if (!this.authRef.startsWith("profile:")) return null;
|
|
25
|
+
return this.authRef.slice("profile:".length);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
resolveAuthProfile() {
|
|
29
|
+
const profileId = this.authProfileId;
|
|
30
|
+
if (!profileId) return null;
|
|
31
|
+
return getAuthProfile(profileId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
resolveAuthToken() {
|
|
35
|
+
if (this.authRef.startsWith("env:")) {
|
|
36
|
+
const envName = this.authRef.slice(4);
|
|
37
|
+
return process.env[envName] || null;
|
|
38
|
+
}
|
|
39
|
+
if (this.authRef.startsWith("profile:")) {
|
|
40
|
+
return resolveProfileSecret(this.resolveAuthProfile());
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ensureAuthToken() {
|
|
46
|
+
const token = this.resolveAuthToken();
|
|
47
|
+
if (!token) {
|
|
48
|
+
throw new Error(`Missing auth token for ${this.config.id || "provider"} (${this.authRef})`);
|
|
49
|
+
}
|
|
50
|
+
return token;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
buildUrl(endpoint) {
|
|
54
|
+
const baseUrl = String(this.config.baseUrl || "").replace(/\/$/, "");
|
|
55
|
+
const pathPart = String(endpoint || "").replace(/^\//, "");
|
|
56
|
+
return `${baseUrl}/${pathPart}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
buildRequestOptions(options = {}, runtimeOptions = {}) {
|
|
60
|
+
const timeoutMs = Number((runtimeOptions.timeoutMs ?? this.config.defaultTimeoutMs) || 0);
|
|
61
|
+
if (!timeoutMs || typeof AbortSignal?.timeout !== "function") {
|
|
62
|
+
return options;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
...options,
|
|
67
|
+
signal: options.signal || AbortSignal.timeout(timeoutMs)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
resolveRequestUrl(target) {
|
|
72
|
+
if (typeof target === "string" && /^[a-z]+:\/\//i.test(target)) {
|
|
73
|
+
return target;
|
|
74
|
+
}
|
|
75
|
+
return this.buildUrl(target);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async fetchResponse(target, options = {}, runtimeOptions = {}) {
|
|
79
|
+
this.requireFetch();
|
|
80
|
+
const timeoutMs = Number((runtimeOptions.timeoutMs ?? this.config.defaultTimeoutMs) || 0);
|
|
81
|
+
const isOllama = this.config.id === "ollama" || this.config.adapter === "ollama";
|
|
82
|
+
try {
|
|
83
|
+
// Provider URLs come from runtime configuration. Only Ollama is meant to
|
|
84
|
+
// stay local, and even then it must resolve to loopback.
|
|
85
|
+
return await fetchWithOutboundPolicy(
|
|
86
|
+
this.resolveRequestUrl(target),
|
|
87
|
+
this.buildRequestOptions(options, runtimeOptions),
|
|
88
|
+
{
|
|
89
|
+
fetchImpl: this.fetchImpl,
|
|
90
|
+
lookupImpl: this.lookupImpl,
|
|
91
|
+
surface: "provider",
|
|
92
|
+
privateAddressMessage: `Provider request blocked — target resolves to a private/reserved IP address for ${this.config.id || "provider"}.`,
|
|
93
|
+
loopbackOnlyMessage: `Ollama base URL must resolve to loopback only; refusing ${this.resolveRequestUrl(target)}.`,
|
|
94
|
+
addressPolicy: isOllama ? OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK : OUTBOUND_ADDRESS_POLICY.BLOCK_PRIVATE,
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
99
|
+
throw annotateError(
|
|
100
|
+
new Error(`Provider request timed out after ${timeoutMs || "configured"} ms for ${this.config.id || "provider"}`),
|
|
101
|
+
{
|
|
102
|
+
surface: "provider"
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
throw annotateError(error, { surface: "provider" });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getCapabilities() {
|
|
111
|
+
return {
|
|
112
|
+
supportsToolDefinitions: false,
|
|
113
|
+
supportsToolCalls: false,
|
|
114
|
+
structuredOutputMode: "none",
|
|
115
|
+
supportsReasoningSchema: false,
|
|
116
|
+
tokenCountingMode: "heuristic",
|
|
117
|
+
toolReliabilityTier: "unsupported",
|
|
118
|
+
supportsVision: false
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
supportsNativeStructuredOutput() {
|
|
123
|
+
return new Set(["response_format", "forced_tool", "constrained_decoding"]).has(
|
|
124
|
+
this.getCapabilities().structuredOutputMode
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
buildStructuredOutputFormat(_input) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async countTokens(_input) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async listModels() {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
normalizeResponse(raw) {
|
|
141
|
+
if (!raw) {
|
|
142
|
+
return {
|
|
143
|
+
summary: "Provider returned no data.",
|
|
144
|
+
output: "",
|
|
145
|
+
nextActions: [],
|
|
146
|
+
reasoning: null,
|
|
147
|
+
raw
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const text = this.extractText(raw);
|
|
152
|
+
const parsedJson = text ? this.parseStructuredJson(text) : null;
|
|
153
|
+
if (parsedJson) {
|
|
154
|
+
return {
|
|
155
|
+
summary: parsedJson.summary || "Provider returned structured JSON output.",
|
|
156
|
+
output: parsedJson.output || text,
|
|
157
|
+
nextActions: Array.isArray(parsedJson.nextActions) ? parsedJson.nextActions : [],
|
|
158
|
+
reasoning: parsedJson.analysis || null,
|
|
159
|
+
raw
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (text) {
|
|
164
|
+
return {
|
|
165
|
+
summary: text.slice(0, 200) || "Provider responded.",
|
|
166
|
+
output: text,
|
|
167
|
+
nextActions: [],
|
|
168
|
+
reasoning: null,
|
|
169
|
+
raw
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
summary: "Provider responded with structured data.",
|
|
175
|
+
output: JSON.stringify(raw, null, 2),
|
|
176
|
+
nextActions: [],
|
|
177
|
+
reasoning: null,
|
|
178
|
+
raw
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
extractText(raw) {
|
|
183
|
+
return raw?.content && Array.isArray(raw.content)
|
|
184
|
+
? raw.content
|
|
185
|
+
.map((item) => item.text || item.content || "")
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join("\n")
|
|
188
|
+
: raw?.choices?.[0]?.message?.content || raw?.message?.content
|
|
189
|
+
? String(raw?.choices?.[0]?.message?.content || raw?.message?.content)
|
|
190
|
+
: null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
parseStructuredJson(text) {
|
|
194
|
+
const trimmed = String(text).trim();
|
|
195
|
+
const fencedMatch = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
196
|
+
const candidate = fencedMatch ? fencedMatch[1].trim() : trimmed;
|
|
197
|
+
if (!(candidate.startsWith("{") && candidate.endsWith("}"))) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(candidate);
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async postJson(endpoint, body, headers = {}, runtimeOptions = {}) {
|
|
209
|
+
let response;
|
|
210
|
+
try {
|
|
211
|
+
response = await this.fetchResponse(
|
|
212
|
+
endpoint,
|
|
213
|
+
{
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: {
|
|
216
|
+
"content-type": "application/json",
|
|
217
|
+
...headers
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify(body)
|
|
220
|
+
},
|
|
221
|
+
runtimeOptions
|
|
222
|
+
);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
const text = await response.text();
|
|
227
|
+
let data = null;
|
|
228
|
+
if (text) {
|
|
229
|
+
try {
|
|
230
|
+
data = JSON.parse(text);
|
|
231
|
+
} catch {
|
|
232
|
+
data = text;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
throw annotateError(
|
|
238
|
+
new Error(`Provider error ${response.status}: ${typeof data === "string" ? data : JSON.stringify(data)}`),
|
|
239
|
+
{
|
|
240
|
+
surface: "provider"
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return data;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const DEFAULT_TRANSIENT_CODES = new Set([408, 429, 500, 502, 503, 504]);
|
|
2
|
+
|
|
3
|
+
export function ensureBreakerSchema(db) {
|
|
4
|
+
db.exec(`
|
|
5
|
+
create table if not exists provider_breakers (
|
|
6
|
+
provider_key text primary key,
|
|
7
|
+
state text not null default 'closed',
|
|
8
|
+
failure_count integer not null default 0,
|
|
9
|
+
last_failure_at text,
|
|
10
|
+
last_success_at text,
|
|
11
|
+
opened_at text,
|
|
12
|
+
half_open_at text
|
|
13
|
+
);
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class CircuitBreaker {
|
|
18
|
+
constructor(providerKey, db, config = {}) {
|
|
19
|
+
this._key = providerKey;
|
|
20
|
+
this._db = db;
|
|
21
|
+
this._failureThreshold = config.failureThreshold ?? 5;
|
|
22
|
+
this._resetTimeoutSeconds = config.resetTimeoutSeconds ?? 30;
|
|
23
|
+
this._transientCodes = new Set(config.transientCodes ?? DEFAULT_TRANSIENT_CODES);
|
|
24
|
+
|
|
25
|
+
ensureBreakerSchema(db);
|
|
26
|
+
this._load();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static forProvider(providerKey, db, config = {}) {
|
|
30
|
+
return new CircuitBreaker(providerKey, db, config);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_load() {
|
|
34
|
+
const row = this._db.prepare("select * from provider_breakers where provider_key = ?").get(this._key);
|
|
35
|
+
if (row) {
|
|
36
|
+
this._state = row.state;
|
|
37
|
+
this._failureCount = row.failure_count;
|
|
38
|
+
this._lastFailureAt = row.last_failure_at;
|
|
39
|
+
this._lastSuccessAt = row.last_success_at;
|
|
40
|
+
this._openedAt = row.opened_at;
|
|
41
|
+
this._halfOpenAt = row.half_open_at;
|
|
42
|
+
} else {
|
|
43
|
+
this._state = "closed";
|
|
44
|
+
this._failureCount = 0;
|
|
45
|
+
this._lastFailureAt = null;
|
|
46
|
+
this._lastSuccessAt = null;
|
|
47
|
+
this._openedAt = null;
|
|
48
|
+
this._halfOpenAt = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_persist() {
|
|
53
|
+
this._db.prepare(`
|
|
54
|
+
insert into provider_breakers (provider_key, state, failure_count, last_failure_at, last_success_at, opened_at, half_open_at)
|
|
55
|
+
values (?, ?, ?, ?, ?, ?, ?)
|
|
56
|
+
on conflict(provider_key) do update set
|
|
57
|
+
state = excluded.state,
|
|
58
|
+
failure_count = excluded.failure_count,
|
|
59
|
+
last_failure_at = excluded.last_failure_at,
|
|
60
|
+
last_success_at = excluded.last_success_at,
|
|
61
|
+
opened_at = excluded.opened_at,
|
|
62
|
+
half_open_at = excluded.half_open_at
|
|
63
|
+
`).run(
|
|
64
|
+
this._key,
|
|
65
|
+
this._state,
|
|
66
|
+
this._failureCount,
|
|
67
|
+
this._lastFailureAt,
|
|
68
|
+
this._lastSuccessAt,
|
|
69
|
+
this._openedAt,
|
|
70
|
+
this._halfOpenAt
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_checkHalfOpen() {
|
|
75
|
+
if (this._state !== "open") return;
|
|
76
|
+
if (!this._openedAt) return;
|
|
77
|
+
const elapsed = (Date.now() - new Date(this._openedAt).getTime()) / 1000;
|
|
78
|
+
if (elapsed >= this._resetTimeoutSeconds) {
|
|
79
|
+
this._state = "half_open";
|
|
80
|
+
this._halfOpenAt = new Date().toISOString();
|
|
81
|
+
this._persist();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
isClosed() {
|
|
86
|
+
this._checkHalfOpen();
|
|
87
|
+
return this._state === "closed";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
isOpen() {
|
|
91
|
+
this._checkHalfOpen();
|
|
92
|
+
return this._state === "open";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
isHalfOpen() {
|
|
96
|
+
this._checkHalfOpen();
|
|
97
|
+
return this._state === "half_open";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
failureCount() {
|
|
101
|
+
return this._failureCount;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
retryAfter() {
|
|
105
|
+
if (this._state !== "open" || !this._openedAt) return 0;
|
|
106
|
+
const elapsed = (Date.now() - new Date(this._openedAt).getTime()) / 1000;
|
|
107
|
+
return Math.max(0, this._resetTimeoutSeconds - elapsed);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
recordSuccess() {
|
|
111
|
+
this._state = "closed";
|
|
112
|
+
this._failureCount = 0;
|
|
113
|
+
this._lastSuccessAt = new Date().toISOString();
|
|
114
|
+
this._openedAt = null;
|
|
115
|
+
this._halfOpenAt = null;
|
|
116
|
+
this._persist();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
recordFailure(errorCode) {
|
|
120
|
+
if (!this._transientCodes.has(errorCode)) return;
|
|
121
|
+
|
|
122
|
+
this._failureCount += 1;
|
|
123
|
+
this._lastFailureAt = new Date().toISOString();
|
|
124
|
+
|
|
125
|
+
if (this._state === "half_open") {
|
|
126
|
+
this._state = "open";
|
|
127
|
+
this._openedAt = new Date().toISOString();
|
|
128
|
+
this._halfOpenAt = null;
|
|
129
|
+
} else if (this._failureCount >= this._failureThreshold) {
|
|
130
|
+
this._state = "open";
|
|
131
|
+
this._openedAt = new Date().toISOString();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this._persist();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { ProviderAdapter } from "./base.js";
|
|
2
|
+
|
|
3
|
+
export class OllamaAdapter extends ProviderAdapter {
|
|
4
|
+
static providerId = "ollama";
|
|
5
|
+
|
|
6
|
+
getCapabilities() {
|
|
7
|
+
return {
|
|
8
|
+
supportsToolDefinitions: true,
|
|
9
|
+
supportsToolCalls: true,
|
|
10
|
+
structuredOutputMode: "prompt_contract",
|
|
11
|
+
supportsReasoningSchema: false,
|
|
12
|
+
tokenCountingMode: "heuristic",
|
|
13
|
+
toolReliabilityTier: "medium",
|
|
14
|
+
supportsVision: false
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
normalizeModel(model) {
|
|
19
|
+
if (typeof model !== "string") return model;
|
|
20
|
+
return model.startsWith("ollama/") ? model.slice("ollama/".length) : model;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
buildInvokePayload({ model, system, messages, tools, options }) {
|
|
24
|
+
const payload = {
|
|
25
|
+
model: this.normalizeModel(model),
|
|
26
|
+
stream: false,
|
|
27
|
+
system,
|
|
28
|
+
messages,
|
|
29
|
+
options
|
|
30
|
+
};
|
|
31
|
+
// Ollama /api/chat supports OpenAI-compatible tools array
|
|
32
|
+
if (tools?.length) payload.tools = tools;
|
|
33
|
+
return payload;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async invoke(input) {
|
|
37
|
+
return this.postJson("api/chat", this.buildInvokePayload(input), {}, {
|
|
38
|
+
timeoutMs: input.timeoutMs
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
normalizeResponse(raw) {
|
|
43
|
+
// Handle Ollama tool_calls (OpenAI-compatible format)
|
|
44
|
+
const toolCalls = raw?.message?.tool_calls;
|
|
45
|
+
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
46
|
+
// Convert to Anthropic-style tool_use blocks for executor compatibility
|
|
47
|
+
const content = toolCalls.map((tc) => ({
|
|
48
|
+
type: "tool_use",
|
|
49
|
+
id: tc.id || `ollama_tool_${Date.now()}`,
|
|
50
|
+
name: tc.function?.name || tc.name,
|
|
51
|
+
input: tc.function?.arguments
|
|
52
|
+
? (typeof tc.function.arguments === "string"
|
|
53
|
+
? JSON.parse(tc.function.arguments)
|
|
54
|
+
: tc.function.arguments)
|
|
55
|
+
: {}
|
|
56
|
+
}));
|
|
57
|
+
return {
|
|
58
|
+
summary: `Tool call: ${content.map(c => c.name).join(", ")}`,
|
|
59
|
+
output: null,
|
|
60
|
+
nextActions: [],
|
|
61
|
+
reasoning: null,
|
|
62
|
+
raw,
|
|
63
|
+
content
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const text = raw?.message?.content ? String(raw.message.content) : null;
|
|
68
|
+
const parsedJson = text ? this.parseStructuredJson(text) : null;
|
|
69
|
+
if (parsedJson) {
|
|
70
|
+
return {
|
|
71
|
+
summary: parsedJson.summary || "Provider returned structured JSON output.",
|
|
72
|
+
output: parsedJson.output || text,
|
|
73
|
+
nextActions: Array.isArray(parsedJson.nextActions) ? parsedJson.nextActions : [],
|
|
74
|
+
reasoning: parsedJson.analysis || null,
|
|
75
|
+
raw
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (text) {
|
|
79
|
+
return {
|
|
80
|
+
summary: text.slice(0, 200) || "Provider responded.",
|
|
81
|
+
output: text,
|
|
82
|
+
nextActions: [],
|
|
83
|
+
reasoning: null,
|
|
84
|
+
raw
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return super.normalizeResponse(raw);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async embed({ model, input }) {
|
|
92
|
+
const payload = {
|
|
93
|
+
model: this.normalizeModel(model),
|
|
94
|
+
input
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const result = await this.postJson("api/embed", payload, {}, {
|
|
99
|
+
timeoutMs: input.timeoutMs
|
|
100
|
+
});
|
|
101
|
+
if (Array.isArray(result.embeddings) && result.embeddings.length > 0) {
|
|
102
|
+
return {
|
|
103
|
+
embeddings: result.embeddings
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(result.embedding)) {
|
|
107
|
+
return {
|
|
108
|
+
embeddings: [result.embedding]
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (!String(error.message || "").includes("404")) throw error;
|
|
114
|
+
const fallback = await this.postJson("api/embeddings", payload, {}, {
|
|
115
|
+
timeoutMs: input.timeoutMs
|
|
116
|
+
});
|
|
117
|
+
if (Array.isArray(fallback.embedding)) {
|
|
118
|
+
return {
|
|
119
|
+
embeddings: [fallback.embedding]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return fallback;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async healthCheck() {
|
|
127
|
+
// Ollama is intentionally local-only. fetchResponse enforces loopback-only
|
|
128
|
+
// on the configured baseUrl before any health probe leaves the process.
|
|
129
|
+
const response = await this.fetchResponse("api/tags", {
|
|
130
|
+
method: "GET"
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
ok: response.ok,
|
|
134
|
+
status: response.status
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async listModels() {
|
|
139
|
+
const response = await this.fetchResponse("api/tags", {
|
|
140
|
+
method: "GET"
|
|
141
|
+
});
|
|
142
|
+
const text = await response.text();
|
|
143
|
+
let data = null;
|
|
144
|
+
if (text) {
|
|
145
|
+
try {
|
|
146
|
+
data = JSON.parse(text);
|
|
147
|
+
} catch {
|
|
148
|
+
data = null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
throw new Error(`Provider error ${response.status}: ${typeof data === "string" ? data : JSON.stringify(data)}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const models = Array.isArray(data?.models)
|
|
156
|
+
? data.models
|
|
157
|
+
.map((item) => item?.name || item?.model || null)
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
: [];
|
|
160
|
+
|
|
161
|
+
return models;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { resolveOpenAICodexAccess } from "../auth/openai-codex-oauth.js";
|
|
2
|
+
import { ProviderAdapter } from "./base.js";
|
|
3
|
+
|
|
4
|
+
export class OpenAICodexAdapter extends ProviderAdapter {
|
|
5
|
+
static providerId = "openai-codex";
|
|
6
|
+
|
|
7
|
+
normalizeModel(model) {
|
|
8
|
+
if (typeof model !== "string") return model;
|
|
9
|
+
return model.startsWith("openai-codex/") ? model.slice("openai-codex/".length) : model;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getCapabilities() {
|
|
13
|
+
return {
|
|
14
|
+
supportsToolDefinitions: true,
|
|
15
|
+
supportsToolCalls: true,
|
|
16
|
+
structuredOutputMode: "response_format",
|
|
17
|
+
supportsReasoningSchema: false,
|
|
18
|
+
tokenCountingMode: "heuristic",
|
|
19
|
+
toolReliabilityTier: "high",
|
|
20
|
+
supportsVision: true
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
buildStructuredOutputFormat(input) {
|
|
25
|
+
if (!input?.responseSchema?.schema) return null;
|
|
26
|
+
return {
|
|
27
|
+
type: "json_schema",
|
|
28
|
+
json_schema: {
|
|
29
|
+
name: input.responseSchema.name || "nemoris_response",
|
|
30
|
+
strict: true,
|
|
31
|
+
schema: input.responseSchema.schema
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
buildInvokePayload(input) {
|
|
37
|
+
const { model: rawModel, system, messages, maxTokens = 2048, tools, responseSchema } = input;
|
|
38
|
+
const model = this.normalizeModel(rawModel);
|
|
39
|
+
const normalizedMessages = [];
|
|
40
|
+
if (system) {
|
|
41
|
+
normalizedMessages.push({
|
|
42
|
+
role: "system",
|
|
43
|
+
content: system
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
normalizedMessages.push(...messages);
|
|
47
|
+
|
|
48
|
+
const payload = {
|
|
49
|
+
model,
|
|
50
|
+
messages: normalizedMessages,
|
|
51
|
+
max_tokens: maxTokens
|
|
52
|
+
};
|
|
53
|
+
if (tools?.length) payload.tools = tools;
|
|
54
|
+
const responseFormat = this.buildStructuredOutputFormat({ responseSchema });
|
|
55
|
+
if (responseFormat) payload.response_format = responseFormat;
|
|
56
|
+
return payload;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getAccessToken() {
|
|
60
|
+
if (this.authRef.startsWith("profile:")) {
|
|
61
|
+
const profileId = this.authRef.slice("profile:".length);
|
|
62
|
+
const resolved = await resolveOpenAICodexAccess(profileId, {
|
|
63
|
+
fetchImpl: this.fetchImpl
|
|
64
|
+
});
|
|
65
|
+
return resolved.token;
|
|
66
|
+
}
|
|
67
|
+
return this.ensureAuthToken();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async invoke(input) {
|
|
71
|
+
const token = await this.getAccessToken();
|
|
72
|
+
return this.postJson(
|
|
73
|
+
"chat/completions",
|
|
74
|
+
this.buildInvokePayload(input),
|
|
75
|
+
{
|
|
76
|
+
authorization: `Bearer ${token}`
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
timeoutMs: input.timeoutMs
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
normalizeResponse(raw) {
|
|
85
|
+
const text = raw?.choices?.[0]?.message?.content
|
|
86
|
+
? String(raw.choices[0].message.content)
|
|
87
|
+
: null;
|
|
88
|
+
const parsedJson = text ? this.parseStructuredJson(text) : null;
|
|
89
|
+
if (parsedJson) {
|
|
90
|
+
return {
|
|
91
|
+
summary: parsedJson.summary || "Provider returned structured JSON output.",
|
|
92
|
+
output: parsedJson.output || text,
|
|
93
|
+
nextActions: Array.isArray(parsedJson.nextActions) ? parsedJson.nextActions : [],
|
|
94
|
+
reasoning: parsedJson.analysis || null,
|
|
95
|
+
raw
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (text) {
|
|
99
|
+
return {
|
|
100
|
+
summary: text.slice(0, 200) || "Provider responded.",
|
|
101
|
+
output: text,
|
|
102
|
+
nextActions: [],
|
|
103
|
+
reasoning: null,
|
|
104
|
+
raw
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return super.normalizeResponse(raw);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async healthCheck() {
|
|
112
|
+
const token = await this.getAccessToken();
|
|
113
|
+
const response = await this.fetchResponse("models", {
|
|
114
|
+
method: "GET",
|
|
115
|
+
headers: {
|
|
116
|
+
authorization: `Bearer ${token}`
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
ok: response.ok,
|
|
121
|
+
status: response.status
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async listModels() {
|
|
126
|
+
const token = await this.getAccessToken();
|
|
127
|
+
const response = await this.fetchResponse("models", {
|
|
128
|
+
method: "GET",
|
|
129
|
+
headers: {
|
|
130
|
+
authorization: `Bearer ${token}`
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
const text = await response.text();
|
|
134
|
+
let data = null;
|
|
135
|
+
if (text) {
|
|
136
|
+
try {
|
|
137
|
+
data = JSON.parse(text);
|
|
138
|
+
} catch {
|
|
139
|
+
data = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
throw new Error(`Provider error ${response.status}: ${typeof data === "string" ? data : JSON.stringify(data)}`);
|
|
144
|
+
}
|
|
145
|
+
return Array.isArray(data?.data)
|
|
146
|
+
? data.data.map((item) => item?.id).filter(Boolean)
|
|
147
|
+
: [];
|
|
148
|
+
}
|
|
149
|
+
}
|