deepclaw-openclaw 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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/SECURITY.md +30 -0
- package/index.ts +26 -0
- package/openclaw.plugin.json +47 -0
- package/package.json +84 -0
- package/src/config.ts +22 -0
- package/src/pricing.ts +190 -0
- package/src/service.ts +202 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented here.
|
|
4
|
+
|
|
5
|
+
## 0.1.0 - Public release candidate
|
|
6
|
+
|
|
7
|
+
- Initial OpenClaw plugin package metadata.
|
|
8
|
+
- DeepClaw plugin manifest.
|
|
9
|
+
- Runtime service for `llm_output` and `session_end` telemetry.
|
|
10
|
+
- Provider-aware pricing table with cache and reasoning token support.
|
|
11
|
+
- TypeScript validation, Vitest tests, and package dry-run workflow.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Digitizers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# DeepClaw OpenClaw Plugin
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Digitizers/deepclaw-openclaw/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://github.com/Digitizers/deepclaw-openclaw)
|
|
6
|
+
[](https://github.com/Digitizers/deepclaw-openclaw)
|
|
7
|
+
|
|
8
|
+
**DeepClaw for OpenClaw** streams LLM usage telemetry from OpenClaw into DeepClaw: model, provider, input/output tokens, cache reads/writes, reasoning tokens, and calculated cost breakdowns.
|
|
9
|
+
|
|
10
|
+
It is built for teams that run multi-provider AI agents and need to know, in near real time, where LLM spend is going.
|
|
11
|
+
|
|
12
|
+
## Highlights
|
|
13
|
+
|
|
14
|
+
- **Real-time telemetry** from OpenClaw `llm_output` hooks.
|
|
15
|
+
- **Per-session batching** with periodic flush and final `session_end` flush.
|
|
16
|
+
- **Token-level breakdowns**: input, output, cache read, cache write, reasoning/thinking tokens.
|
|
17
|
+
- **Cost attribution** using a maintained pricing table with explicit `costSource` metadata.
|
|
18
|
+
- **Provider-aware pricing** for Gemini, OpenAI, Anthropic, DeepSeek, and xAI models.
|
|
19
|
+
- **Safe by default**: disabled until configured with an explicit sync token.
|
|
20
|
+
- **Small package surface**: only runtime files, plugin manifest, README, license, changelog, and security policy are published.
|
|
21
|
+
|
|
22
|
+
## Why this plugin exists
|
|
23
|
+
|
|
24
|
+
Agent cost tracking often breaks around preview models, cache tokens, reasoning tokens, or provider-specific usage fields. The result is usually either `$0` cost, inflated estimates, or numbers that cannot be audited later.
|
|
25
|
+
|
|
26
|
+
This plugin captures the raw usage shape exposed by OpenClaw, normalizes the important fields, calculates a structured cost breakdown, and sends it to DeepClaw for long-term analysis.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install deepclaw-openclaw
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For local development or manual installation:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone https://github.com/Digitizers/deepclaw-openclaw.git
|
|
38
|
+
cd deepclaw-openclaw
|
|
39
|
+
npm install
|
|
40
|
+
npm run ci
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
Configure the plugin in your OpenClaw agent config:
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
plugins:
|
|
49
|
+
deepclaw-openclaw:
|
|
50
|
+
enabled: true
|
|
51
|
+
syncToken: "YOUR_DEEPCLAW_SYNC_TOKEN"
|
|
52
|
+
instanceId: "prod-agent-01"
|
|
53
|
+
apiUrl: "https://app.deep-claw.com"
|
|
54
|
+
flushIntervalMs: 5000
|
|
55
|
+
debug: false
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Environment variables are also supported:
|
|
59
|
+
|
|
60
|
+
| Variable | Required | Description |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `DEEPCLAW_SYNC_TOKEN` | Yes | Bearer token used to authenticate with DeepClaw. |
|
|
63
|
+
| `DEEPCLAW_INSTANCE_ID` | Recommended | Stable identifier for this OpenClaw runtime. Defaults to `default`. |
|
|
64
|
+
| `DEEPCLAW_API_URL` | No | DeepClaw base URL. Defaults to `https://app.deep-claw.com`. |
|
|
65
|
+
|
|
66
|
+
## Data flow
|
|
67
|
+
|
|
68
|
+
| OpenClaw hook | What happens |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `llm_output` | Capture provider, model, usage counters, cache counters, reasoning tokens, and calculated cost. |
|
|
71
|
+
| Periodic timer | Flush accumulated records every `flushIntervalMs` milliseconds. |
|
|
72
|
+
| `session_end` | Flush final session data and clear the local accumulator. |
|
|
73
|
+
|
|
74
|
+
Payloads are sent to:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
POST /api/sync/session
|
|
78
|
+
Authorization: Bearer <syncToken>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Cost source semantics
|
|
82
|
+
|
|
83
|
+
Each LLM record includes `costSource`:
|
|
84
|
+
|
|
85
|
+
| Value | Meaning |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `table` | Cost was calculated using the plugin's pricing table. |
|
|
88
|
+
| `unknown` | No supported pricing entry was found; cost is sent as `0` so DeepClaw can estimate or flag it. |
|
|
89
|
+
|
|
90
|
+
> Note: usage counters come from OpenClaw/provider response metadata. Dollar cost is calculated locally by this plugin unless OpenClaw adds a trusted provider-cost field in a future hook shape.
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm install
|
|
96
|
+
npm run typecheck
|
|
97
|
+
npm test
|
|
98
|
+
npm pack --dry-run
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Useful scripts:
|
|
102
|
+
|
|
103
|
+
| Script | Purpose |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `npm run typecheck` | Strict TypeScript validation. |
|
|
106
|
+
| `npm test` | Run Vitest tests. |
|
|
107
|
+
| `npm run ci` | Typecheck, test, and dry-run package contents. |
|
|
108
|
+
| `npm run smoke` | Run plugin smoke tests. |
|
|
109
|
+
|
|
110
|
+
## Published package contents
|
|
111
|
+
|
|
112
|
+
The package is intentionally narrow:
|
|
113
|
+
|
|
114
|
+
- `index.ts`
|
|
115
|
+
- `src/config.ts`
|
|
116
|
+
- `src/pricing.ts`
|
|
117
|
+
- `src/service.ts`
|
|
118
|
+
- `openclaw.plugin.json`
|
|
119
|
+
- `README.md`
|
|
120
|
+
- `LICENSE`
|
|
121
|
+
- `SECURITY.md`
|
|
122
|
+
- `CHANGELOG.md`
|
|
123
|
+
|
|
124
|
+
## Security
|
|
125
|
+
|
|
126
|
+
Do not commit sync tokens or OpenClaw runtime state. See [SECURITY.md](SECURITY.md) for reporting and handling guidance.
|
|
127
|
+
|
|
128
|
+
## Status
|
|
129
|
+
|
|
130
|
+
`0.1.0` is an initial public release candidate. APIs may still evolve with OpenClaw plugin hook changes.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT © Ben Kalsky / Digitizers.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Security fixes are handled on the latest `main` branch until the first stable release line is established.
|
|
6
|
+
|
|
7
|
+
## Reporting a Vulnerability
|
|
8
|
+
|
|
9
|
+
Please do **not** open public GitHub issues for secrets, authentication bypasses, or data exposure reports.
|
|
10
|
+
|
|
11
|
+
Report privately via one of these channels:
|
|
12
|
+
|
|
13
|
+
- GitHub Security Advisory: https://github.com/Digitizers/deepclaw-openclaw/security/advisories/new
|
|
14
|
+
- Email: ben@digitizer.co.il
|
|
15
|
+
|
|
16
|
+
Please include:
|
|
17
|
+
|
|
18
|
+
- Affected version or commit SHA
|
|
19
|
+
- Reproduction steps
|
|
20
|
+
- Expected vs. actual behavior
|
|
21
|
+
- Any relevant logs with secrets redacted
|
|
22
|
+
|
|
23
|
+
## Secret Handling
|
|
24
|
+
|
|
25
|
+
This plugin uses a DeepClaw sync token for outbound telemetry. Never commit:
|
|
26
|
+
|
|
27
|
+
- `DEEPCLAW_SYNC_TOKEN`
|
|
28
|
+
- `.env` files
|
|
29
|
+
- OpenClaw runtime config containing credentials
|
|
30
|
+
- Session dumps or raw provider responses that may include user data
|
package/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// DeepClaw OpenClaw Plugin — real-time LLM cost & usage tracking
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
4
|
+
import { createDeepClawService } from "./src/service.js";
|
|
5
|
+
import { parseDeepClawConfig } from "./src/config.js";
|
|
6
|
+
|
|
7
|
+
const plugin = {
|
|
8
|
+
id: "deepclaw-openclaw",
|
|
9
|
+
name: "DeepClaw",
|
|
10
|
+
description: "Export real-time LLM cost & usage data to DeepClaw analytics",
|
|
11
|
+
configSchema: emptyPluginConfigSchema(),
|
|
12
|
+
register(api: OpenClawPluginApi) {
|
|
13
|
+
const config = parseDeepClawConfig(api.pluginConfig);
|
|
14
|
+
if (!config.enabled || !config.syncToken) {
|
|
15
|
+
api.logger.info(
|
|
16
|
+
"[deepclaw] plugin loaded but disabled (set enabled=true and syncToken in config)"
|
|
17
|
+
);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const service = createDeepClawService(api, config);
|
|
21
|
+
service.registerHooks();
|
|
22
|
+
api.logger.info(`[deepclaw] started — tracking to ${config.apiUrl}`);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default plugin;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "deepclaw-openclaw",
|
|
3
|
+
"name": "DeepClaw",
|
|
4
|
+
"description": "Export real-time LLM cost & usage data to DeepClaw analytics",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"enabled": { "type": "boolean" },
|
|
10
|
+
"apiUrl": { "type": "string" },
|
|
11
|
+
"syncToken": { "type": "string" },
|
|
12
|
+
"instanceId": { "type": "string" },
|
|
13
|
+
"flushIntervalMs": { "type": "number" },
|
|
14
|
+
"debug": { "type": "boolean" }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"uiHints": {
|
|
18
|
+
"enabled": {
|
|
19
|
+
"label": "Enabled",
|
|
20
|
+
"help": "Enable DeepClaw cost tracking."
|
|
21
|
+
},
|
|
22
|
+
"apiUrl": {
|
|
23
|
+
"label": "DeepClaw API URL",
|
|
24
|
+
"placeholder": "https://deepclaw.app",
|
|
25
|
+
"help": "Base URL of your DeepClaw instance."
|
|
26
|
+
},
|
|
27
|
+
"syncToken": {
|
|
28
|
+
"label": "Sync Token",
|
|
29
|
+
"sensitive": true,
|
|
30
|
+
"help": "Token from DeepClaw → Settings → API."
|
|
31
|
+
},
|
|
32
|
+
"instanceId": {
|
|
33
|
+
"label": "Instance ID",
|
|
34
|
+
"placeholder": "srv1421692",
|
|
35
|
+
"help": "Identifies this OpenClaw instance in DeepClaw."
|
|
36
|
+
},
|
|
37
|
+
"flushIntervalMs": {
|
|
38
|
+
"label": "Flush Interval (ms)",
|
|
39
|
+
"placeholder": "5000",
|
|
40
|
+
"help": "How often to batch-send accumulated usage. Default: 5000."
|
|
41
|
+
},
|
|
42
|
+
"debug": {
|
|
43
|
+
"label": "Debug Logging",
|
|
44
|
+
"help": "Log each LLM event to console."
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "deepclaw-openclaw",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "DeepClaw observability plugin for OpenClaw — real-time LLM usage, token, cache, reasoning, and cost telemetry.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Digitizers/deepclaw-openclaw.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/Digitizers/deepclaw-openclaw#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Digitizers/deepclaw-openclaw/issues"
|
|
13
|
+
},
|
|
14
|
+
"author": "Ben Kalsky <ben@digitizer.co.il> (https://digitizer.co.il)",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22.12.0",
|
|
18
|
+
"npm": ">=10"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"lint": "tsc --noEmit",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"smoke": "vitest run src/**/*.test.ts",
|
|
25
|
+
"ci": "npm run typecheck && npm test && npm pack --dry-run",
|
|
26
|
+
"prepack": "npm run typecheck && npm test"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"ai-gateway",
|
|
30
|
+
"ai-observability",
|
|
31
|
+
"analytics",
|
|
32
|
+
"cache-tokens",
|
|
33
|
+
"cost-tracking",
|
|
34
|
+
"deepclaw",
|
|
35
|
+
"llm-analytics",
|
|
36
|
+
"llm-costs",
|
|
37
|
+
"llm-observability",
|
|
38
|
+
"monitoring",
|
|
39
|
+
"openclaw",
|
|
40
|
+
"openclaw-extension",
|
|
41
|
+
"openclaw-plugin",
|
|
42
|
+
"reasoning-tokens",
|
|
43
|
+
"token-usage"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"openclaw": ">=2026.3.2"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^22.0.0",
|
|
51
|
+
"tsx": "^4.21.0",
|
|
52
|
+
"typescript": "^5.4.0",
|
|
53
|
+
"vitest": "^4.1.5",
|
|
54
|
+
"openclaw": "^2026.4.24"
|
|
55
|
+
},
|
|
56
|
+
"files": [
|
|
57
|
+
"index.ts",
|
|
58
|
+
"src/config.ts",
|
|
59
|
+
"src/pricing.ts",
|
|
60
|
+
"src/service.ts",
|
|
61
|
+
"openclaw.plugin.json",
|
|
62
|
+
"README.md",
|
|
63
|
+
"LICENSE",
|
|
64
|
+
"CHANGELOG.md",
|
|
65
|
+
"SECURITY.md"
|
|
66
|
+
],
|
|
67
|
+
"openclaw": {
|
|
68
|
+
"type": "plugin",
|
|
69
|
+
"category": "observability",
|
|
70
|
+
"tags": [
|
|
71
|
+
"deepclaw",
|
|
72
|
+
"cost-tracking",
|
|
73
|
+
"tokens",
|
|
74
|
+
"llm-observability",
|
|
75
|
+
"monitoring"
|
|
76
|
+
],
|
|
77
|
+
"extensions": [
|
|
78
|
+
"./index.ts"
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
"publishConfig": {
|
|
82
|
+
"access": "public"
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface DeepClawConfig {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
syncToken: string;
|
|
5
|
+
instanceId: string;
|
|
6
|
+
flushIntervalMs: number;
|
|
7
|
+
debug: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseDeepClawConfig(raw: unknown): DeepClawConfig {
|
|
11
|
+
const cfg = (raw ?? {}) as Record<string, unknown>;
|
|
12
|
+
return {
|
|
13
|
+
enabled: cfg["enabled"] !== false && cfg["enabled"] !== undefined
|
|
14
|
+
? Boolean(cfg["enabled"])
|
|
15
|
+
: false,
|
|
16
|
+
apiUrl: String(cfg["apiUrl"] ?? process.env["DEEPCLAW_API_URL"] ?? "https://app.deep-claw.com"),
|
|
17
|
+
syncToken: String(cfg["syncToken"] ?? process.env["DEEPCLAW_SYNC_TOKEN"] ?? ""),
|
|
18
|
+
instanceId: String(cfg["instanceId"] ?? process.env["DEEPCLAW_INSTANCE_ID"] ?? "default"),
|
|
19
|
+
flushIntervalMs: Number(cfg["flushIntervalMs"] ?? 5000),
|
|
20
|
+
debug: Boolean(cfg["debug"] ?? false),
|
|
21
|
+
};
|
|
22
|
+
}
|
package/src/pricing.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM pricing table — cost per 1M tokens in USD.
|
|
3
|
+
* Source: official provider pricing pages (updated 2025-04).
|
|
4
|
+
*
|
|
5
|
+
* For Gemini: step-function tiered pricing (NOT graduated).
|
|
6
|
+
* If promptTokenCount exceeds the threshold, ALL tokens are billed at the higher rate.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ModelPricing {
|
|
10
|
+
inputPerM: number;
|
|
11
|
+
outputPerM: number;
|
|
12
|
+
cacheReadPerM?: number;
|
|
13
|
+
cacheWritePerM?: number;
|
|
14
|
+
/** If set, all tokens are billed at higher rates when promptTokens > this */
|
|
15
|
+
tierThreshold?: number;
|
|
16
|
+
inputPerMAboveTier?: number;
|
|
17
|
+
outputPerMAboveTier?: number;
|
|
18
|
+
cacheReadPerMAboveTier?: number;
|
|
19
|
+
/** Separate rate for thinking tokens (e.g. Gemini 2.5) */
|
|
20
|
+
thinkingOutputPerM?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const PRICING: Record<string, ModelPricing> = {
|
|
24
|
+
// ── Gemini ───────────────────────────────────────────────────────────────
|
|
25
|
+
"gemini-2.5-flash": {
|
|
26
|
+
inputPerM: 0.15,
|
|
27
|
+
outputPerM: 0.60,
|
|
28
|
+
cacheReadPerM: 0.0375,
|
|
29
|
+
cacheWritePerM: 1.00,
|
|
30
|
+
thinkingOutputPerM: 3.50,
|
|
31
|
+
tierThreshold: 200_000,
|
|
32
|
+
inputPerMAboveTier: 0.30,
|
|
33
|
+
outputPerMAboveTier: 1.20,
|
|
34
|
+
cacheReadPerMAboveTier: 0.075,
|
|
35
|
+
},
|
|
36
|
+
"gemini-2.5-pro": {
|
|
37
|
+
inputPerM: 1.25,
|
|
38
|
+
outputPerM: 10.00,
|
|
39
|
+
cacheReadPerM: 0.31,
|
|
40
|
+
tierThreshold: 200_000,
|
|
41
|
+
inputPerMAboveTier: 2.50,
|
|
42
|
+
outputPerMAboveTier: 15.00,
|
|
43
|
+
cacheReadPerMAboveTier: 0.63,
|
|
44
|
+
},
|
|
45
|
+
"gemini-2.0-flash": {
|
|
46
|
+
inputPerM: 0.10,
|
|
47
|
+
outputPerM: 0.40,
|
|
48
|
+
cacheReadPerM: 0.025,
|
|
49
|
+
},
|
|
50
|
+
"gemini-1.5-pro": {
|
|
51
|
+
inputPerM: 1.25,
|
|
52
|
+
outputPerM: 5.00,
|
|
53
|
+
cacheReadPerM: 0.3125,
|
|
54
|
+
tierThreshold: 128_000,
|
|
55
|
+
inputPerMAboveTier: 2.50,
|
|
56
|
+
outputPerMAboveTier: 10.00,
|
|
57
|
+
cacheReadPerMAboveTier: 0.625,
|
|
58
|
+
},
|
|
59
|
+
"gemini-1.5-flash": {
|
|
60
|
+
inputPerM: 0.075,
|
|
61
|
+
outputPerM: 0.30,
|
|
62
|
+
cacheReadPerM: 0.01875,
|
|
63
|
+
tierThreshold: 128_000,
|
|
64
|
+
inputPerMAboveTier: 0.15,
|
|
65
|
+
outputPerMAboveTier: 0.60,
|
|
66
|
+
cacheReadPerMAboveTier: 0.0375,
|
|
67
|
+
},
|
|
68
|
+
// ── OpenAI ───────────────────────────────────────────────────────────────
|
|
69
|
+
"gpt-4o": { inputPerM: 2.50, outputPerM: 10.00, cacheReadPerM: 1.25 },
|
|
70
|
+
"gpt-4o-mini": { inputPerM: 0.15, outputPerM: 0.60, cacheReadPerM: 0.075 },
|
|
71
|
+
"gpt-4.1": { inputPerM: 2.00, outputPerM: 8.00, cacheReadPerM: 0.50 },
|
|
72
|
+
"gpt-4.1-mini": { inputPerM: 0.40, outputPerM: 1.60, cacheReadPerM: 0.10 },
|
|
73
|
+
"gpt-5": { inputPerM: 10.00, outputPerM: 40.00, cacheReadPerM: 2.50 },
|
|
74
|
+
"gpt-5.4": { inputPerM: 10.00, outputPerM: 40.00, cacheReadPerM: 2.50 },
|
|
75
|
+
"gpt-5.4-mini": { inputPerM: 0.40, outputPerM: 1.60, cacheReadPerM: 0.10 },
|
|
76
|
+
"o3": { inputPerM: 10.00, outputPerM: 40.00, cacheReadPerM: 2.50 },
|
|
77
|
+
"o4-mini": { inputPerM: 1.10, outputPerM: 4.40, cacheReadPerM: 0.275 },
|
|
78
|
+
// ── Anthropic ────────────────────────────────────────────────────────────
|
|
79
|
+
"claude-opus-4": { inputPerM: 15.00, outputPerM: 75.00, cacheReadPerM: 1.50, cacheWritePerM: 18.75 },
|
|
80
|
+
"claude-opus-4-5": { inputPerM: 15.00, outputPerM: 75.00, cacheReadPerM: 1.50, cacheWritePerM: 18.75 },
|
|
81
|
+
"claude-opus-4-6": { inputPerM: 15.00, outputPerM: 75.00, cacheReadPerM: 1.50, cacheWritePerM: 18.75 },
|
|
82
|
+
"claude-sonnet-4": { inputPerM: 3.00, outputPerM: 15.00, cacheReadPerM: 0.30, cacheWritePerM: 3.75 },
|
|
83
|
+
"claude-sonnet-4-5": { inputPerM: 3.00, outputPerM: 15.00, cacheReadPerM: 0.30, cacheWritePerM: 3.75 },
|
|
84
|
+
"claude-sonnet-4-6": { inputPerM: 3.00, outputPerM: 15.00, cacheReadPerM: 0.30, cacheWritePerM: 3.75 },
|
|
85
|
+
"claude-haiku-3-5": { inputPerM: 0.80, outputPerM: 4.00, cacheReadPerM: 0.08, cacheWritePerM: 1.00 },
|
|
86
|
+
// ── DeepSeek ─────────────────────────────────────────────────────────────
|
|
87
|
+
"deepseek-chat": { inputPerM: 0.27, outputPerM: 1.10, cacheReadPerM: 0.07 },
|
|
88
|
+
"deepseek-reasoner": { inputPerM: 0.55, outputPerM: 2.19, cacheReadPerM: 0.14 },
|
|
89
|
+
// ── xAI ──────────────────────────────────────────────────────────────────
|
|
90
|
+
"grok-3": { inputPerM: 3.00, outputPerM: 15.00 },
|
|
91
|
+
"grok-3-mini": { inputPerM: 0.30, outputPerM: 0.50 },
|
|
92
|
+
"grok-2-vision-1212": { inputPerM: 2.00, outputPerM: 10.00 },
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Normalize model name for lookup (strip provider prefix, version suffixes like -latest) */
|
|
96
|
+
function normalizeModel(raw: string): string {
|
|
97
|
+
const lower = raw.toLowerCase();
|
|
98
|
+
// Strip provider prefix (e.g. "google/gemini-2.5-flash" → "gemini-2.5-flash")
|
|
99
|
+
const slash = lower.lastIndexOf("/");
|
|
100
|
+
const name = slash >= 0 ? lower.slice(slash + 1) : lower;
|
|
101
|
+
// Strip common suffixes
|
|
102
|
+
return name
|
|
103
|
+
.replace(/-latest$/, "")
|
|
104
|
+
.replace(/-\d{8}$/, "") // date suffixes like -20241022
|
|
105
|
+
.replace(/-exp$/, "")
|
|
106
|
+
.replace(/-preview$/, "");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface CostBreakdown {
|
|
110
|
+
inputCost: number;
|
|
111
|
+
outputCost: number;
|
|
112
|
+
thinkingCost: number;
|
|
113
|
+
cacheReadCost: number;
|
|
114
|
+
cacheWriteCost: number;
|
|
115
|
+
totalCost: number;
|
|
116
|
+
priceSource: "table" | "unknown";
|
|
117
|
+
modelKey: string | null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function calculateCost(opts: {
|
|
121
|
+
model: string;
|
|
122
|
+
inputTokens: number;
|
|
123
|
+
outputTokens: number;
|
|
124
|
+
cacheReadTokens?: number;
|
|
125
|
+
cacheWriteTokens?: number;
|
|
126
|
+
thinkingTokens?: number;
|
|
127
|
+
}): CostBreakdown {
|
|
128
|
+
const key = normalizeModel(opts.model);
|
|
129
|
+
|
|
130
|
+
// Try exact match, then longest prefix match for dated/preview provider aliases.
|
|
131
|
+
let modelKey: string | null = PRICING[key] ? key : null;
|
|
132
|
+
let pricing: ModelPricing | undefined = modelKey ? PRICING[modelKey] : undefined;
|
|
133
|
+
if (!pricing) {
|
|
134
|
+
modelKey = Object.keys(PRICING)
|
|
135
|
+
.filter((k) => key.startsWith(k) || k.startsWith(key))
|
|
136
|
+
.sort((a, b) => b.length - a.length)[0] ?? null;
|
|
137
|
+
pricing = modelKey ? PRICING[modelKey] : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!pricing) {
|
|
141
|
+
return {
|
|
142
|
+
inputCost: 0, outputCost: 0,
|
|
143
|
+
thinkingCost: 0,
|
|
144
|
+
cacheReadCost: 0, cacheWriteCost: 0,
|
|
145
|
+
totalCost: 0,
|
|
146
|
+
priceSource: "unknown",
|
|
147
|
+
modelKey: null,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const M = 1_000_000;
|
|
152
|
+
const totalInput = opts.inputTokens;
|
|
153
|
+
|
|
154
|
+
// Gemini step-function tier: if total prompt > threshold, ALL tokens at higher rate
|
|
155
|
+
const aboveTier = pricing.tierThreshold && totalInput > pricing.tierThreshold;
|
|
156
|
+
const inputRate = aboveTier ? (pricing.inputPerMAboveTier ?? pricing.inputPerM) : pricing.inputPerM;
|
|
157
|
+
const outputRate = aboveTier ? (pricing.outputPerMAboveTier ?? pricing.outputPerM) : pricing.outputPerM;
|
|
158
|
+
const cacheReadRate = aboveTier
|
|
159
|
+
? (pricing.cacheReadPerMAboveTier ?? pricing.cacheReadPerM ?? 0)
|
|
160
|
+
: (pricing.cacheReadPerM ?? 0);
|
|
161
|
+
|
|
162
|
+
const cacheReadTokens = opts.cacheReadTokens ?? 0;
|
|
163
|
+
const cacheWriteTokens = opts.cacheWriteTokens ?? 0;
|
|
164
|
+
const thinkingTokens = opts.thinkingTokens ?? 0;
|
|
165
|
+
|
|
166
|
+
// For cached input: pay cache-read rate instead of full input rate
|
|
167
|
+
const nonCachedInput = Math.max(0, totalInput - cacheReadTokens);
|
|
168
|
+
const inputCost = (nonCachedInput / M) * inputRate;
|
|
169
|
+
const cacheReadCost = (cacheReadTokens / M) * cacheReadRate;
|
|
170
|
+
const cacheWriteCost = (cacheWriteTokens / M) * (pricing.cacheWritePerM ?? inputRate);
|
|
171
|
+
|
|
172
|
+
// Thinking vs normal output (additive — Gemini separates candidatesTokenCount and thoughtsTokenCount)
|
|
173
|
+
const outputCost = (opts.outputTokens / M) * outputRate;
|
|
174
|
+
const thinkingCost = pricing.thinkingOutputPerM
|
|
175
|
+
? (thinkingTokens / M) * pricing.thinkingOutputPerM
|
|
176
|
+
: (thinkingTokens / M) * outputRate;
|
|
177
|
+
|
|
178
|
+
const totalCost = inputCost + outputCost + thinkingCost + cacheReadCost + cacheWriteCost;
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
inputCost,
|
|
182
|
+
outputCost,
|
|
183
|
+
thinkingCost,
|
|
184
|
+
cacheReadCost,
|
|
185
|
+
cacheWriteCost,
|
|
186
|
+
totalCost,
|
|
187
|
+
priceSource: "table",
|
|
188
|
+
modelKey,
|
|
189
|
+
};
|
|
190
|
+
}
|
package/src/service.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { DeepClawConfig } from "./config.js";
|
|
3
|
+
import { calculateCost } from "./pricing.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One LLM call record — captured from llm_output hook.
|
|
7
|
+
* usage.total comes directly from the provider (not estimated).
|
|
8
|
+
*/
|
|
9
|
+
export interface LlmRecord {
|
|
10
|
+
runId: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
provider: string;
|
|
13
|
+
model: string;
|
|
14
|
+
tokensIn: number;
|
|
15
|
+
tokensOut: number;
|
|
16
|
+
cacheRead: number;
|
|
17
|
+
cacheWrite: number;
|
|
18
|
+
thinkingTokens: number; // thinking/reasoning tokens (Gemini thoughtsTokenCount, OpenAI reasoning_tokens)
|
|
19
|
+
/** Calculated cost in USD */
|
|
20
|
+
costUsd: number;
|
|
21
|
+
costSource: "table" | "unknown";
|
|
22
|
+
/** Per-component breakdown */
|
|
23
|
+
costBreakdown?: { input: number; output: number; cacheRead: number; cacheWrite: number; thinking: number; };
|
|
24
|
+
timestamp: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Aggregated per-session totals */
|
|
28
|
+
interface SessionAccumulator {
|
|
29
|
+
sessionId: string;
|
|
30
|
+
records: LlmRecord[];
|
|
31
|
+
startedAt: number;
|
|
32
|
+
lastSeenAt: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createDeepClawService(api: OpenClawPluginApi, config: DeepClawConfig) {
|
|
36
|
+
const logger = api.logger;
|
|
37
|
+
const sessions = new Map<string, SessionAccumulator>();
|
|
38
|
+
let flushTimer: ReturnType<typeof setInterval> | undefined;
|
|
39
|
+
|
|
40
|
+
// ─── helpers ────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function getOrCreateSession(sessionId: string): SessionAccumulator {
|
|
43
|
+
let acc = sessions.get(sessionId);
|
|
44
|
+
if (!acc) {
|
|
45
|
+
acc = { sessionId, records: [], startedAt: Date.now(), lastSeenAt: Date.now() };
|
|
46
|
+
sessions.set(sessionId, acc);
|
|
47
|
+
}
|
|
48
|
+
acc.lastSeenAt = Date.now();
|
|
49
|
+
return acc;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function postToDeepClaw(path: string, body: unknown): Promise<void> {
|
|
53
|
+
const url = `${config.apiUrl.replace(/\/$/, "")}${path}`;
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
Authorization: `Bearer ${config.syncToken}`,
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
logger.warn(`[deepclaw] POST ${path} → ${res.status}: ${await res.text().catch(() => "")}`);
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
logger.warn(`[deepclaw] POST ${path} failed: ${String(err)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function flushSession(sessionId: string, reason: "session_end" | "periodic"): Promise<void> {
|
|
72
|
+
const acc = sessions.get(sessionId);
|
|
73
|
+
if (!acc || acc.records.length === 0) return;
|
|
74
|
+
|
|
75
|
+
const totalCostUsd = acc.records.reduce((sum, r) => sum + (r.costUsd ?? 0), 0);
|
|
76
|
+
const totalIn = acc.records.reduce((sum, r) => sum + r.tokensIn, 0);
|
|
77
|
+
const totalOut = acc.records.reduce((sum, r) => sum + r.tokensOut, 0);
|
|
78
|
+
const totalCacheRead = acc.records.reduce((sum, r) => sum + r.cacheRead, 0);
|
|
79
|
+
const totalCacheWrite = acc.records.reduce((sum, r) => sum + r.cacheWrite, 0);
|
|
80
|
+
const totalThinking = acc.records.reduce((sum, r) => sum + r.thinkingTokens, 0);
|
|
81
|
+
|
|
82
|
+
const payload = {
|
|
83
|
+
instanceId: config.instanceId,
|
|
84
|
+
sessionId,
|
|
85
|
+
flushReason: reason,
|
|
86
|
+
llmCalls: acc.records,
|
|
87
|
+
summary: {
|
|
88
|
+
totalCostUsd,
|
|
89
|
+
costSource: acc.records.some((r) => r.costSource === "table") ? "table" : "unknown",
|
|
90
|
+
totalTokensIn: totalIn,
|
|
91
|
+
totalTokensOut: totalOut,
|
|
92
|
+
totalCacheRead,
|
|
93
|
+
totalCacheWrite,
|
|
94
|
+
totalThinkingTokens: totalThinking,
|
|
95
|
+
callCount: acc.records.length,
|
|
96
|
+
startedAt: acc.startedAt,
|
|
97
|
+
lastSeenAt: acc.lastSeenAt,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (config.debug) {
|
|
102
|
+
logger.info(`[deepclaw] flushing session ${sessionId}: ${acc.records.length} calls, $${totalCostUsd.toFixed(6)}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await postToDeepClaw("/api/sync/session", payload);
|
|
106
|
+
|
|
107
|
+
if (reason === "session_end") {
|
|
108
|
+
sessions.delete(sessionId);
|
|
109
|
+
} else {
|
|
110
|
+
// Keep session alive but clear records to avoid double-counting
|
|
111
|
+
acc.records = [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function flushAll(reason: "periodic"): Promise<void> {
|
|
116
|
+
const ids = Array.from(sessions.keys());
|
|
117
|
+
await Promise.all(ids.map((id) => flushSession(id, reason)));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── public ─────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
registerHooks() {
|
|
124
|
+
// Capture actual cost from provider on every LLM completion
|
|
125
|
+
api.on("llm_output", async (event) => {
|
|
126
|
+
const tokensIn = event.usage?.input ?? 0;
|
|
127
|
+
const tokensOut = event.usage?.output ?? 0;
|
|
128
|
+
const cacheRead = event.usage?.cacheRead ?? 0;
|
|
129
|
+
const cacheWrite = event.usage?.cacheWrite ?? 0;
|
|
130
|
+
|
|
131
|
+
// Try multiple paths — OpenClaw may expose it under different field names
|
|
132
|
+
const thinkingTokens =
|
|
133
|
+
(event.usage as any)?.thinking ??
|
|
134
|
+
(event.usage as any)?.thoughtsTokenCount ??
|
|
135
|
+
(event.usage as any)?.reasoning ??
|
|
136
|
+
(event as any)?.rawResponse?.usageMetadata?.thoughtsTokenCount ??
|
|
137
|
+
0;
|
|
138
|
+
|
|
139
|
+
const cost = calculateCost({
|
|
140
|
+
model: event.model,
|
|
141
|
+
inputTokens: tokensIn,
|
|
142
|
+
outputTokens: tokensOut,
|
|
143
|
+
cacheReadTokens: cacheRead,
|
|
144
|
+
cacheWriteTokens: cacheWrite,
|
|
145
|
+
thinkingTokens,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const record: LlmRecord = {
|
|
149
|
+
runId: event.runId,
|
|
150
|
+
sessionId: event.sessionId,
|
|
151
|
+
provider: event.provider,
|
|
152
|
+
model: event.model,
|
|
153
|
+
tokensIn,
|
|
154
|
+
tokensOut,
|
|
155
|
+
cacheRead,
|
|
156
|
+
cacheWrite,
|
|
157
|
+
thinkingTokens,
|
|
158
|
+
costUsd: cost.totalCost,
|
|
159
|
+
costSource: cost.priceSource,
|
|
160
|
+
costBreakdown: cost.priceSource === "table" ? {
|
|
161
|
+
input: cost.inputCost,
|
|
162
|
+
output: cost.outputCost,
|
|
163
|
+
cacheRead: cost.cacheReadCost,
|
|
164
|
+
cacheWrite: cost.cacheWriteCost,
|
|
165
|
+
thinking: cost.thinkingCost,
|
|
166
|
+
} : undefined,
|
|
167
|
+
timestamp: Date.now(),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const acc = getOrCreateSession(event.sessionId);
|
|
171
|
+
acc.records.push(record);
|
|
172
|
+
|
|
173
|
+
if (config.debug) {
|
|
174
|
+
logger.info(
|
|
175
|
+
`[deepclaw] llm_output session=${event.sessionId} model=${event.model} ` +
|
|
176
|
+
`in=${tokensIn} out=${tokensOut} cacheRead=${cacheRead} thinking=${thinkingTokens} ` +
|
|
177
|
+
`cost=$${record.costUsd.toFixed(6)} source=${record.costSource}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Flush & close on session end
|
|
183
|
+
api.on("session_end", async (event) => {
|
|
184
|
+
await flushSession(event.sessionId, "session_end");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Start periodic flush timer
|
|
188
|
+
flushTimer = setInterval(() => {
|
|
189
|
+
flushAll("periodic").catch((err) => {
|
|
190
|
+
logger.warn(`[deepclaw] periodic flush error: ${String(err)}`);
|
|
191
|
+
});
|
|
192
|
+
}, config.flushIntervalMs);
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
async shutdown() {
|
|
196
|
+
if (flushTimer) clearInterval(flushTimer);
|
|
197
|
+
// Final flush of all open sessions
|
|
198
|
+
const ids = Array.from(sessions.keys());
|
|
199
|
+
await Promise.all(ids.map((id) => flushSession(id, "session_end")));
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|