@opencompress/opencompress 1.9.2 → 2.0.1

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.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * OpenCompress Plugin for OpenClaw v2
3
+ *
4
+ * Registers as a Provider — users select opencompress/* models.
5
+ * Runs a local HTTP proxy that compresses requests via opencompress.ai
6
+ * then forwards to the user's upstream provider. Keys stay local.
7
+ */
8
+ type ModelApi = "openai-completions" | "openai-responses" | "anthropic-messages" | "google-generative-ai";
9
+ type ModelDefinitionConfig = {
10
+ id: string;
11
+ name: string;
12
+ api?: ModelApi;
13
+ reasoning?: boolean;
14
+ input?: string[];
15
+ cost?: {
16
+ input: number;
17
+ output: number;
18
+ cacheRead: number;
19
+ cacheWrite: number;
20
+ };
21
+ contextWindow?: number;
22
+ maxTokens?: number;
23
+ [key: string]: unknown;
24
+ };
25
+ type ModelProviderConfig = {
26
+ baseUrl: string;
27
+ apiKey?: string;
28
+ api?: ModelApi;
29
+ models: ModelDefinitionConfig[];
30
+ [key: string]: unknown;
31
+ };
32
+ type ProviderPlugin = {
33
+ id: string;
34
+ label: string;
35
+ aliases?: string[];
36
+ envVars?: string[];
37
+ models?: ModelProviderConfig;
38
+ auth: Array<{
39
+ id: string;
40
+ label: string;
41
+ hint?: string;
42
+ kind: string;
43
+ run: (ctx: any) => Promise<any>;
44
+ }>;
45
+ };
46
+ type OpenClawPluginApi = {
47
+ id: string;
48
+ name: string;
49
+ version?: string;
50
+ config: Record<string, any>;
51
+ pluginConfig?: Record<string, any>;
52
+ logger: {
53
+ info: (msg: string) => void;
54
+ warn: (msg: string) => void;
55
+ error: (msg: string) => void;
56
+ };
57
+ registerProvider: (provider: ProviderPlugin) => void;
58
+ registerService: (service: {
59
+ id: string;
60
+ start: () => void | Promise<void>;
61
+ stop?: () => void | Promise<void>;
62
+ }) => void;
63
+ registerCommand: (command: {
64
+ name: string;
65
+ description: string;
66
+ acceptsArgs?: boolean;
67
+ handler: (ctx: {
68
+ args?: string;
69
+ }) => Promise<{
70
+ text: string;
71
+ }>;
72
+ }) => void;
73
+ resolvePath: (input: string) => string;
74
+ on: (hookName: string, handler: unknown) => void;
75
+ };
76
+ declare const plugin: {
77
+ id: string;
78
+ name: string;
79
+ description: string;
80
+ version: string;
81
+ register(api: OpenClawPluginApi): void;
82
+ };
83
+
84
+ export { plugin as default };
package/dist/index.js CHANGED
@@ -1,512 +1,423 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
1
+ // src/config.ts
2
+ var VERSION = "2.0.1";
3
+ var PROXY_PORT = 8401;
4
+ var PROXY_HOST = "127.0.0.1";
5
+ var OCC_API = "https://www.opencompress.ai/api";
6
+ var PROVIDER_ID = "opencompress";
7
7
 
8
- // src/index.ts
9
- var VERSION = "1.9.2";
10
- var DEFAULT_BASE_URL = "https://www.opencompress.ai/api";
11
- function getApiKey(api2) {
12
- const auth = api2.config.auth;
13
- const fromConfig = auth?.profiles?.opencompress?.credentials?.["api-key"]?.apiKey;
14
- if (fromConfig) return fromConfig;
15
- if (process.env.OPENCOMPRESS_API_KEY) return process.env.OPENCOMPRESS_API_KEY;
16
- if (api2.pluginConfig?.apiKey) return api2.pluginConfig.apiKey;
17
- return void 0;
18
- }
19
- function persistApiKey(api2, apiKey) {
20
- try {
21
- const fs = __require("fs");
22
- const path = __require("path");
23
- const agentDir = api2.resolvePath(".");
24
- const authPath = path.join(agentDir, "auth-profiles.json");
25
- let profiles = {
26
- version: 1,
27
- profiles: {}
28
- };
29
- if (fs.existsSync(authPath)) {
30
- try {
31
- profiles = JSON.parse(fs.readFileSync(authPath, "utf-8"));
32
- } catch {
33
- }
8
+ // src/models.ts
9
+ function resolveUpstream(modelId, providers) {
10
+ const stripped = modelId.replace(/^opencompress\//, "");
11
+ if (stripped === "auto") {
12
+ for (const [id, config2] of Object.entries(providers)) {
13
+ if (id === "opencompress") continue;
14
+ const firstModel = config2.models?.[0]?.id;
15
+ if (!firstModel) continue;
16
+ return {
17
+ upstreamProvider: id,
18
+ upstreamModel: firstModel,
19
+ upstreamKey: config2.apiKey,
20
+ upstreamBaseUrl: config2.baseUrl,
21
+ upstreamApi: config2.api || "openai-completions"
22
+ };
34
23
  }
35
- profiles.profiles["opencompress:default"] = {
36
- type: "api_key",
37
- provider: "opencompress",
38
- key: apiKey
39
- };
40
- fs.writeFileSync(authPath, JSON.stringify(profiles, null, 2) + "\n");
41
- } catch {
24
+ return null;
42
25
  }
43
- }
44
- function proxyStatePath() {
45
- const os = __require("os");
46
- const path = __require("path");
47
- return path.join(os.homedir(), ".openclaw", "opencompress-proxy.json");
48
- }
49
- function readProxyState() {
50
- try {
51
- const fs = __require("fs");
52
- const p = proxyStatePath();
53
- if (!fs.existsSync(p)) return { enabled: false, originals: {} };
54
- return JSON.parse(fs.readFileSync(p, "utf-8"));
55
- } catch {
56
- return { enabled: false, originals: {} };
57
- }
58
- }
59
- function writeProxyState(state) {
60
- try {
61
- const fs = __require("fs");
62
- fs.writeFileSync(proxyStatePath(), JSON.stringify(state, null, 2) + "\n");
63
- } catch {
26
+ const slashIdx = stripped.indexOf("/");
27
+ if (slashIdx === -1) {
28
+ const config2 = providers[stripped];
29
+ if (!config2) return null;
30
+ return {
31
+ upstreamProvider: stripped,
32
+ upstreamModel: config2.models?.[0]?.id || stripped,
33
+ upstreamKey: config2.apiKey,
34
+ upstreamBaseUrl: config2.baseUrl,
35
+ upstreamApi: config2.api || "openai-completions"
36
+ };
64
37
  }
38
+ const upstreamProvider = stripped.slice(0, slashIdx);
39
+ const upstreamModel = stripped.slice(slashIdx + 1);
40
+ const config = providers[upstreamProvider];
41
+ if (!config) return null;
42
+ return {
43
+ upstreamProvider,
44
+ upstreamModel,
45
+ upstreamKey: config.apiKey,
46
+ upstreamBaseUrl: config.baseUrl,
47
+ upstreamApi: config.api || "openai-completions"
48
+ };
65
49
  }
66
- function readProvidersFromDisk() {
67
- try {
68
- const os = __require("os");
69
- const fs = __require("fs");
70
- const path = __require("path");
71
- const configPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
72
- if (!fs.existsSync(configPath)) return void 0;
73
- const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
74
- const providers = raw?.models?.providers;
75
- if (!providers || Object.keys(providers).length === 0) return void 0;
76
- return providers;
77
- } catch {
78
- return void 0;
50
+ function generateModelCatalog(providers) {
51
+ const models = [];
52
+ for (const [providerId, config] of Object.entries(providers)) {
53
+ if (providerId === "opencompress") continue;
54
+ for (const model of config.models || []) {
55
+ models.push({
56
+ ...model,
57
+ id: `opencompress/${providerId}/${model.id}`,
58
+ name: `${model.name || model.id} (compressed)`,
59
+ api: config.api || "openai-completions"
60
+ });
61
+ }
79
62
  }
63
+ models.unshift({
64
+ id: "opencompress/auto",
65
+ name: "OpenCompress Auto (compressed, uses default provider)",
66
+ api: "openai-completions",
67
+ reasoning: false,
68
+ input: ["text"],
69
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
70
+ contextWindow: 2e5,
71
+ maxTokens: 8192
72
+ });
73
+ return models;
80
74
  }
81
- function persistProviderToDisk(providerId, config) {
82
- const os = __require("os");
83
- const fs = __require("fs");
84
- const path = __require("path");
85
- try {
86
- const configPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
87
- if (fs.existsSync(configPath)) {
88
- const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
89
- if (!raw.models) raw.models = {};
90
- if (!raw.models.providers) raw.models.providers = {};
91
- raw.models.providers[providerId] = {
92
- baseUrl: config.baseUrl,
93
- api: config.api || "openai-completions",
94
- apiKey: config.apiKey || void 0,
95
- models: config.models?.map((m) => ({ id: m.id, name: m.name })) || [],
96
- ...config.headers ? { headers: config.headers } : {}
97
- };
98
- fs.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
75
+
76
+ // src/proxy.ts
77
+ import http from "http";
78
+ var server = null;
79
+ function startProxy(getProviders2, getOccKey2) {
80
+ if (server) return server;
81
+ server = http.createServer(async (req, res) => {
82
+ if (req.url === "/health" && req.method === "GET") {
83
+ res.writeHead(200, { "Content-Type": "application/json" });
84
+ res.end(JSON.stringify({ status: "ok", version: "2.0.0" }));
85
+ return;
99
86
  }
100
- } catch {
101
- }
102
- try {
103
- const agentsDir = path.join(os.homedir(), ".openclaw", "agents");
104
- if (!fs.existsSync(agentsDir)) return;
105
- for (const agent of fs.readdirSync(agentsDir)) {
106
- const modelsPath = path.join(agentsDir, agent, "agent", "models.json");
107
- if (!fs.existsSync(path.dirname(modelsPath))) continue;
108
- let data = { providers: {} };
109
- if (fs.existsSync(modelsPath)) {
87
+ if (req.method !== "POST") {
88
+ res.writeHead(405);
89
+ res.end("Method not allowed");
90
+ return;
91
+ }
92
+ const isMessages = req.url === "/v1/messages";
93
+ const isCompletions = req.url === "/v1/chat/completions";
94
+ if (!isMessages && !isCompletions) {
95
+ res.writeHead(404);
96
+ res.end("Not found");
97
+ return;
98
+ }
99
+ try {
100
+ const body = await readBody(req);
101
+ const parsed = JSON.parse(body);
102
+ const modelId = parsed.model || "opencompress/auto";
103
+ const upstream = resolveUpstream(modelId, getProviders2());
104
+ if (!upstream) {
105
+ res.writeHead(400, { "Content-Type": "application/json" });
106
+ res.end(JSON.stringify({
107
+ error: { message: `Cannot resolve upstream for model: ${modelId}. Check your provider config.` }
108
+ }));
109
+ return;
110
+ }
111
+ const occKey = getOccKey2();
112
+ if (!occKey) {
113
+ res.writeHead(401, { "Content-Type": "application/json" });
114
+ res.end(JSON.stringify({
115
+ error: { message: "No OpenCompress API key. Run: openclaw onboard opencompress" }
116
+ }));
117
+ return;
118
+ }
119
+ const occEndpoint = upstream.upstreamApi === "anthropic-messages" ? `${OCC_API}/v1/messages` : `${OCC_API}/v1/chat/completions`;
120
+ const headers = {
121
+ "Content-Type": "application/json",
122
+ "x-api-key": occKey
123
+ };
124
+ if (upstream.upstreamKey) {
125
+ headers["x-upstream-key"] = upstream.upstreamKey;
126
+ }
127
+ if (upstream.upstreamBaseUrl) {
128
+ headers["x-upstream-base-url"] = upstream.upstreamBaseUrl;
129
+ }
130
+ if (upstream.upstreamApi === "anthropic-messages") {
131
+ headers["anthropic-version"] = req.headers["anthropic-version"] || "2023-06-01";
132
+ }
133
+ for (const [key, val] of Object.entries(req.headers)) {
134
+ if (key.startsWith("anthropic-") && typeof val === "string") {
135
+ headers[key] = val;
136
+ }
137
+ }
138
+ parsed.model = upstream.upstreamModel;
139
+ const isStream = parsed.stream !== false;
140
+ if (isStream) {
141
+ res.writeHead(200, {
142
+ "Content-Type": "text/event-stream",
143
+ "Cache-Control": "no-cache",
144
+ Connection: "keep-alive"
145
+ });
146
+ const heartbeat = setInterval(() => {
147
+ try {
148
+ res.write(": heartbeat\n\n");
149
+ } catch {
150
+ clearInterval(heartbeat);
151
+ }
152
+ }, 2e3);
110
153
  try {
111
- data = JSON.parse(fs.readFileSync(modelsPath, "utf-8"));
154
+ const occRes = await fetch(occEndpoint, {
155
+ method: "POST",
156
+ headers,
157
+ body: JSON.stringify(parsed)
158
+ });
159
+ clearInterval(heartbeat);
160
+ if (!occRes.ok) {
161
+ const fallbackRes = await directUpstream(upstream, parsed, req.headers);
162
+ if (fallbackRes) {
163
+ for await (const chunk of fallbackRes.body) {
164
+ res.write(chunk);
165
+ }
166
+ } else {
167
+ res.write(`data: ${JSON.stringify({ error: { message: `OpenCompress error: ${occRes.status}` } })}
168
+
169
+ `);
170
+ }
171
+ res.end();
172
+ return;
173
+ }
174
+ for await (const chunk of occRes.body) {
175
+ res.write(chunk);
176
+ }
177
+ res.end();
178
+ } catch (err) {
179
+ clearInterval(heartbeat);
180
+ try {
181
+ const fallbackRes = await directUpstream(upstream, parsed, req.headers);
182
+ if (fallbackRes) {
183
+ for await (const chunk of fallbackRes.body) {
184
+ res.write(chunk);
185
+ }
186
+ }
187
+ } catch {
188
+ }
189
+ res.end();
190
+ }
191
+ } else {
192
+ try {
193
+ const occRes = await fetch(occEndpoint, {
194
+ method: "POST",
195
+ headers,
196
+ body: JSON.stringify(parsed)
197
+ });
198
+ if (!occRes.ok) {
199
+ const fallbackRes = await directUpstream(upstream, parsed, req.headers);
200
+ const fallbackBody = fallbackRes ? await fallbackRes.text() : JSON.stringify({ error: { message: "Compression + direct both failed" } });
201
+ res.writeHead(fallbackRes?.status || 502, { "Content-Type": "application/json" });
202
+ res.end(fallbackBody);
203
+ return;
204
+ }
205
+ const data = await occRes.text();
206
+ res.writeHead(200, { "Content-Type": "application/json" });
207
+ res.end(data);
112
208
  } catch {
209
+ const fallbackRes = await directUpstream(upstream, parsed, req.headers);
210
+ const fallbackBody = fallbackRes ? await fallbackRes.text() : JSON.stringify({ error: { message: "Both paths failed" } });
211
+ res.writeHead(fallbackRes?.status || 502, { "Content-Type": "application/json" });
212
+ res.end(fallbackBody);
113
213
  }
114
- if (!data.providers) data.providers = {};
115
214
  }
116
- data.providers[providerId] = {
117
- baseUrl: config.baseUrl,
118
- api: config.api || "openai-completions",
119
- apiKey: config.apiKey || void 0,
120
- models: config.models?.map((m) => ({
121
- id: m.id,
122
- name: m.name,
123
- api: m.api || config.api || "openai-completions",
124
- reasoning: m.reasoning ?? false,
125
- input: m.input || ["text"],
126
- cost: m.cost || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
127
- contextWindow: m.contextWindow || 2e5,
128
- maxTokens: m.maxTokens || 8192
129
- })) || [],
130
- ...config.headers ? { headers: config.headers } : {}
131
- };
132
- fs.writeFileSync(modelsPath, JSON.stringify(data, null, 2) + "\n");
215
+ } catch (err) {
216
+ res.writeHead(500, { "Content-Type": "application/json" });
217
+ res.end(JSON.stringify({ error: { message: String(err) } }));
133
218
  }
134
- } catch {
135
- }
219
+ });
220
+ server.listen(PROXY_PORT, PROXY_HOST, () => {
221
+ });
222
+ server.on("error", (err) => {
223
+ if (err.code === "EADDRINUSE") {
224
+ server = null;
225
+ }
226
+ });
227
+ return server;
136
228
  }
137
- function enableProxy(api2, baseUrl) {
138
- const occKey = getApiKey(api2);
139
- if (!occKey) return { proxied: [], skipped: [] };
140
- let providers = api2.config.models?.providers;
141
- let source = "api.config";
142
- if (!providers || Object.keys(providers).length === 0) {
143
- providers = readProvidersFromDisk();
144
- source = "disk (openclaw.json)";
229
+ function stopProxy() {
230
+ if (server) {
231
+ server.close();
232
+ server = null;
145
233
  }
146
- if (!providers) return { proxied: [], skipped: [] };
147
- const state = readProxyState();
148
- const proxied = [];
149
- const skipped = [];
150
- for (const [id, raw] of Object.entries(providers)) {
151
- if (id === "opencompress") continue;
152
- const provider = raw;
153
- if (provider.baseUrl?.includes("opencompress.ai")) continue;
154
- const pApi = provider.api || "openai-completions";
155
- if (pApi === "google-generative-ai") {
156
- skipped.push(`${id} (${pApi})`);
157
- continue;
158
- }
159
- state.originals[id] = {
160
- baseUrl: provider.baseUrl,
161
- apiKey: provider.apiKey,
162
- api: provider.api,
163
- headers: provider.headers ? { ...provider.headers } : void 0
234
+ }
235
+ async function directUpstream(upstream, body, originalHeaders) {
236
+ try {
237
+ const url = upstream.upstreamApi === "anthropic-messages" ? `${upstream.upstreamBaseUrl}/v1/messages` : `${upstream.upstreamBaseUrl}/v1/chat/completions`;
238
+ const headers = {
239
+ "Content-Type": "application/json"
164
240
  };
165
- if (pApi === "anthropic-messages") {
166
- provider.headers = {
167
- ...provider.headers || {},
168
- "x-upstream-key": provider.apiKey || ""
169
- };
170
- provider.baseUrl = `${baseUrl}/v1`;
171
- provider.apiKey = occKey;
241
+ if (upstream.upstreamApi === "anthropic-messages") {
242
+ headers["x-api-key"] = upstream.upstreamKey || "";
243
+ headers["anthropic-version"] = originalHeaders["anthropic-version"] || "2023-06-01";
172
244
  } else {
173
- provider.headers = {
174
- ...provider.headers || {},
175
- "X-Upstream-Key": provider.apiKey || "",
176
- "X-Upstream-Base-Url": provider.baseUrl
177
- };
178
- provider.baseUrl = `${baseUrl}/v1`;
179
- provider.apiKey = occKey;
245
+ headers["Authorization"] = `Bearer ${upstream.upstreamKey || ""}`;
180
246
  }
181
- persistProviderToDisk(id, provider);
182
- proxied.push(id);
247
+ return await fetch(url, {
248
+ method: "POST",
249
+ headers,
250
+ body: JSON.stringify(body)
251
+ });
252
+ } catch {
253
+ return null;
183
254
  }
184
- state.enabled = true;
185
- writeProxyState(state);
186
- return { proxied, skipped, source };
187
255
  }
188
- function disableProxy(api2) {
189
- const state = readProxyState();
190
- if (!state.enabled) return [];
191
- let providers = api2.config.models?.providers;
192
- if (!providers || Object.keys(providers).length === 0) {
193
- providers = readProvidersFromDisk();
194
- }
195
- if (!providers) return [];
196
- const restored = [];
197
- for (const [id, orig] of Object.entries(state.originals)) {
198
- const provider = providers[id];
199
- if (!provider) continue;
200
- provider.baseUrl = orig.baseUrl;
201
- provider.apiKey = orig.apiKey;
202
- provider.api = orig.api;
203
- if (orig.headers) {
204
- provider.headers = orig.headers;
205
- } else {
206
- const h = provider.headers;
207
- if (h) {
208
- delete h["X-Upstream-Key"];
209
- delete h["X-Upstream-Base-Url"];
210
- delete h["x-api-key"];
211
- delete h["x-upstream-key"];
212
- if (Object.keys(h).length === 0) delete provider.headers;
213
- }
214
- }
215
- persistProviderToDisk(id, provider);
216
- restored.push(id);
217
- }
218
- state.enabled = false;
219
- state.originals = {};
220
- writeProxyState(state);
221
- return restored;
256
+ function readBody(req) {
257
+ return new Promise((resolve, reject) => {
258
+ let data = "";
259
+ req.on("data", (chunk) => data += chunk);
260
+ req.on("end", () => resolve(data));
261
+ req.on("error", reject);
262
+ });
222
263
  }
223
- var opencompressProvider = {
224
- id: "opencompress",
225
- label: "OpenCompress",
226
- docsPath: "https://docs.opencompress.ai",
227
- aliases: ["oc", "compress"],
228
- envVars: ["OPENCOMPRESS_API_KEY"],
229
- // No models — we're a transparent proxy, not a router.
230
- // Users keep their existing providers; we just compress their traffic.
231
- models: {
232
- baseUrl: `${DEFAULT_BASE_URL}/v1`,
233
- api: "openai-completions",
234
- models: []
235
- },
236
- formatApiKey: (cred) => cred.apiKey || "",
237
- auth: [
238
- {
239
- id: "api-key",
240
- label: "OpenCompress",
241
- hint: "Compress all LLM calls automatically \u2014 save 40-60% on any provider",
242
- kind: "custom",
243
- run: async (ctx) => {
244
- ctx.prompter.note(
245
- "\u{1F99E} Welcome to OpenCompress!\n\nWe compress your LLM prompts automatically \u2014 saving 40-70% on token costs.\nWorks with Claude, GPT, and any provider you already have.\nYour API keys stay yours. We just make the traffic smaller.\n\nWe're giving you $1 free credit to try it out!"
246
- );
247
- const spinner = ctx.prompter.progress("Setting up your account...");
248
- try {
249
- const res = await fetch(`${DEFAULT_BASE_URL}/v1/provision`, {
250
- method: "POST",
251
- headers: { "Content-Type": "application/json" },
252
- body: JSON.stringify({})
253
- // BYOK — no upstream key needed here, user's existing providers handle that
254
- });
255
- if (!res.ok) {
256
- const err = await res.json().catch(() => ({ error: { message: "Unknown error" } }));
257
- spinner.stop("Setup failed");
258
- throw new Error(
259
- `Provisioning failed: ${err.error?.message || res.statusText}`
260
- );
261
- }
262
- const data = await res.json();
263
- spinner.stop("Account created!");
264
- persistApiKey(api, data.apiKey);
265
- const enableNow = await ctx.prompter.text({
266
- message: "Ready to compress? This will route your existing LLM providers through OpenCompress. Enable now? (yes/no)",
267
- validate: (v) => {
268
- const lower = v.toLowerCase().trim();
269
- if (!["yes", "no", "y", "n"].includes(lower)) return "Please answer yes or no";
270
- return void 0;
264
+
265
+ // src/index.ts
266
+ function getOccKey(api) {
267
+ const auth = api.config.auth;
268
+ const fromConfig = auth?.profiles?.opencompress?.credentials?.["api-key"]?.apiKey;
269
+ if (fromConfig) return fromConfig;
270
+ if (process.env.OPENCOMPRESS_API_KEY) return process.env.OPENCOMPRESS_API_KEY;
271
+ if (api.pluginConfig?.apiKey) return api.pluginConfig.apiKey;
272
+ return void 0;
273
+ }
274
+ function getProviders(api) {
275
+ return api.config.models?.providers || {};
276
+ }
277
+ function createProvider(api) {
278
+ return {
279
+ id: PROVIDER_ID,
280
+ label: "OpenCompress (Save Tokens + Improve Quality)",
281
+ aliases: ["oc", "compress"],
282
+ envVars: ["OPENCOMPRESS_API_KEY"],
283
+ // Dynamic model catalog — mirrors user's existing providers
284
+ // Detect primary provider's API type for compatibility
285
+ models: (() => {
286
+ const providers = getProviders(api);
287
+ const firstProvider = Object.values(providers).find((p) => p.api);
288
+ const primaryApi = firstProvider?.api || "openai-completions";
289
+ return {
290
+ baseUrl: `http://${PROXY_HOST}:${PROXY_PORT}/v1`,
291
+ api: primaryApi,
292
+ models: generateModelCatalog(providers)
293
+ };
294
+ })(),
295
+ auth: [
296
+ {
297
+ id: "api-key",
298
+ label: "OpenCompress",
299
+ hint: "Save tokens and improve quality on any LLM. Your API keys stay local.",
300
+ kind: "custom",
301
+ run: async (ctx) => {
302
+ ctx.prompter.note(
303
+ "\u{1F99E} OpenCompress \u2014 save tokens and improve quality on every LLM call\n\nUse your existing LLM providers. Your API keys stay on your machine.\nWe compress prompts to reduce costs and sharpen output quality."
304
+ );
305
+ const spinner = ctx.prompter.progress("Creating your account...");
306
+ try {
307
+ const res = await fetch(`${OCC_API}/v1/provision`, {
308
+ method: "POST",
309
+ headers: { "Content-Type": "application/json" },
310
+ body: JSON.stringify({})
311
+ });
312
+ if (!res.ok) {
313
+ spinner.stop("Failed");
314
+ throw new Error(`Provisioning failed: ${res.statusText}`);
271
315
  }
272
- });
273
- const shouldEnable = typeof enableNow === "string" && ["yes", "y"].includes(enableNow.toLowerCase().trim());
274
- const notes = [
275
- "\u{1F99E} OpenCompress is ready!",
276
- `\u{1F4B0} $1 free credit \u2014 no credit card needed.`,
277
- "",
278
- "How it works: your existing API keys (Claude, GPT, etc.) stay the same.",
279
- "We just compress the prompts in between \u2014 you save 40-70% on every call."
280
- ];
281
- if (shouldEnable) {
282
- notes.push(
283
- "",
284
- "\u2705 Compression is now active! All your LLM calls are being compressed.",
285
- "Run `/compress-stats` anytime to see how much you're saving."
286
- );
287
- } else {
288
- notes.push(
289
- "",
290
- "Run `/compress on` whenever you're ready to start saving."
291
- );
292
- }
293
- notes.push("", "Dashboard: https://www.opencompress.ai/dashboard");
294
- return {
295
- profiles: [
296
- {
316
+ const data = await res.json();
317
+ spinner.stop("Account created!");
318
+ return {
319
+ profiles: [{
297
320
  profileId: "default",
298
321
  credential: { apiKey: data.apiKey }
299
- }
300
- ],
301
- configPatch: shouldEnable ? { _autoEnableProxy: true } : void 0,
302
- notes
303
- };
304
- } catch (err) {
305
- spinner.stop("Setup failed");
306
- throw err instanceof Error ? err : new Error(String(err));
322
+ }],
323
+ notes: [
324
+ "\u{1F99E} OpenCompress ready!",
325
+ `\u{1F4B0} ${data.freeCredit} free credit.`,
326
+ "",
327
+ "Select any opencompress/* model to use compression.",
328
+ "Your existing provider keys are used automatically.",
329
+ "",
330
+ "Dashboard: https://www.opencompress.ai/dashboard"
331
+ ]
332
+ };
333
+ } catch (err) {
334
+ spinner.stop("Failed");
335
+ throw err instanceof Error ? err : new Error(String(err));
336
+ }
307
337
  }
308
338
  }
309
- }
310
- ]
311
- };
339
+ ]
340
+ };
341
+ }
312
342
  var plugin = {
313
343
  id: "opencompress",
314
344
  name: "OpenCompress",
315
- description: "Transparent prompt compression \u2014 save 40-70% on Claude, GPT, and any LLM provider",
345
+ description: "Save tokens and improve quality on any LLM \u2014 use your existing providers",
316
346
  version: VERSION,
317
- register(api2) {
318
- const baseUrl = api2.pluginConfig?.baseUrl || DEFAULT_BASE_URL;
319
- api2.registerProvider(opencompressProvider);
320
- const apiKey = getApiKey(api2);
321
- if (apiKey) {
322
- persistApiKey(api2, apiKey);
323
- }
324
- api2.logger.info("OpenCompress registered (transparent proxy mode)");
325
- api2.registerCommand({
347
+ register(api) {
348
+ api.registerProvider(createProvider(api));
349
+ api.logger.info(`OpenCompress v${VERSION} registered as provider`);
350
+ api.registerService({
351
+ id: "opencompress-proxy",
352
+ start: () => {
353
+ startProxy(
354
+ () => getProviders(api),
355
+ () => getOccKey(api)
356
+ );
357
+ api.logger.info(`Compression proxy started on ${PROXY_HOST}:${PROXY_PORT}`);
358
+ },
359
+ stop: () => {
360
+ stopProxy();
361
+ api.logger.info("Compression proxy stopped");
362
+ }
363
+ });
364
+ api.registerCommand({
326
365
  name: "compress-stats",
327
- description: "Show OpenCompress usage statistics and savings",
328
- acceptsArgs: true,
329
- requireAuth: false,
366
+ description: "Show compression savings and balance",
330
367
  handler: async () => {
331
- const apiKey2 = getApiKey(api2);
332
- if (!apiKey2) {
333
- return {
334
- text: "No API key found. Run `openclaw onboard opencompress` to set up."
335
- };
368
+ const occKey = getOccKey(api);
369
+ if (!occKey) {
370
+ return { text: "No API key. Run `openclaw onboard opencompress` first." };
336
371
  }
337
372
  try {
338
- const res = await fetch(`${baseUrl}/user/stats`, {
339
- headers: { Authorization: `Bearer ${apiKey2}` }
373
+ const res = await fetch(`${OCC_API}/user/stats`, {
374
+ headers: { Authorization: `Bearer ${occKey}` }
340
375
  });
341
- if (!res.ok) {
342
- return { text: `Failed to fetch stats: HTTP ${res.status}` };
343
- }
344
- const stats = await res.json();
345
- const balance = Number(stats.balanceUsd || 0);
346
- const calls = stats.monthlyApiCalls ?? stats.totalCalls ?? 0;
347
- const savings = Number(stats.monthlySavings || stats.totalSavings || 0).toFixed(4);
348
- const rate = stats.avgCompressionRate ? `${(Number(stats.avgCompressionRate) * 100).toFixed(1)}%` : "N/A";
349
- const origTokens = Number(stats.totalOriginalTokens || 0).toLocaleString();
350
- const compTokens = Number(stats.totalCompressedTokens || 0).toLocaleString();
351
- const lines = [
352
- "```",
353
- "\u{1F99E} OpenCompress Stats",
354
- "=====================",
355
- `Balance: $${balance.toFixed(2)}`,
356
- `API calls: ${calls}`,
357
- `Avg compression: ${rate}`,
358
- `Original tokens: ${origTokens}`,
359
- `Compressed tokens: ${compTokens}`,
360
- `Total savings: $${savings}`,
361
- "```"
362
- ];
363
- if (balance < 0.5) {
364
- const linkUrl = `https://www.opencompress.ai/dashboard?link=${encodeURIComponent(apiKey2)}`;
365
- lines.push(
366
- "",
367
- "\u26A0\uFE0F **Balance is low!** Link your account to get **$10 bonus credit**:",
368
- linkUrl
369
- );
370
- } else {
371
- lines.push("", "Dashboard: https://www.opencompress.ai/dashboard");
372
- }
373
- return { text: lines.join("\n") };
374
- } catch (err) {
375
- return {
376
- text: `Error fetching stats: ${err instanceof Error ? err.message : String(err)}`
377
- };
378
- }
379
- }
380
- });
381
- api2.logger.info("Registered /compress-stats command");
382
- api2.registerCommand({
383
- name: "compress",
384
- description: "Toggle transparent compression for all LLM providers",
385
- acceptsArgs: true,
386
- requireAuth: false,
387
- handler: async (ctx) => {
388
- const arg = ctx.args?.trim().toLowerCase();
389
- if (arg === "on" || arg === "enable") {
390
- const occKey2 = getApiKey(api2);
391
- if (!occKey2) {
392
- return { text: "No API key found. Run `openclaw onboard opencompress` first." };
393
- }
394
- const result = enableProxy(api2, baseUrl);
395
- if (result.proxied.length === 0 && result.skipped.length === 0) {
396
- return { text: "No providers found to proxy. Add LLM providers first." };
397
- }
398
- const lines = [
399
- `**Compression enabled** for all compatible providers (source: ${result.source}).`,
400
- ""
401
- ];
402
- if (result.proxied.length > 0) {
403
- lines.push(`Proxied (${result.proxied.length}): ${result.proxied.join(", ")}`);
404
- }
405
- if (result.skipped.length > 0) {
406
- lines.push(`Skipped (incompatible format): ${result.skipped.join(", ")}`);
407
- }
408
- lines.push("", "All requests now route through OpenCompress for automatic compression.");
409
- lines.push("To disable: `/compress off`");
410
- return { text: lines.join("\n") };
411
- }
412
- if (arg === "off" || arg === "disable") {
413
- const restored = disableProxy(api2);
414
- if (restored.length === 0) {
415
- return { text: "Compression proxy was not active." };
416
- }
376
+ if (!res.ok) return { text: `Failed: HTTP ${res.status}` };
377
+ const s = await res.json();
378
+ const balance = Number(s.balanceUsd || s.balance || 0);
379
+ const calls = s.monthlyApiCalls ?? s.totalCalls ?? 0;
380
+ const rate = s.avgCompressionRate ? `${(Number(s.avgCompressionRate) * 100).toFixed(1)}%` : "N/A";
417
381
  return {
418
382
  text: [
419
- "**Compression disabled.** Restored original provider configs.",
420
- "",
421
- `Restored: ${restored.join(", ")}`,
383
+ "```",
384
+ "\u{1F99E} OpenCompress Stats",
385
+ "=====================",
386
+ `Balance: $${balance.toFixed(2)}`,
387
+ `API calls: ${calls}`,
388
+ `Avg compression: ${rate}`,
389
+ `Tokens saved: ${(Number(s.totalOriginalTokens || 0) - Number(s.totalCompressedTokens || 0)).toLocaleString()}`,
390
+ "```",
422
391
  "",
423
- "To re-enable: `/compress on`"
392
+ balance < 0.5 ? `\u26A0\uFE0F Low balance! Link account for $10 bonus: https://www.opencompress.ai/dashboard?link=${encodeURIComponent(occKey)}` : "Dashboard: https://www.opencompress.ai/dashboard"
424
393
  ].join("\n")
425
394
  };
395
+ } catch (err) {
396
+ return { text: `Error: ${err instanceof Error ? err.message : String(err)}` };
426
397
  }
427
- const proxyState = readProxyState();
428
- const occKey = getApiKey(api2);
429
- const statusLines = [
430
- "**OpenCompress Transparent Proxy**",
431
- "",
432
- `Status: ${proxyState.enabled ? "**ON**" : "**OFF**"}`,
433
- `API key: ${occKey ? `${occKey.slice(0, 12)}...` : "not set"}`
434
- ];
435
- if (proxyState.enabled && Object.keys(proxyState.originals).length > 0) {
436
- statusLines.push(`Proxied providers: ${Object.keys(proxyState.originals).join(", ")}`);
437
- }
438
- statusLines.push(
439
- "",
440
- "**Usage:**",
441
- " `/compress on` \u2014 Route all providers through OpenCompress",
442
- " `/compress off` \u2014 Restore original provider configs",
443
- " `/compress-stats` \u2014 View compression savings"
444
- );
445
- return { text: statusLines.join("\n") };
446
398
  }
447
399
  });
448
- api2.logger.info("Registered /compress command");
449
- setTimeout(() => {
450
- const proxyState = readProxyState();
451
- const autoEnable = api2.pluginConfig?._autoEnableProxy;
452
- if (proxyState.enabled || autoEnable) {
453
- const result = enableProxy(api2, baseUrl);
454
- if (result.proxied.length > 0) {
455
- api2.logger.info(`Compression active: ${result.proxied.length} providers (${result.proxied.join(", ")})`);
456
- }
457
- }
458
- }, 2e3);
459
- const os = __require("os");
460
- const fs = __require("fs");
461
- const path = __require("path");
462
- const logDir = path.join(os.homedir(), ".openclaw", "opencompress-logs");
463
- try {
464
- fs.mkdirSync(logDir, { recursive: true });
465
- } catch {
466
- }
467
- api2.on("llm_input", (...args) => {
468
- try {
469
- const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
470
- const event = args[0] || {};
471
- const systemPromptLen = typeof event.systemPrompt === "string" ? event.systemPrompt.length : 0;
472
- const historyMessages = Array.isArray(event.historyMessages) ? event.historyMessages : [];
473
- const promptLen = typeof event.prompt === "string" ? event.prompt.length : 0;
474
- const systemTokens = Math.ceil(systemPromptLen / 4);
475
- const historyText = JSON.stringify(historyMessages);
476
- const historyTokens = Math.ceil(historyText.length / 4);
477
- const promptTokens = Math.ceil(promptLen / 4);
478
- const totalTokens = systemTokens + historyTokens + promptTokens;
479
- const entry = {
480
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
481
- _argCount: args.length,
482
- _eventKeys: Object.keys(event),
483
- runId: event.runId,
484
- sessionId: event.sessionId,
485
- provider: event.provider,
486
- model: event.model,
487
- imagesCount: event.imagesCount || 0,
488
- tokenEstimate: { systemPrompt: systemTokens, history: historyTokens, currentPrompt: promptTokens, total: totalTokens },
489
- charLengths: { systemPrompt: systemPromptLen, history: historyText.length, currentPrompt: promptLen },
490
- historyMessageCount: historyMessages.length,
491
- systemPrompt: event.systemPrompt,
492
- historyMessages: event.historyMessages,
493
- prompt: event.prompt
494
- };
495
- const model = typeof event.model === "string" ? event.model.replace(/\//g, "_") : "unknown";
496
- const filename = `${ts}_${model}.json`;
497
- fs.writeFileSync(path.join(logDir, filename), JSON.stringify(entry, null, 2) + "\n");
498
- } catch (err) {
499
- try {
500
- const errFile = path.join(logDir, `error_${Date.now()}.txt`);
501
- fs.writeFileSync(errFile, `${err}
502
- ${err.stack || ""}
503
- args: ${JSON.stringify(args?.map((a) => typeof a))}
504
- `);
505
- } catch {
506
- }
400
+ api.registerCommand({
401
+ name: "compress",
402
+ description: "Show OpenCompress status and available compressed models",
403
+ handler: async () => {
404
+ const occKey = getOccKey(api);
405
+ const providers = getProviders(api);
406
+ const models = generateModelCatalog(providers);
407
+ const lines = [
408
+ "**OpenCompress Status**",
409
+ "",
410
+ `API key: ${occKey ? `${occKey.slice(0, 12)}...` : "not set (run `openclaw onboard opencompress`)"}`,
411
+ `Proxy: http://${PROXY_HOST}:${PROXY_PORT}`,
412
+ "",
413
+ "**Available compressed models:**",
414
+ ...models.map((m) => ` ${m.id}`),
415
+ "",
416
+ "Select any of these models to use compression."
417
+ ];
418
+ return { text: lines.join("\n") };
507
419
  }
508
420
  });
509
- api2.logger.info(`LLM input logging enabled \u2192 ${logDir}`);
510
421
  }
511
422
  };
512
423
  var index_default = plugin;
@@ -1,20 +1,19 @@
1
1
  {
2
2
  "id": "opencompress",
3
3
  "name": "OpenCompress",
4
- "description": "5-layer prompt compression 53% input reduction, 62% latency cut, 96% quality. Save 40-70% on any LLM.",
4
+ "description": "Save tokens and improve quality on any LLM. Use your existing providers keys stay local.",
5
+ "providers": ["opencompress"],
5
6
  "configSchema": {
6
7
  "type": "object",
7
8
  "properties": {
8
9
  "apiKey": {
9
10
  "type": "string",
10
11
  "description": "OpenCompress API key (sk-occ-...)"
11
- },
12
- "baseUrl": {
13
- "type": "string",
14
- "default": "https://www.opencompress.ai/api",
15
- "description": "OpenCompress API base URL"
16
12
  }
17
13
  },
18
14
  "required": []
15
+ },
16
+ "providerAuthEnvVars": {
17
+ "opencompress": ["OPENCOMPRESS_API_KEY"]
19
18
  }
20
19
  }
@@ -1,14 +1,10 @@
1
1
  {
2
2
  "id": "opencompress",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "securityProfile": {
5
5
  "dataHandling": {
6
- "promptContent": "pass-through",
7
- "promptStorage": "none",
8
- "responseContent": "pass-through",
9
- "responseStorage": "none",
10
- "upstreamApiKeys": "local-only",
11
- "upstreamApiKeyStorage": "never-server-side"
6
+ "promptContent": "pass-through (compressed in-transit, never stored)",
7
+ "upstreamApiKeys": "local-only (read from api.config, sent per-request as header, never persisted)"
12
8
  },
13
9
  "networkAccess": {
14
10
  "outbound": [
@@ -16,57 +12,26 @@
16
12
  "host": "www.opencompress.ai",
17
13
  "port": 443,
18
14
  "protocol": "https",
19
- "purpose": "API proxy — compress prompts and forward to upstream LLM provider"
20
- }
21
- ],
22
- "inbound": []
23
- },
24
- "localStorage": {
25
- "authProfiles": {
26
- "path": "~/.openclaw/agents/*/agent/auth-profiles.json",
27
- "content": "OpenCompress API key (sk-occ-*)",
28
- "sensitive": true
29
- },
30
- "providerConfig": {
31
- "path": "~/.openclaw/openclaw.json",
32
- "content": "Provider config, optional upstream key in headers (BYOK pass-through)",
33
- "sensitive": true
34
- }
35
- },
36
- "secrets": {
37
- "required": [
15
+ "purpose": "Compress prompts and forward to user's upstream LLM provider"
16
+ },
38
17
  {
39
- "name": "OpenCompress API Key",
40
- "format": "sk-occ-*",
41
- "purpose": "Authentication and billing for compression service",
42
- "serverSideStorage": "hashed (SHA-256), never plaintext"
43
- }
44
- ],
45
- "optional": [
46
- {
47
- "name": "Upstream LLM API Key",
48
- "format": "sk-proj-* / sk-ant-* / sk-or-* / AIza*",
49
- "purpose": "BYOK mode — forwarded to user's LLM provider",
50
- "serverSideStorage": "NEVER stored. Passed via X-Upstream-Key header per-request, discarded after forwarding."
18
+ "host": "127.0.0.1",
19
+ "port": 8401,
20
+ "protocol": "http",
21
+ "purpose": "Local proxy — receives requests from OpenClaw, routes to opencompress.ai"
51
22
  }
52
23
  ]
53
24
  },
54
25
  "permissions": {
55
- "fileSystem": "read/write ~/.openclaw/ config files only",
56
- "environment": ["OPENCOMPRESS_API_KEY", "OPENCOMPRESS_LLM_KEY"],
26
+ "fileSystem": "none (all config via api.config runtime API)",
27
+ "environment": ["OPENCOMPRESS_API_KEY"],
57
28
  "shell": "none",
58
- "network": "outbound HTTPS to www.opencompress.ai only"
29
+ "network": "outbound HTTPS to www.opencompress.ai + local HTTP proxy on 127.0.0.1:8401"
59
30
  }
60
31
  },
61
32
  "privacyPolicy": {
62
- "promptsAndResponses": "OpenCompress compresses prompts in-memory and forwards to the upstream LLM provider. Prompt and response content is NEVER stored, logged, or used for training. Only token counts are recorded for billing.",
63
- "apiKeys": "Your upstream LLM API key (OpenAI, Anthropic, etc.) is stored ONLY on your local machine. It is sent to our server as a per-request header (X-Upstream-Key) and discarded immediately after forwarding. We never persist your upstream keys.",
64
- "billing": "We record: timestamp, model, original token count, compressed token count, cost. We do NOT record prompt content.",
65
- "thirdParties": "We do not share any data with third parties. Your requests are forwarded only to the LLM provider you specify."
66
- },
67
- "auditability": {
68
- "pluginSource": "Open source — https://github.com/claw-compactor/openclaw-plugin",
69
- "serverSource": "Forwarding logic documented in security docs. Core proxy is open for audit.",
70
- "verification": "Users can inspect all network requests via OpenClaw's diagnostics plugin or standard proxy tools."
33
+ "prompts": "Compressed in-memory and forwarded. NEVER stored or logged.",
34
+ "upstreamKeys": "Read from OpenClaw runtime config (api.config). Sent per-request via x-upstream-key header. Never persisted server-side.",
35
+ "billing": "Only token counts recorded (original, compressed, model). No prompt content."
71
36
  }
72
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencompress/opencompress",
3
- "version": "1.9.2",
3
+ "version": "2.0.1",
4
4
  "description": "OpenCompress plugin for OpenClaw — automatic 5-layer prompt compression for any LLM",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",