@opencompress/opencompress 1.9.1 → 2.0.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/dist/index.d.ts +43 -103
- package/dist/index.js +364 -518
- package/openclaw.plugin.json +5 -6
- package/openclaw.security.json +15 -50
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,144 +1,84 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* OpenCompress Plugin for OpenClaw v2
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
7
|
*/
|
|
8
8
|
type ModelApi = "openai-completions" | "openai-responses" | "anthropic-messages" | "google-generative-ai";
|
|
9
9
|
type ModelDefinitionConfig = {
|
|
10
10
|
id: string;
|
|
11
11
|
name: string;
|
|
12
12
|
api?: ModelApi;
|
|
13
|
-
reasoning
|
|
14
|
-
input
|
|
15
|
-
cost
|
|
13
|
+
reasoning?: boolean;
|
|
14
|
+
input?: string[];
|
|
15
|
+
cost?: {
|
|
16
16
|
input: number;
|
|
17
17
|
output: number;
|
|
18
18
|
cacheRead: number;
|
|
19
19
|
cacheWrite: number;
|
|
20
20
|
};
|
|
21
|
-
contextWindow
|
|
22
|
-
maxTokens
|
|
23
|
-
|
|
21
|
+
contextWindow?: number;
|
|
22
|
+
maxTokens?: number;
|
|
23
|
+
[key: string]: unknown;
|
|
24
24
|
};
|
|
25
25
|
type ModelProviderConfig = {
|
|
26
26
|
baseUrl: string;
|
|
27
27
|
apiKey?: string;
|
|
28
28
|
api?: ModelApi;
|
|
29
|
-
headers?: Record<string, string>;
|
|
30
|
-
authHeader?: boolean;
|
|
31
29
|
models: ModelDefinitionConfig[];
|
|
32
30
|
[key: string]: unknown;
|
|
33
31
|
};
|
|
34
|
-
type AuthProfileCredential = {
|
|
35
|
-
apiKey?: string;
|
|
36
|
-
type?: string;
|
|
37
|
-
[key: string]: unknown;
|
|
38
|
-
};
|
|
39
|
-
type ProviderAuthResult = {
|
|
40
|
-
profiles: Array<{
|
|
41
|
-
profileId: string;
|
|
42
|
-
credential: AuthProfileCredential;
|
|
43
|
-
}>;
|
|
44
|
-
configPatch?: Record<string, unknown>;
|
|
45
|
-
defaultModel?: string;
|
|
46
|
-
notes?: string[];
|
|
47
|
-
};
|
|
48
|
-
type WizardPrompter = {
|
|
49
|
-
text: (opts: {
|
|
50
|
-
message: string;
|
|
51
|
-
validate?: (value: string) => string | undefined;
|
|
52
|
-
}) => Promise<string | symbol>;
|
|
53
|
-
note: (message: string) => void;
|
|
54
|
-
progress: (message: string) => {
|
|
55
|
-
stop: (message?: string) => void;
|
|
56
|
-
};
|
|
57
|
-
};
|
|
58
|
-
type ProviderAuthContext = {
|
|
59
|
-
config: Record<string, unknown>;
|
|
60
|
-
agentDir?: string;
|
|
61
|
-
workspaceDir?: string;
|
|
62
|
-
prompter: WizardPrompter;
|
|
63
|
-
runtime: {
|
|
64
|
-
log: (message: string) => void;
|
|
65
|
-
};
|
|
66
|
-
isRemote: boolean;
|
|
67
|
-
openUrl: (url: string) => Promise<void>;
|
|
68
|
-
};
|
|
69
|
-
type ProviderAuthMethod = {
|
|
70
|
-
id: string;
|
|
71
|
-
label: string;
|
|
72
|
-
hint?: string;
|
|
73
|
-
kind: "oauth" | "api_key" | "token" | "device_code" | "custom";
|
|
74
|
-
run: (ctx: ProviderAuthContext) => Promise<ProviderAuthResult>;
|
|
75
|
-
};
|
|
76
32
|
type ProviderPlugin = {
|
|
77
33
|
id: string;
|
|
78
34
|
label: string;
|
|
79
|
-
docsPath?: string;
|
|
80
35
|
aliases?: string[];
|
|
81
36
|
envVars?: string[];
|
|
82
37
|
models?: ModelProviderConfig;
|
|
83
|
-
auth:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
warn: (message: string) => void;
|
|
90
|
-
error: (message: string) => void;
|
|
91
|
-
};
|
|
92
|
-
type OpenClawPluginService = {
|
|
93
|
-
id: string;
|
|
94
|
-
start: () => void | Promise<void>;
|
|
95
|
-
stop?: () => void | Promise<void>;
|
|
96
|
-
};
|
|
97
|
-
type CommandHandler = {
|
|
98
|
-
name: string;
|
|
99
|
-
description: string;
|
|
100
|
-
acceptsArgs?: boolean;
|
|
101
|
-
requireAuth?: boolean;
|
|
102
|
-
handler: (ctx: {
|
|
103
|
-
args?: string;
|
|
104
|
-
}) => Promise<{
|
|
105
|
-
text: string;
|
|
38
|
+
auth: Array<{
|
|
39
|
+
id: string;
|
|
40
|
+
label: string;
|
|
41
|
+
hint?: string;
|
|
42
|
+
kind: string;
|
|
43
|
+
run: (ctx: any) => Promise<any>;
|
|
106
44
|
}>;
|
|
107
45
|
};
|
|
108
46
|
type OpenClawPluginApi = {
|
|
109
47
|
id: string;
|
|
110
48
|
name: string;
|
|
111
49
|
version?: string;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
agents?: Record<string, unknown>;
|
|
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;
|
|
119
56
|
};
|
|
120
|
-
pluginConfig?: Record<string, unknown>;
|
|
121
|
-
logger: PluginLogger;
|
|
122
57
|
registerProvider: (provider: ProviderPlugin) => void;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
}>;
|
|
128
72
|
}) => void;
|
|
129
|
-
registerService: (service: OpenClawPluginService) => void;
|
|
130
|
-
registerCommand: (command: CommandHandler) => void;
|
|
131
73
|
resolvePath: (input: string) => string;
|
|
132
|
-
on: (hookName: string, handler: unknown
|
|
74
|
+
on: (hookName: string, handler: unknown) => void;
|
|
133
75
|
};
|
|
134
|
-
|
|
135
|
-
id
|
|
136
|
-
name
|
|
137
|
-
description
|
|
138
|
-
version
|
|
139
|
-
register
|
|
140
|
-
activate?: (api: OpenClawPluginApi) => void | Promise<void>;
|
|
76
|
+
declare const plugin: {
|
|
77
|
+
id: string;
|
|
78
|
+
name: string;
|
|
79
|
+
description: string;
|
|
80
|
+
version: string;
|
|
81
|
+
register(api: OpenClawPluginApi): void;
|
|
141
82
|
};
|
|
142
|
-
declare const plugin: OpenClawPluginDefinition;
|
|
143
83
|
|
|
144
84
|
export { plugin as default };
|
package/dist/index.js
CHANGED
|
@@ -1,571 +1,417 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var VERSION = "2.0.0";
|
|
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/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
for (const agent of agentDirs) {
|
|
24
|
-
const authPath = path.join(agentsDir, agent, "agent", "auth-profiles.json");
|
|
25
|
-
if (!fs.existsSync(authPath)) continue;
|
|
26
|
-
try {
|
|
27
|
-
const profiles = JSON.parse(fs.readFileSync(authPath, "utf-8"));
|
|
28
|
-
const ocProfile = profiles?.profiles?.["opencompress:default"];
|
|
29
|
-
if (ocProfile?.key) return ocProfile.key;
|
|
30
|
-
} catch {
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
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
|
+
};
|
|
33
23
|
}
|
|
34
|
-
|
|
24
|
+
return null;
|
|
35
25
|
}
|
|
36
|
-
|
|
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
|
+
};
|
|
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
|
+
};
|
|
37
49
|
}
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
}
|
|
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;
|
|
74
|
+
}
|
|
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;
|
|
86
|
+
}
|
|
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;
|
|
51
118
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
55
123
|
};
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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;
|
|
60
136
|
}
|
|
61
137
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const agentsDir = path.join(os.homedir(), ".openclaw", "agents");
|
|
78
|
-
if (!fs.existsSync(agentsDir)) return;
|
|
79
|
-
const agentDirs = fs.readdirSync(agentsDir);
|
|
80
|
-
for (const agent of agentDirs) {
|
|
81
|
-
const authPath = path.join(agentsDir, agent, "agent", "auth.json");
|
|
82
|
-
const authDir = path.dirname(authPath);
|
|
83
|
-
if (!fs.existsSync(authDir)) continue;
|
|
84
|
-
let data = {};
|
|
85
|
-
if (fs.existsSync(authPath)) {
|
|
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);
|
|
86
153
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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();
|
|
90
190
|
}
|
|
91
|
-
}
|
|
92
|
-
data.opencompress = {
|
|
93
|
-
type: "api_key",
|
|
94
|
-
key: apiKey
|
|
95
|
-
};
|
|
96
|
-
fs.writeFileSync(authPath, JSON.stringify(data, null, 2) + "\n");
|
|
97
|
-
}
|
|
98
|
-
} catch {
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
function proxyStatePath() {
|
|
102
|
-
const os = __require("os");
|
|
103
|
-
const path = __require("path");
|
|
104
|
-
return path.join(os.homedir(), ".openclaw", "opencompress-proxy.json");
|
|
105
|
-
}
|
|
106
|
-
function readProxyState() {
|
|
107
|
-
try {
|
|
108
|
-
const fs = __require("fs");
|
|
109
|
-
const p = proxyStatePath();
|
|
110
|
-
if (!fs.existsSync(p)) return { enabled: false, originals: {} };
|
|
111
|
-
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
112
|
-
} catch {
|
|
113
|
-
return { enabled: false, originals: {} };
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
function writeProxyState(state) {
|
|
117
|
-
try {
|
|
118
|
-
const fs = __require("fs");
|
|
119
|
-
fs.writeFileSync(proxyStatePath(), JSON.stringify(state, null, 2) + "\n");
|
|
120
|
-
} catch {
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
function readProvidersFromDisk() {
|
|
124
|
-
try {
|
|
125
|
-
const os = __require("os");
|
|
126
|
-
const fs = __require("fs");
|
|
127
|
-
const path = __require("path");
|
|
128
|
-
const configPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
129
|
-
if (!fs.existsSync(configPath)) return void 0;
|
|
130
|
-
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
131
|
-
const providers = raw?.models?.providers;
|
|
132
|
-
if (!providers || Object.keys(providers).length === 0) return void 0;
|
|
133
|
-
return providers;
|
|
134
|
-
} catch {
|
|
135
|
-
return void 0;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
function persistProviderToDisk(providerId, config) {
|
|
139
|
-
const os = __require("os");
|
|
140
|
-
const fs = __require("fs");
|
|
141
|
-
const path = __require("path");
|
|
142
|
-
try {
|
|
143
|
-
const configPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
144
|
-
if (fs.existsSync(configPath)) {
|
|
145
|
-
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
146
|
-
if (!raw.models) raw.models = {};
|
|
147
|
-
if (!raw.models.providers) raw.models.providers = {};
|
|
148
|
-
raw.models.providers[providerId] = {
|
|
149
|
-
baseUrl: config.baseUrl,
|
|
150
|
-
api: config.api || "openai-completions",
|
|
151
|
-
apiKey: config.apiKey || void 0,
|
|
152
|
-
models: config.models?.map((m) => ({ id: m.id, name: m.name })) || [],
|
|
153
|
-
...config.headers ? { headers: config.headers } : {}
|
|
154
|
-
};
|
|
155
|
-
fs.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
156
|
-
}
|
|
157
|
-
} catch {
|
|
158
|
-
}
|
|
159
|
-
try {
|
|
160
|
-
const agentsDir = path.join(os.homedir(), ".openclaw", "agents");
|
|
161
|
-
if (!fs.existsSync(agentsDir)) return;
|
|
162
|
-
for (const agent of fs.readdirSync(agentsDir)) {
|
|
163
|
-
const modelsPath = path.join(agentsDir, agent, "agent", "models.json");
|
|
164
|
-
if (!fs.existsSync(path.dirname(modelsPath))) continue;
|
|
165
|
-
let data = { providers: {} };
|
|
166
|
-
if (fs.existsSync(modelsPath)) {
|
|
191
|
+
} else {
|
|
167
192
|
try {
|
|
168
|
-
|
|
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);
|
|
169
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);
|
|
170
213
|
}
|
|
171
|
-
if (!data.providers) data.providers = {};
|
|
172
214
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
apiKey: config.apiKey || void 0,
|
|
177
|
-
models: config.models?.map((m) => ({
|
|
178
|
-
id: m.id,
|
|
179
|
-
name: m.name,
|
|
180
|
-
api: m.api || config.api || "openai-completions",
|
|
181
|
-
reasoning: m.reasoning ?? false,
|
|
182
|
-
input: m.input || ["text"],
|
|
183
|
-
cost: m.cost || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
184
|
-
contextWindow: m.contextWindow || 2e5,
|
|
185
|
-
maxTokens: m.maxTokens || 8192
|
|
186
|
-
})) || [],
|
|
187
|
-
...config.headers ? { headers: config.headers } : {}
|
|
188
|
-
};
|
|
189
|
-
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) } }));
|
|
190
218
|
}
|
|
191
|
-
}
|
|
192
|
-
|
|
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;
|
|
193
228
|
}
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
let source = "api.config";
|
|
199
|
-
if (!providers || Object.keys(providers).length === 0) {
|
|
200
|
-
providers = readProvidersFromDisk();
|
|
201
|
-
source = "disk (openclaw.json)";
|
|
229
|
+
function stopProxy() {
|
|
230
|
+
if (server) {
|
|
231
|
+
server.close();
|
|
232
|
+
server = null;
|
|
202
233
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const provider = raw;
|
|
210
|
-
if (provider.baseUrl?.includes("opencompress.ai")) continue;
|
|
211
|
-
const pApi = provider.api || "openai-completions";
|
|
212
|
-
if (pApi === "google-generative-ai") {
|
|
213
|
-
skipped.push(`${id} (${pApi})`);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
state.originals[id] = {
|
|
217
|
-
baseUrl: provider.baseUrl,
|
|
218
|
-
apiKey: provider.apiKey,
|
|
219
|
-
api: provider.api,
|
|
220
|
-
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"
|
|
221
240
|
};
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"x-upstream-key": provider.apiKey || ""
|
|
226
|
-
};
|
|
227
|
-
provider.baseUrl = `${baseUrl}/v1`;
|
|
228
|
-
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";
|
|
229
244
|
} else {
|
|
230
|
-
|
|
231
|
-
...provider.headers || {},
|
|
232
|
-
"X-Upstream-Key": provider.apiKey || "",
|
|
233
|
-
"X-Upstream-Base-Url": provider.baseUrl
|
|
234
|
-
};
|
|
235
|
-
provider.baseUrl = `${baseUrl}/v1`;
|
|
236
|
-
provider.apiKey = occKey;
|
|
245
|
+
headers["Authorization"] = `Bearer ${upstream.upstreamKey || ""}`;
|
|
237
246
|
}
|
|
238
|
-
|
|
239
|
-
|
|
247
|
+
return await fetch(url, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers,
|
|
250
|
+
body: JSON.stringify(body)
|
|
251
|
+
});
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
240
254
|
}
|
|
241
|
-
state.enabled = true;
|
|
242
|
-
writeProxyState(state);
|
|
243
|
-
return { proxied, skipped, source };
|
|
244
255
|
}
|
|
245
|
-
function
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
if (!providers) return [];
|
|
253
|
-
const restored = [];
|
|
254
|
-
for (const [id, orig] of Object.entries(state.originals)) {
|
|
255
|
-
const provider = providers[id];
|
|
256
|
-
if (!provider) continue;
|
|
257
|
-
provider.baseUrl = orig.baseUrl;
|
|
258
|
-
provider.apiKey = orig.apiKey;
|
|
259
|
-
provider.api = orig.api;
|
|
260
|
-
if (orig.headers) {
|
|
261
|
-
provider.headers = orig.headers;
|
|
262
|
-
} else {
|
|
263
|
-
const h = provider.headers;
|
|
264
|
-
if (h) {
|
|
265
|
-
delete h["X-Upstream-Key"];
|
|
266
|
-
delete h["X-Upstream-Base-Url"];
|
|
267
|
-
delete h["x-api-key"];
|
|
268
|
-
delete h["x-upstream-key"];
|
|
269
|
-
if (Object.keys(h).length === 0) delete provider.headers;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
persistProviderToDisk(id, provider);
|
|
273
|
-
restored.push(id);
|
|
274
|
-
}
|
|
275
|
-
state.enabled = false;
|
|
276
|
-
state.originals = {};
|
|
277
|
-
writeProxyState(state);
|
|
278
|
-
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
|
+
});
|
|
279
263
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
validate: (v) => {
|
|
326
|
-
const lower = v.toLowerCase().trim();
|
|
327
|
-
if (!["yes", "no", "y", "n"].includes(lower)) return "Please answer yes or no";
|
|
328
|
-
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 (BYOK Compression Proxy)",
|
|
281
|
+
aliases: ["oc", "compress"],
|
|
282
|
+
envVars: ["OPENCOMPRESS_API_KEY"],
|
|
283
|
+
// Dynamic model catalog — mirrors user's existing providers
|
|
284
|
+
models: {
|
|
285
|
+
baseUrl: `http://${PROXY_HOST}:${PROXY_PORT}/v1`,
|
|
286
|
+
api: "openai-completions",
|
|
287
|
+
models: generateModelCatalog(getProviders(api))
|
|
288
|
+
},
|
|
289
|
+
auth: [
|
|
290
|
+
{
|
|
291
|
+
id: "api-key",
|
|
292
|
+
label: "OpenCompress",
|
|
293
|
+
hint: "Save 20-40% on any LLM. Your API keys stay local.",
|
|
294
|
+
kind: "custom",
|
|
295
|
+
run: async (ctx) => {
|
|
296
|
+
ctx.prompter.note(
|
|
297
|
+
"\u{1F99E} OpenCompress \u2014 compress every LLM call, save 20-40%\n\nYour existing API keys (Claude, GPT, etc.) stay on your machine.\nWe just compress the prompts before they're sent."
|
|
298
|
+
);
|
|
299
|
+
const spinner = ctx.prompter.progress("Creating your account...");
|
|
300
|
+
try {
|
|
301
|
+
const res = await fetch(`${OCC_API}/v1/provision`, {
|
|
302
|
+
method: "POST",
|
|
303
|
+
headers: { "Content-Type": "application/json" },
|
|
304
|
+
body: JSON.stringify({})
|
|
305
|
+
});
|
|
306
|
+
if (!res.ok) {
|
|
307
|
+
spinner.stop("Failed");
|
|
308
|
+
throw new Error(`Provisioning failed: ${res.statusText}`);
|
|
329
309
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
`\u{1F4B0} $1 free credit \u2014 no credit card needed.`,
|
|
335
|
-
"",
|
|
336
|
-
"How it works: your existing API keys (Claude, GPT, etc.) stay the same.",
|
|
337
|
-
"We just compress the prompts in between \u2014 you save 40-70% on every call."
|
|
338
|
-
];
|
|
339
|
-
if (shouldEnable) {
|
|
340
|
-
notes.push(
|
|
341
|
-
"",
|
|
342
|
-
"\u2705 Compression is now active! All your LLM calls are being compressed.",
|
|
343
|
-
"Run `/compress-stats` anytime to see how much you're saving."
|
|
344
|
-
);
|
|
345
|
-
} else {
|
|
346
|
-
notes.push(
|
|
347
|
-
"",
|
|
348
|
-
"Run `/compress on` whenever you're ready to start saving."
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
notes.push("", "Dashboard: https://www.opencompress.ai/dashboard");
|
|
352
|
-
return {
|
|
353
|
-
profiles: [
|
|
354
|
-
{
|
|
310
|
+
const data = await res.json();
|
|
311
|
+
spinner.stop("Account created!");
|
|
312
|
+
return {
|
|
313
|
+
profiles: [{
|
|
355
314
|
profileId: "default",
|
|
356
315
|
credential: { apiKey: data.apiKey }
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
316
|
+
}],
|
|
317
|
+
notes: [
|
|
318
|
+
"\u{1F99E} OpenCompress ready!",
|
|
319
|
+
`\u{1F4B0} ${data.freeCredit} free credit.`,
|
|
320
|
+
"",
|
|
321
|
+
"Select any opencompress/* model to use compression.",
|
|
322
|
+
"Your existing provider keys are used automatically.",
|
|
323
|
+
"",
|
|
324
|
+
"Dashboard: https://www.opencompress.ai/dashboard"
|
|
325
|
+
]
|
|
326
|
+
};
|
|
327
|
+
} catch (err) {
|
|
328
|
+
spinner.stop("Failed");
|
|
329
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
330
|
+
}
|
|
365
331
|
}
|
|
366
332
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
333
|
+
]
|
|
334
|
+
};
|
|
335
|
+
}
|
|
370
336
|
var plugin = {
|
|
371
337
|
id: "opencompress",
|
|
372
338
|
name: "OpenCompress",
|
|
373
|
-
description: "
|
|
339
|
+
description: "BYOK prompt compression \u2014 save 20-40% on any LLM provider",
|
|
374
340
|
version: VERSION,
|
|
375
341
|
register(api) {
|
|
376
|
-
|
|
377
|
-
api.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
342
|
+
api.registerProvider(createProvider(api));
|
|
343
|
+
api.logger.info(`OpenCompress v${VERSION} registered as provider`);
|
|
344
|
+
api.registerService({
|
|
345
|
+
id: "opencompress-proxy",
|
|
346
|
+
start: () => {
|
|
347
|
+
startProxy(
|
|
348
|
+
() => getProviders(api),
|
|
349
|
+
() => getOccKey(api)
|
|
350
|
+
);
|
|
351
|
+
api.logger.info(`Compression proxy started on ${PROXY_HOST}:${PROXY_PORT}`);
|
|
352
|
+
},
|
|
353
|
+
stop: () => {
|
|
354
|
+
stopProxy();
|
|
355
|
+
api.logger.info("Compression proxy stopped");
|
|
356
|
+
}
|
|
357
|
+
});
|
|
384
358
|
api.registerCommand({
|
|
385
359
|
name: "compress-stats",
|
|
386
|
-
description: "Show
|
|
387
|
-
acceptsArgs: true,
|
|
388
|
-
requireAuth: false,
|
|
360
|
+
description: "Show compression savings and balance",
|
|
389
361
|
handler: async () => {
|
|
390
|
-
const
|
|
391
|
-
if (!
|
|
392
|
-
return {
|
|
393
|
-
text: "No API key found. Run `openclaw onboard opencompress` to set up."
|
|
394
|
-
};
|
|
362
|
+
const occKey = getOccKey(api);
|
|
363
|
+
if (!occKey) {
|
|
364
|
+
return { text: "No API key. Run `openclaw onboard opencompress` first." };
|
|
395
365
|
}
|
|
396
366
|
try {
|
|
397
|
-
const res = await fetch(`${
|
|
398
|
-
headers: { Authorization: `Bearer ${
|
|
367
|
+
const res = await fetch(`${OCC_API}/user/stats`, {
|
|
368
|
+
headers: { Authorization: `Bearer ${occKey}` }
|
|
399
369
|
});
|
|
400
|
-
if (!res.ok) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
const calls = stats.monthlyApiCalls ?? stats.totalCalls ?? 0;
|
|
406
|
-
const savings = Number(stats.monthlySavings || stats.totalSavings || 0).toFixed(4);
|
|
407
|
-
const rate = stats.avgCompressionRate ? `${(Number(stats.avgCompressionRate) * 100).toFixed(1)}%` : "N/A";
|
|
408
|
-
const origTokens = Number(stats.totalOriginalTokens || 0).toLocaleString();
|
|
409
|
-
const compTokens = Number(stats.totalCompressedTokens || 0).toLocaleString();
|
|
410
|
-
const lines = [
|
|
411
|
-
"```",
|
|
412
|
-
"\u{1F99E} OpenCompress Stats",
|
|
413
|
-
"=====================",
|
|
414
|
-
`Balance: $${balance.toFixed(2)}`,
|
|
415
|
-
`API calls: ${calls}`,
|
|
416
|
-
`Avg compression: ${rate}`,
|
|
417
|
-
`Original tokens: ${origTokens}`,
|
|
418
|
-
`Compressed tokens: ${compTokens}`,
|
|
419
|
-
`Total savings: $${savings}`,
|
|
420
|
-
"```"
|
|
421
|
-
];
|
|
422
|
-
if (balance < 0.5) {
|
|
423
|
-
const linkUrl = `https://www.opencompress.ai/dashboard?link=${encodeURIComponent(apiKey2)}`;
|
|
424
|
-
lines.push(
|
|
425
|
-
"",
|
|
426
|
-
"\u26A0\uFE0F **Balance is low!** Link your account to get **$10 bonus credit**:",
|
|
427
|
-
linkUrl
|
|
428
|
-
);
|
|
429
|
-
} else {
|
|
430
|
-
lines.push("", "Dashboard: https://www.opencompress.ai/dashboard");
|
|
431
|
-
}
|
|
432
|
-
return { text: lines.join("\n") };
|
|
433
|
-
} catch (err) {
|
|
370
|
+
if (!res.ok) return { text: `Failed: HTTP ${res.status}` };
|
|
371
|
+
const s = await res.json();
|
|
372
|
+
const balance = Number(s.balanceUsd || s.balance || 0);
|
|
373
|
+
const calls = s.monthlyApiCalls ?? s.totalCalls ?? 0;
|
|
374
|
+
const rate = s.avgCompressionRate ? `${(Number(s.avgCompressionRate) * 100).toFixed(1)}%` : "N/A";
|
|
434
375
|
return {
|
|
435
|
-
text:
|
|
376
|
+
text: [
|
|
377
|
+
"```",
|
|
378
|
+
"\u{1F99E} OpenCompress Stats",
|
|
379
|
+
"=====================",
|
|
380
|
+
`Balance: $${balance.toFixed(2)}`,
|
|
381
|
+
`API calls: ${calls}`,
|
|
382
|
+
`Avg compression: ${rate}`,
|
|
383
|
+
`Tokens saved: ${(Number(s.totalOriginalTokens || 0) - Number(s.totalCompressedTokens || 0)).toLocaleString()}`,
|
|
384
|
+
"```",
|
|
385
|
+
"",
|
|
386
|
+
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"
|
|
387
|
+
].join("\n")
|
|
436
388
|
};
|
|
389
|
+
} catch (err) {
|
|
390
|
+
return { text: `Error: ${err instanceof Error ? err.message : String(err)}` };
|
|
437
391
|
}
|
|
438
392
|
}
|
|
439
393
|
});
|
|
440
|
-
api.logger.info("Registered /compress-stats command");
|
|
441
394
|
api.registerCommand({
|
|
442
395
|
name: "compress",
|
|
443
|
-
description: "
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (!occKey2) {
|
|
451
|
-
return { text: "No API key found. Run `openclaw onboard opencompress` first." };
|
|
452
|
-
}
|
|
453
|
-
const result = enableProxy(api, baseUrl);
|
|
454
|
-
if (result.proxied.length === 0 && result.skipped.length === 0) {
|
|
455
|
-
return { text: "No providers found to proxy. Add LLM providers first." };
|
|
456
|
-
}
|
|
457
|
-
const lines = [
|
|
458
|
-
`**Compression enabled** for all compatible providers (source: ${result.source}).`,
|
|
459
|
-
""
|
|
460
|
-
];
|
|
461
|
-
if (result.proxied.length > 0) {
|
|
462
|
-
lines.push(`Proxied (${result.proxied.length}): ${result.proxied.join(", ")}`);
|
|
463
|
-
}
|
|
464
|
-
if (result.skipped.length > 0) {
|
|
465
|
-
lines.push(`Skipped (incompatible format): ${result.skipped.join(", ")}`);
|
|
466
|
-
}
|
|
467
|
-
lines.push("", "All requests now route through OpenCompress for automatic compression.");
|
|
468
|
-
lines.push("To disable: `/compress off`");
|
|
469
|
-
return { text: lines.join("\n") };
|
|
470
|
-
}
|
|
471
|
-
if (arg === "off" || arg === "disable") {
|
|
472
|
-
const restored = disableProxy(api);
|
|
473
|
-
if (restored.length === 0) {
|
|
474
|
-
return { text: "Compression proxy was not active." };
|
|
475
|
-
}
|
|
476
|
-
return {
|
|
477
|
-
text: [
|
|
478
|
-
"**Compression disabled.** Restored original provider configs.",
|
|
479
|
-
"",
|
|
480
|
-
`Restored: ${restored.join(", ")}`,
|
|
481
|
-
"",
|
|
482
|
-
"To re-enable: `/compress on`"
|
|
483
|
-
].join("\n")
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
const proxyState = readProxyState();
|
|
487
|
-
const occKey = getApiKey(api);
|
|
488
|
-
const statusLines = [
|
|
489
|
-
"**OpenCompress Transparent Proxy**",
|
|
396
|
+
description: "Show OpenCompress status and available compressed models",
|
|
397
|
+
handler: async () => {
|
|
398
|
+
const occKey = getOccKey(api);
|
|
399
|
+
const providers = getProviders(api);
|
|
400
|
+
const models = generateModelCatalog(providers);
|
|
401
|
+
const lines = [
|
|
402
|
+
"**OpenCompress Status**",
|
|
490
403
|
"",
|
|
491
|
-
`
|
|
492
|
-
`
|
|
493
|
-
];
|
|
494
|
-
if (proxyState.enabled && Object.keys(proxyState.originals).length > 0) {
|
|
495
|
-
statusLines.push(`Proxied providers: ${Object.keys(proxyState.originals).join(", ")}`);
|
|
496
|
-
}
|
|
497
|
-
statusLines.push(
|
|
404
|
+
`API key: ${occKey ? `${occKey.slice(0, 12)}...` : "not set (run `openclaw onboard opencompress`)"}`,
|
|
405
|
+
`Proxy: http://${PROXY_HOST}:${PROXY_PORT}`,
|
|
498
406
|
"",
|
|
499
|
-
"**
|
|
500
|
-
|
|
501
|
-
"
|
|
502
|
-
"
|
|
503
|
-
|
|
504
|
-
return { text:
|
|
505
|
-
}
|
|
506
|
-
});
|
|
507
|
-
api.logger.info("Registered /compress command");
|
|
508
|
-
setTimeout(() => {
|
|
509
|
-
const proxyState = readProxyState();
|
|
510
|
-
const autoEnable = api.pluginConfig?._autoEnableProxy;
|
|
511
|
-
if (proxyState.enabled || autoEnable) {
|
|
512
|
-
const result = enableProxy(api, baseUrl);
|
|
513
|
-
if (result.proxied.length > 0) {
|
|
514
|
-
api.logger.info(`Compression active: ${result.proxied.length} providers (${result.proxied.join(", ")})`);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}, 2e3);
|
|
518
|
-
const os = __require("os");
|
|
519
|
-
const fs = __require("fs");
|
|
520
|
-
const path = __require("path");
|
|
521
|
-
const logDir = path.join(os.homedir(), ".openclaw", "opencompress-logs");
|
|
522
|
-
try {
|
|
523
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
524
|
-
} catch {
|
|
525
|
-
}
|
|
526
|
-
api.on("llm_input", (...args) => {
|
|
527
|
-
try {
|
|
528
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
529
|
-
const event = args[0] || {};
|
|
530
|
-
const systemPromptLen = typeof event.systemPrompt === "string" ? event.systemPrompt.length : 0;
|
|
531
|
-
const historyMessages = Array.isArray(event.historyMessages) ? event.historyMessages : [];
|
|
532
|
-
const promptLen = typeof event.prompt === "string" ? event.prompt.length : 0;
|
|
533
|
-
const systemTokens = Math.ceil(systemPromptLen / 4);
|
|
534
|
-
const historyText = JSON.stringify(historyMessages);
|
|
535
|
-
const historyTokens = Math.ceil(historyText.length / 4);
|
|
536
|
-
const promptTokens = Math.ceil(promptLen / 4);
|
|
537
|
-
const totalTokens = systemTokens + historyTokens + promptTokens;
|
|
538
|
-
const entry = {
|
|
539
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
540
|
-
_argCount: args.length,
|
|
541
|
-
_eventKeys: Object.keys(event),
|
|
542
|
-
runId: event.runId,
|
|
543
|
-
sessionId: event.sessionId,
|
|
544
|
-
provider: event.provider,
|
|
545
|
-
model: event.model,
|
|
546
|
-
imagesCount: event.imagesCount || 0,
|
|
547
|
-
tokenEstimate: { systemPrompt: systemTokens, history: historyTokens, currentPrompt: promptTokens, total: totalTokens },
|
|
548
|
-
charLengths: { systemPrompt: systemPromptLen, history: historyText.length, currentPrompt: promptLen },
|
|
549
|
-
historyMessageCount: historyMessages.length,
|
|
550
|
-
systemPrompt: event.systemPrompt,
|
|
551
|
-
historyMessages: event.historyMessages,
|
|
552
|
-
prompt: event.prompt
|
|
553
|
-
};
|
|
554
|
-
const model = typeof event.model === "string" ? event.model.replace(/\//g, "_") : "unknown";
|
|
555
|
-
const filename = `${ts}_${model}.json`;
|
|
556
|
-
fs.writeFileSync(path.join(logDir, filename), JSON.stringify(entry, null, 2) + "\n");
|
|
557
|
-
} catch (err) {
|
|
558
|
-
try {
|
|
559
|
-
const errFile = path.join(logDir, `error_${Date.now()}.txt`);
|
|
560
|
-
fs.writeFileSync(errFile, `${err}
|
|
561
|
-
${err.stack || ""}
|
|
562
|
-
args: ${JSON.stringify(args?.map((a) => typeof a))}
|
|
563
|
-
`);
|
|
564
|
-
} catch {
|
|
565
|
-
}
|
|
407
|
+
"**Available compressed models:**",
|
|
408
|
+
...models.map((m) => ` ${m.id}`),
|
|
409
|
+
"",
|
|
410
|
+
"Select any of these models to use compression."
|
|
411
|
+
];
|
|
412
|
+
return { text: lines.join("\n") };
|
|
566
413
|
}
|
|
567
414
|
});
|
|
568
|
-
api.logger.info(`LLM input logging enabled \u2192 ${logDir}`);
|
|
569
415
|
}
|
|
570
416
|
};
|
|
571
417
|
var index_default = plugin;
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "opencompress",
|
|
3
3
|
"name": "OpenCompress",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "BYOK prompt compression — save 20-40% on any LLM. Your 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
|
}
|
package/openclaw.security.json
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "opencompress",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"securityProfile": {
|
|
5
5
|
"dataHandling": {
|
|
6
|
-
"promptContent": "pass-through",
|
|
7
|
-
"
|
|
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": "
|
|
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
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
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": "
|
|
56
|
-
"environment": ["OPENCOMPRESS_API_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
|
|
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
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"billing": "
|
|
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
|
}
|