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 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
+ [![CI](https://github.com/Digitizers/deepclaw-openclaw/actions/workflows/ci.yml/badge.svg)](https://github.com/Digitizers/deepclaw-openclaw/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+ [![OpenClaw Plugin](https://img.shields.io/badge/OpenClaw-plugin-111827.svg)](https://github.com/Digitizers/deepclaw-openclaw)
6
+ [![Category: Observability](https://img.shields.io/badge/category-observability-7c3aed.svg)](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
+ }