@didim365/agent-cli-core 0.2.9 → 0.2.11
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/README.md +53 -43
- package/dist/src/config/costEstimation.d.ts +65 -0
- package/dist/src/config/costEstimation.js +133 -0
- package/dist/src/config/costEstimation.js.map +1 -0
- package/dist/src/core/loggingContentGenerator.d.ts +7 -1
- package/dist/src/core/loggingContentGenerator.js +81 -5
- package/dist/src/core/loggingContentGenerator.js.map +1 -1
- package/dist/src/core/recordingContentGenerator.d.ts +3 -0
- package/dist/src/core/recordingContentGenerator.js +6 -0
- package/dist/src/core/recordingContentGenerator.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +1 -1
- package/dist/src/generated/git-commit.js +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/output/stream-json-formatter.js +3 -0
- package/dist/src/output/stream-json-formatter.js.map +1 -1
- package/dist/src/output/types.d.ts +1 -0
- package/dist/src/providers/claude/adapter.js +19 -5
- package/dist/src/providers/claude/adapter.js.map +1 -1
- package/dist/src/providers/claude/converter.js +3 -1
- package/dist/src/providers/claude/converter.js.map +1 -1
- package/dist/src/providers/events.d.ts +11 -0
- package/dist/src/providers/events.js.map +1 -1
- package/dist/src/providers/openai/adapter.js +28 -5
- package/dist/src/providers/openai/adapter.js.map +1 -1
- package/dist/src/providers/rateLimitUtils.d.ts +43 -0
- package/dist/src/providers/rateLimitUtils.js +75 -0
- package/dist/src/providers/rateLimitUtils.js.map +1 -0
- package/dist/src/providers/telemetryBridge.js +2 -0
- package/dist/src/providers/telemetryBridge.js.map +1 -1
- package/dist/src/providers/types.d.ts +2 -0
- package/dist/src/providers/types.js.map +1 -1
- package/dist/src/telemetry/index.d.ts +2 -0
- package/dist/src/telemetry/index.js +1 -0
- package/dist/src/telemetry/index.js.map +1 -1
- package/dist/src/telemetry/loggers.d.ts +19 -0
- package/dist/src/telemetry/loggers.js +72 -0
- package/dist/src/telemetry/loggers.js.map +1 -1
- package/dist/src/telemetry/metrics.d.ts +2 -2
- package/dist/src/telemetry/metrics.js.map +1 -1
- package/dist/src/telemetry/providerQuotaService.d.ts +27 -0
- package/dist/src/telemetry/providerQuotaService.js +28 -0
- package/dist/src/telemetry/providerQuotaService.js.map +1 -0
- package/dist/src/telemetry/types.d.ts +15 -0
- package/dist/src/telemetry/types.js +2 -0
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/telemetry/uiTelemetry.d.ts +29 -0
- package/dist/src/telemetry/uiTelemetry.js +63 -2
- package/dist/src/telemetry/uiTelemetry.js.map +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Didim Agent CLI
|
|
2
2
|
|
|
3
|
-
[](https://github.com/google-gemini/gemini-cli/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/google-gemini/gemini-cli/actions/workflows/chained_e2e.yml)
|
|
5
5
|
[](https://www.npmjs.com/package/@didim365/agent-cli)
|
|
6
6
|
[](https://github.com/google-gemini/gemini-cli/blob/main/LICENSE)
|
|
7
|
-
[](https://codewiki.google/github.com/google-gemini/gemini-cli?utm_source=badge&utm_medium=github&utm_campaign=github.com/google-gemini/gemini-cli)
|
|
8
7
|
|
|
9
|
-

|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
providers directly into your terminal.
|
|
13
|
-
**OpenAI**, and **OpenAI-compatible** (vLLM,
|
|
14
|
-
|
|
10
|
+
Didim Agent CLI is an open-source AI agent that brings the power of multiple AI
|
|
11
|
+
providers directly into your terminal. Built on the Gemini CLI foundation, it
|
|
12
|
+
supports **Gemini**, **Claude**, **OpenAI**, and **OpenAI-compatible** (vLLM,
|
|
13
|
+
Ollama, LM Studio) endpoints through a unified **provider adapter
|
|
14
|
+
architecture**, giving you the most direct path from your prompt to your
|
|
15
|
+
preferred model.
|
|
15
16
|
|
|
16
|
-
Learn all about
|
|
17
|
+
Learn all about Didim Agent CLI in our [documentation](./docs/index.md).
|
|
17
18
|
|
|
18
|
-
## 🚀 Why
|
|
19
|
+
## 🚀 Why Didim Agent CLI?
|
|
19
20
|
|
|
20
|
-
- **🎯 Free tier**: 60 requests/min and 1,000 requests/day with personal Google
|
|
21
|
-
account.
|
|
22
21
|
- **🧠 Multi-provider support**: Use Gemini, Claude, OpenAI, or local models
|
|
23
|
-
(vLLM/Ollama) — switch providers and models with `/model`.
|
|
22
|
+
(vLLM/Ollama) — switch providers and models with `/model` or `/auth login`.
|
|
24
23
|
- **🔧 Built-in tools**: Google Search grounding, file operations, shell
|
|
25
|
-
commands, web fetching.
|
|
26
|
-
- **🔌 Extensible**: MCP (Model Context Protocol) support
|
|
27
|
-
|
|
24
|
+
commands, web fetching — all tools work across providers.
|
|
25
|
+
- **🔌 Extensible**: MCP (Model Context Protocol) support with deterministic
|
|
26
|
+
tool naming and sLM-compatible parameter normalization.
|
|
27
|
+
- **🤖 Sub-agent support**: Sub-agents work with all providers via the
|
|
28
|
+
provider-independent `llm*` pipeline.
|
|
28
29
|
- **💻 Terminal-first**: Designed for developers who live in the command line.
|
|
29
30
|
- **🛡️ Open source**: Apache 2.0 licensed.
|
|
30
31
|
|
|
@@ -129,7 +130,7 @@ npm install -g @didim365/agent-cli@nightly
|
|
|
129
130
|
[Google Search](https://ai.google.dev/gemini-api/docs/grounding) for real-time
|
|
130
131
|
information
|
|
131
132
|
- Conversation checkpointing to save and resume complex sessions
|
|
132
|
-
- Custom context files (
|
|
133
|
+
- Custom context files (AGENTS.md) to tailor behavior for your projects
|
|
133
134
|
|
|
134
135
|
### GitHub Integration
|
|
135
136
|
|
|
@@ -151,12 +152,16 @@ Choose the authentication method that best fits your needs. You can also use
|
|
|
151
152
|
`/auth login` inside the CLI to interactively select a provider and enter your
|
|
152
153
|
API key.
|
|
153
154
|
|
|
155
|
+
> **Note:** Both `DIDIM_*` and `GEMINI_*` environment variable prefixes are
|
|
156
|
+
> supported. The CLI uses a central `resolveEnv()` utility that checks `DIDIM_*`
|
|
157
|
+
> first, then falls back to `GEMINI_*` for backward compatibility.
|
|
158
|
+
|
|
154
159
|
### Option 1: Login with Google (Gemini)
|
|
155
160
|
|
|
156
161
|
**✨ Best for:** Individual developers and Gemini Code Assist license holders.
|
|
157
162
|
|
|
158
163
|
```bash
|
|
159
|
-
|
|
164
|
+
didim
|
|
160
165
|
# Select "Login with Google" and follow the browser authentication flow
|
|
161
166
|
```
|
|
162
167
|
|
|
@@ -164,7 +169,7 @@ For organization accounts, set your Google Cloud project first:
|
|
|
164
169
|
|
|
165
170
|
```bash
|
|
166
171
|
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
|
167
|
-
|
|
172
|
+
didim
|
|
168
173
|
```
|
|
169
174
|
|
|
170
175
|
### Option 2: Gemini API Key
|
|
@@ -173,7 +178,7 @@ gemini
|
|
|
173
178
|
|
|
174
179
|
```bash
|
|
175
180
|
export GEMINI_API_KEY="YOUR_API_KEY"
|
|
176
|
-
|
|
181
|
+
didim
|
|
177
182
|
```
|
|
178
183
|
|
|
179
184
|
### Option 3: Claude (Anthropic)
|
|
@@ -182,7 +187,7 @@ gemini
|
|
|
182
187
|
|
|
183
188
|
```bash
|
|
184
189
|
export ANTHROPIC_API_KEY="YOUR_API_KEY"
|
|
185
|
-
|
|
190
|
+
didim
|
|
186
191
|
```
|
|
187
192
|
|
|
188
193
|
### Option 4: OpenAI
|
|
@@ -191,7 +196,7 @@ gemini
|
|
|
191
196
|
|
|
192
197
|
```bash
|
|
193
198
|
export OPENAI_API_KEY="YOUR_API_KEY"
|
|
194
|
-
|
|
199
|
+
didim
|
|
195
200
|
```
|
|
196
201
|
|
|
197
202
|
### Option 5: Vertex AI
|
|
@@ -201,7 +206,7 @@ gemini
|
|
|
201
206
|
```bash
|
|
202
207
|
export GOOGLE_API_KEY="YOUR_API_KEY"
|
|
203
208
|
export GOOGLE_GENAI_USE_VERTEXAI=true
|
|
204
|
-
|
|
209
|
+
didim
|
|
205
210
|
```
|
|
206
211
|
|
|
207
212
|
### Option 6: OpenAI-compatible (vLLM, Ollama, LM Studio)
|
|
@@ -212,7 +217,7 @@ gemini
|
|
|
212
217
|
export ENABLE_MULTI_PROVIDER=true
|
|
213
218
|
export LLM_PROVIDER=openai-compatible
|
|
214
219
|
export LLM_BASE_URL="http://localhost:8000/v1"
|
|
215
|
-
|
|
220
|
+
didim
|
|
216
221
|
```
|
|
217
222
|
|
|
218
223
|
For detailed setup for each provider, see the
|
|
@@ -226,21 +231,21 @@ For detailed setup for each provider, see the
|
|
|
226
231
|
#### Start in current directory
|
|
227
232
|
|
|
228
233
|
```bash
|
|
229
|
-
|
|
234
|
+
didim
|
|
230
235
|
```
|
|
231
236
|
|
|
232
237
|
#### Include multiple directories
|
|
233
238
|
|
|
234
239
|
```bash
|
|
235
|
-
|
|
240
|
+
didim --include-directories ../lib,../docs
|
|
236
241
|
```
|
|
237
242
|
|
|
238
243
|
#### Use specific model
|
|
239
244
|
|
|
240
245
|
```bash
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
didim -m gemini-2.5-flash # Gemini
|
|
247
|
+
didim -m claude-sonnet-4-5-20250929 # Claude
|
|
248
|
+
didim -m gpt-4.1 # OpenAI
|
|
244
249
|
```
|
|
245
250
|
|
|
246
251
|
#### Non-interactive mode for scripts
|
|
@@ -248,21 +253,21 @@ gemini -m gpt-4.1 # OpenAI
|
|
|
248
253
|
Get a simple text response:
|
|
249
254
|
|
|
250
255
|
```bash
|
|
251
|
-
|
|
256
|
+
didim -p "Explain the architecture of this codebase"
|
|
252
257
|
```
|
|
253
258
|
|
|
254
259
|
For more advanced scripting, including how to parse JSON and handle errors, use
|
|
255
260
|
the `--output-format json` flag to get structured output:
|
|
256
261
|
|
|
257
262
|
```bash
|
|
258
|
-
|
|
263
|
+
didim -p "Explain the architecture of this codebase" --output-format json
|
|
259
264
|
```
|
|
260
265
|
|
|
261
266
|
For real-time event streaming (useful for monitoring long-running operations),
|
|
262
267
|
use `--output-format stream-json` to get newline-delimited JSON events:
|
|
263
268
|
|
|
264
269
|
```bash
|
|
265
|
-
|
|
270
|
+
didim -p "Run tests and deploy" --output-format stream-json
|
|
266
271
|
```
|
|
267
272
|
|
|
268
273
|
### Quick Examples
|
|
@@ -271,16 +276,16 @@ gemini -p "Run tests and deploy" --output-format stream-json
|
|
|
271
276
|
|
|
272
277
|
```bash
|
|
273
278
|
cd new-project/
|
|
274
|
-
|
|
279
|
+
didim
|
|
275
280
|
> Write me a Discord bot that answers questions using a FAQ.md file I will provide
|
|
276
281
|
```
|
|
277
282
|
|
|
278
283
|
#### Analyze existing code
|
|
279
284
|
|
|
280
285
|
```bash
|
|
281
|
-
git clone https://github.com/
|
|
282
|
-
cd
|
|
283
|
-
|
|
286
|
+
git clone https://github.com/user/project
|
|
287
|
+
cd project
|
|
288
|
+
didim
|
|
284
289
|
> Give me a summary of all of the changes that went in yesterday
|
|
285
290
|
```
|
|
286
291
|
|
|
@@ -303,8 +308,8 @@ gemini
|
|
|
303
308
|
(`/help`, `/chat`, etc).
|
|
304
309
|
- [**Custom Commands**](./docs/cli/custom-commands.md) - Create your own
|
|
305
310
|
reusable commands.
|
|
306
|
-
- [**Context Files (
|
|
307
|
-
context to
|
|
311
|
+
- [**Context Files (AGENTS.md)**](./docs/cli/gemini-md.md) - Provide persistent
|
|
312
|
+
context to the CLI.
|
|
308
313
|
- [**Checkpointing**](./docs/cli/checkpointing.md) - Save and resume
|
|
309
314
|
conversations.
|
|
310
315
|
- [**Token Caching**](./docs/cli/token-caching.md) - Optimize token usage.
|
|
@@ -352,7 +357,7 @@ export ENABLE_MULTI_PROVIDER=true
|
|
|
352
357
|
export LLM_PROVIDER=openai-compatible
|
|
353
358
|
export LLM_BASE_URL="http://localhost:8000/v1"
|
|
354
359
|
export LLM_MODEL="Qwen/Qwen2.5-7B-Instruct"
|
|
355
|
-
|
|
360
|
+
didim -m Qwen/Qwen2.5-7B-Instruct
|
|
356
361
|
```
|
|
357
362
|
|
|
358
363
|
### Troubleshooting & Support
|
|
@@ -364,8 +369,8 @@ gemini -m Qwen/Qwen2.5-7B-Instruct
|
|
|
364
369
|
|
|
365
370
|
### Using MCP Servers
|
|
366
371
|
|
|
367
|
-
Configure MCP servers in `~/.
|
|
368
|
-
custom tools:
|
|
372
|
+
Configure MCP servers in `~/.didim/settings.json` (or `~/.gemini/settings.json`
|
|
373
|
+
for backward compatibility) to extend the CLI with custom tools:
|
|
369
374
|
|
|
370
375
|
```text
|
|
371
376
|
> @github List my open pull requests
|
|
@@ -373,6 +378,11 @@ custom tools:
|
|
|
373
378
|
> @database Run a query to find inactive users
|
|
374
379
|
```
|
|
375
380
|
|
|
381
|
+
MCP tool naming is **deterministic** — tools are registered with consistent
|
|
382
|
+
names regardless of server discovery order. Tool parameters are automatically
|
|
383
|
+
normalized via schema-based coercion, with enhanced tolerance for sLM (small
|
|
384
|
+
Language Model) tool call formatting.
|
|
385
|
+
|
|
376
386
|
See the [MCP Server Integration guide](./docs/tools/mcp-server.md) for setup
|
|
377
387
|
instructions.
|
|
378
388
|
|
|
@@ -416,5 +426,5 @@ See the [Uninstall Guide](docs/cli/uninstall.md) for removal instructions.
|
|
|
416
426
|
---
|
|
417
427
|
|
|
418
428
|
<p align="center">
|
|
419
|
-
Built
|
|
429
|
+
Built on Gemini CLI by Google — extended by Didim365
|
|
420
430
|
</p>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Pricing information for a single model.
|
|
8
|
+
*
|
|
9
|
+
* All prices are in USD per 1 million tokens.
|
|
10
|
+
*/
|
|
11
|
+
export interface ModelPricing {
|
|
12
|
+
/** USD per 1M input tokens */
|
|
13
|
+
inputPerMToken: number;
|
|
14
|
+
/** USD per 1M output tokens */
|
|
15
|
+
outputPerMToken: number;
|
|
16
|
+
/** USD per 1M cached input tokens (optional — fallback to inputPerMToken) */
|
|
17
|
+
cachedPerMToken?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Static pricing table: provider → model → pricing.
|
|
21
|
+
*
|
|
22
|
+
* Prices sourced from official API pricing pages (as of 2026-02).
|
|
23
|
+
* Models not listed here will be treated as $0 (unknown pricing).
|
|
24
|
+
*/
|
|
25
|
+
export declare const MODEL_PRICING: Record<string, Record<string, ModelPricing>>;
|
|
26
|
+
/**
|
|
27
|
+
* Result of cost estimation across all models.
|
|
28
|
+
*/
|
|
29
|
+
export interface CostEstimate {
|
|
30
|
+
/** Total estimated cost in USD */
|
|
31
|
+
totalCost: number;
|
|
32
|
+
/** Cost breakdown per provider in USD */
|
|
33
|
+
byProvider: Record<string, number>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Token shape expected by estimateCost — subset of ModelMetrics.tokens.
|
|
37
|
+
*/
|
|
38
|
+
interface TokenInfo {
|
|
39
|
+
input: number;
|
|
40
|
+
cached: number;
|
|
41
|
+
candidates: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Estimate cost based on token usage and static pricing table.
|
|
45
|
+
*
|
|
46
|
+
* @param models - Record of composite-key → token info
|
|
47
|
+
* @param parseKey - Function to extract { provider, model } from composite key
|
|
48
|
+
* @returns CostEstimate with total and per-provider breakdown
|
|
49
|
+
*/
|
|
50
|
+
export declare function estimateCost(models: Record<string, {
|
|
51
|
+
tokens: TokenInfo;
|
|
52
|
+
}>, parseKey: (key: string) => {
|
|
53
|
+
provider: string;
|
|
54
|
+
model: string;
|
|
55
|
+
}): CostEstimate;
|
|
56
|
+
/**
|
|
57
|
+
* Format a CostEstimate into a human-readable string.
|
|
58
|
+
*
|
|
59
|
+
* - Zero: "$0.00"
|
|
60
|
+
* - Very small (< $0.01): "< $0.01"
|
|
61
|
+
* - Single provider: "$1.23"
|
|
62
|
+
* - Multi-provider: "$5.68 (Gemini $2.35 + Claude $3.33)"
|
|
63
|
+
*/
|
|
64
|
+
export declare function formatCostString(estimate: CostEstimate): string;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Static pricing table: provider → model → pricing.
|
|
8
|
+
*
|
|
9
|
+
* Prices sourced from official API pricing pages (as of 2026-02).
|
|
10
|
+
* Models not listed here will be treated as $0 (unknown pricing).
|
|
11
|
+
*/
|
|
12
|
+
export const MODEL_PRICING = {
|
|
13
|
+
gemini: {
|
|
14
|
+
'gemini-2.5-pro': {
|
|
15
|
+
inputPerMToken: 1.25,
|
|
16
|
+
outputPerMToken: 10.0,
|
|
17
|
+
cachedPerMToken: 0.125,
|
|
18
|
+
},
|
|
19
|
+
'gemini-2.5-flash': {
|
|
20
|
+
inputPerMToken: 0.3,
|
|
21
|
+
outputPerMToken: 2.5,
|
|
22
|
+
cachedPerMToken: 0.03,
|
|
23
|
+
},
|
|
24
|
+
'gemini-2.5-flash-lite': {
|
|
25
|
+
inputPerMToken: 0.1,
|
|
26
|
+
outputPerMToken: 0.4,
|
|
27
|
+
cachedPerMToken: 0.01,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
claude: {
|
|
31
|
+
'claude-opus-4-6': {
|
|
32
|
+
inputPerMToken: 5.0,
|
|
33
|
+
outputPerMToken: 25.0,
|
|
34
|
+
cachedPerMToken: 0.5,
|
|
35
|
+
},
|
|
36
|
+
'claude-sonnet-4-5-20250929': {
|
|
37
|
+
inputPerMToken: 3.0,
|
|
38
|
+
outputPerMToken: 15.0,
|
|
39
|
+
cachedPerMToken: 0.3,
|
|
40
|
+
},
|
|
41
|
+
'claude-haiku-4-5-20251001': {
|
|
42
|
+
inputPerMToken: 1.0,
|
|
43
|
+
outputPerMToken: 5.0,
|
|
44
|
+
cachedPerMToken: 0.1,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
openai: {
|
|
48
|
+
'gpt-5.2': {
|
|
49
|
+
inputPerMToken: 1.25,
|
|
50
|
+
outputPerMToken: 10.0,
|
|
51
|
+
cachedPerMToken: 0.625,
|
|
52
|
+
},
|
|
53
|
+
'gpt-5-mini': {
|
|
54
|
+
inputPerMToken: 0.4,
|
|
55
|
+
outputPerMToken: 1.6,
|
|
56
|
+
cachedPerMToken: 0.1,
|
|
57
|
+
},
|
|
58
|
+
'gpt-4.1': {
|
|
59
|
+
inputPerMToken: 2.0,
|
|
60
|
+
outputPerMToken: 8.0,
|
|
61
|
+
cachedPerMToken: 0.5,
|
|
62
|
+
},
|
|
63
|
+
'gpt-4.1-mini': {
|
|
64
|
+
inputPerMToken: 0.4,
|
|
65
|
+
outputPerMToken: 1.6,
|
|
66
|
+
cachedPerMToken: 0.1,
|
|
67
|
+
},
|
|
68
|
+
o3: {
|
|
69
|
+
inputPerMToken: 2.0,
|
|
70
|
+
outputPerMToken: 8.0,
|
|
71
|
+
},
|
|
72
|
+
'o4-mini': {
|
|
73
|
+
inputPerMToken: 1.1,
|
|
74
|
+
outputPerMToken: 4.4,
|
|
75
|
+
cachedPerMToken: 0.275,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const TOKENS_PER_MILLION = 1_000_000;
|
|
80
|
+
/**
|
|
81
|
+
* Estimate cost based on token usage and static pricing table.
|
|
82
|
+
*
|
|
83
|
+
* @param models - Record of composite-key → token info
|
|
84
|
+
* @param parseKey - Function to extract { provider, model } from composite key
|
|
85
|
+
* @returns CostEstimate with total and per-provider breakdown
|
|
86
|
+
*/
|
|
87
|
+
export function estimateCost(models, parseKey) {
|
|
88
|
+
const byProvider = {};
|
|
89
|
+
for (const [key, entry] of Object.entries(models)) {
|
|
90
|
+
const { provider, model } = parseKey(key);
|
|
91
|
+
const pricing = MODEL_PRICING[provider]?.[model];
|
|
92
|
+
if (!pricing)
|
|
93
|
+
continue;
|
|
94
|
+
const nonCachedInput = Math.max(0, entry.tokens.input - entry.tokens.cached);
|
|
95
|
+
const cachedRate = pricing.cachedPerMToken ?? pricing.inputPerMToken;
|
|
96
|
+
const inputCost = (nonCachedInput / TOKENS_PER_MILLION) * pricing.inputPerMToken;
|
|
97
|
+
const cachedCost = (entry.tokens.cached / TOKENS_PER_MILLION) * cachedRate;
|
|
98
|
+
const outputCost = (entry.tokens.candidates / TOKENS_PER_MILLION) * pricing.outputPerMToken;
|
|
99
|
+
const modelCost = inputCost + cachedCost + outputCost;
|
|
100
|
+
byProvider[provider] = (byProvider[provider] ?? 0) + modelCost;
|
|
101
|
+
}
|
|
102
|
+
const totalCost = Object.values(byProvider).reduce((sum, v) => sum + v, 0);
|
|
103
|
+
return { totalCost, byProvider };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Capitalize first letter of a string.
|
|
107
|
+
*/
|
|
108
|
+
function capitalize(s) {
|
|
109
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Format a CostEstimate into a human-readable string.
|
|
113
|
+
*
|
|
114
|
+
* - Zero: "$0.00"
|
|
115
|
+
* - Very small (< $0.01): "< $0.01"
|
|
116
|
+
* - Single provider: "$1.23"
|
|
117
|
+
* - Multi-provider: "$5.68 (Gemini $2.35 + Claude $3.33)"
|
|
118
|
+
*/
|
|
119
|
+
export function formatCostString(estimate) {
|
|
120
|
+
if (estimate.totalCost === 0)
|
|
121
|
+
return '$0.00';
|
|
122
|
+
if (estimate.totalCost > 0 && estimate.totalCost < 0.01)
|
|
123
|
+
return '< $0.01';
|
|
124
|
+
const providers = Object.entries(estimate.byProvider);
|
|
125
|
+
const totalStr = `$${estimate.totalCost.toFixed(2)}`;
|
|
126
|
+
if (providers.length <= 1)
|
|
127
|
+
return totalStr;
|
|
128
|
+
const breakdown = providers
|
|
129
|
+
.map(([name, cost]) => `${capitalize(name)} $${cost.toFixed(2)}`)
|
|
130
|
+
.join(' + ');
|
|
131
|
+
return `${totalStr} (${breakdown})`;
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=costEstimation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"costEstimation.js","sourceRoot":"","sources":["../../../src/config/costEstimation.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,13 +8,17 @@ import type { Config } from '../config/config.js';
|
|
|
8
8
|
import type { UserTierId } from '../code_assist/types.js';
|
|
9
9
|
import type { LlmGenerateRequest, LlmGenerateResponse, LlmTokenCount, GenerateOptions } from '../providers/types.js';
|
|
10
10
|
import type { LlmEventStream } from '../providers/events.js';
|
|
11
|
+
import type { ProviderQuotaService } from '../telemetry/providerQuotaService.js';
|
|
11
12
|
/**
|
|
12
13
|
* A decorator that wraps a ContentGenerator to add logging to API calls.
|
|
13
14
|
*/
|
|
14
15
|
export declare class LoggingContentGenerator implements ContentGenerator {
|
|
15
16
|
private readonly wrapped;
|
|
16
17
|
private readonly config;
|
|
17
|
-
|
|
18
|
+
private providerQuotaService?;
|
|
19
|
+
constructor(wrapped: ContentGenerator, config: Config, providerQuotaService?: ProviderQuotaService);
|
|
20
|
+
/** Set the ProviderQuotaService after construction (for late binding). */
|
|
21
|
+
setProviderQuotaService(service: ProviderQuotaService): void;
|
|
18
22
|
getWrapped(): ContentGenerator;
|
|
19
23
|
get userTier(): UserTierId | undefined;
|
|
20
24
|
get userTierName(): string | undefined;
|
|
@@ -31,5 +35,7 @@ export declare class LoggingContentGenerator implements ContentGenerator {
|
|
|
31
35
|
llmGenerateContent(request: LlmGenerateRequest, userPromptId: string, options?: GenerateOptions): Promise<LlmGenerateResponse>;
|
|
32
36
|
llmGenerateContentStream(request: LlmGenerateRequest, userPromptId: string, options?: GenerateOptions): LlmEventStream;
|
|
33
37
|
private llmLoggingStreamWrapper;
|
|
38
|
+
private _logLlmApiResponse;
|
|
39
|
+
private _logLlmApiError;
|
|
34
40
|
llmCountTokens(request: LlmGenerateRequest): Promise<LlmTokenCount>;
|
|
35
41
|
}
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
import { ApiRequestEvent, ApiResponseEvent, ApiErrorEvent, } from '../telemetry/types.js';
|
|
7
|
-
import { logApiError, logApiRequest, logApiResponse, } from '../telemetry/loggers.js';
|
|
7
|
+
import { logApiError, logApiRequest, logApiResponse, logProviderApiResponse, logProviderApiError, } from '../telemetry/loggers.js';
|
|
8
|
+
import { LlmEventType } from '../providers/events.js';
|
|
9
|
+
import { createProviderApiResponseEvent, createProviderApiErrorEvent, } from '../providers/telemetryBridge.js';
|
|
8
10
|
import { debugLogger } from '../utils/debugLogger.js';
|
|
9
11
|
import { CodeAssistServer } from '../code_assist/server.js';
|
|
10
12
|
import { toContents } from '../code_assist/converter.js';
|
|
@@ -16,9 +18,15 @@ import { runInDevTraceSpan } from '../telemetry/trace.js';
|
|
|
16
18
|
export class LoggingContentGenerator {
|
|
17
19
|
wrapped;
|
|
18
20
|
config;
|
|
19
|
-
|
|
21
|
+
providerQuotaService;
|
|
22
|
+
constructor(wrapped, config, providerQuotaService) {
|
|
20
23
|
this.wrapped = wrapped;
|
|
21
24
|
this.config = config;
|
|
25
|
+
this.providerQuotaService = providerQuotaService;
|
|
26
|
+
}
|
|
27
|
+
/** Set the ProviderQuotaService after construction (for late binding). */
|
|
28
|
+
setProviderQuotaService(service) {
|
|
29
|
+
this.providerQuotaService = service;
|
|
22
30
|
}
|
|
23
31
|
getWrapped() {
|
|
24
32
|
return this.wrapped;
|
|
@@ -208,16 +216,23 @@ export class LoggingContentGenerator {
|
|
|
208
216
|
throw new Error('Wrapped generator does not support provider-independent API');
|
|
209
217
|
}
|
|
210
218
|
const startTime = Date.now();
|
|
219
|
+
const provider = this.wrapped.providerName ?? 'unknown';
|
|
211
220
|
debugLogger.debug(`[LLM] generateContent model=${request.model} promptId=${userPromptId}`);
|
|
212
221
|
try {
|
|
213
222
|
const response = await this.wrapped.llmGenerateContent(request, userPromptId, options);
|
|
214
223
|
const durationMs = Date.now() - startTime;
|
|
215
224
|
debugLogger.debug(`[LLM] generateContent completed in ${durationMs}ms`);
|
|
225
|
+
this._logLlmApiResponse(request.model, durationMs, userPromptId, provider, response.usage);
|
|
226
|
+
// Bridge rate-limit data to ProviderQuotaService (non-stream path)
|
|
227
|
+
if (response.rateLimits && this.providerQuotaService) {
|
|
228
|
+
this.providerQuotaService.update(provider, response.rateLimits);
|
|
229
|
+
}
|
|
216
230
|
return response;
|
|
217
231
|
}
|
|
218
232
|
catch (error) {
|
|
219
233
|
const durationMs = Date.now() - startTime;
|
|
220
234
|
debugLogger.debug(`[LLM] generateContent error after ${durationMs}ms: ${error}`);
|
|
235
|
+
this._logLlmApiError(request.model, durationMs, userPromptId, provider, error);
|
|
221
236
|
throw error;
|
|
222
237
|
}
|
|
223
238
|
}
|
|
@@ -226,24 +241,85 @@ export class LoggingContentGenerator {
|
|
|
226
241
|
throw new Error('Wrapped generator does not support provider-independent API');
|
|
227
242
|
}
|
|
228
243
|
debugLogger.debug(`[LLM] generateContentStream model=${request.model} promptId=${userPromptId}`);
|
|
244
|
+
const startTime = Date.now();
|
|
229
245
|
const stream = this.wrapped.llmGenerateContentStream(request, userPromptId, options);
|
|
230
|
-
return this.llmLoggingStreamWrapper(stream, request.model, userPromptId);
|
|
246
|
+
return this.llmLoggingStreamWrapper(stream, request.model, userPromptId, startTime);
|
|
231
247
|
}
|
|
232
|
-
async *llmLoggingStreamWrapper(stream, model, userPromptId) {
|
|
233
|
-
const
|
|
248
|
+
async *llmLoggingStreamWrapper(stream, model, userPromptId, startTime) {
|
|
249
|
+
const provider = this.wrapped.providerName ?? 'unknown';
|
|
250
|
+
let collectedUsage;
|
|
251
|
+
let hasError = false;
|
|
234
252
|
try {
|
|
235
253
|
for await (const event of stream) {
|
|
254
|
+
// Collect usage from MessageEnd or Finished event.
|
|
255
|
+
// OpenAI puts usage in MessageEnd; Claude puts usage in Finished.
|
|
256
|
+
if ((event.type === LlmEventType.MessageEnd ||
|
|
257
|
+
event.type === LlmEventType.Finished) &&
|
|
258
|
+
event.usage) {
|
|
259
|
+
collectedUsage = event.usage;
|
|
260
|
+
}
|
|
261
|
+
// Bridge rate-limit data to ProviderQuotaService
|
|
262
|
+
if (event.type === LlmEventType.MessageEnd) {
|
|
263
|
+
const rateLimits = event
|
|
264
|
+
.rateLimits;
|
|
265
|
+
if (rateLimits && this.providerQuotaService) {
|
|
266
|
+
this.providerQuotaService.update(provider, rateLimits);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Detect Error events (yielded, not thrown)
|
|
270
|
+
if (event.type === LlmEventType.Error) {
|
|
271
|
+
hasError = true;
|
|
272
|
+
const durationMs = Date.now() - startTime;
|
|
273
|
+
this._logLlmApiError(model, durationMs, userPromptId, provider, event.error);
|
|
274
|
+
}
|
|
236
275
|
yield event;
|
|
237
276
|
}
|
|
238
277
|
const durationMs = Date.now() - startTime;
|
|
239
278
|
debugLogger.debug(`[LLM] generateContentStream completed in ${durationMs}ms`);
|
|
279
|
+
// Log response telemetry only if no error event was encountered
|
|
280
|
+
if (!hasError) {
|
|
281
|
+
this._logLlmApiResponse(model, durationMs, userPromptId, provider, collectedUsage);
|
|
282
|
+
}
|
|
240
283
|
}
|
|
241
284
|
catch (error) {
|
|
242
285
|
const durationMs = Date.now() - startTime;
|
|
243
286
|
debugLogger.debug(`[LLM] generateContentStream error after ${durationMs}ms model=${model} promptId=${userPromptId}: ${error}`);
|
|
287
|
+
// Skip error telemetry if:
|
|
288
|
+
// - Error was already logged via yielded Error event (prevents double recording)
|
|
289
|
+
// - User cancelled the operation (AbortError is not an API failure)
|
|
290
|
+
const isAbort = error instanceof Error && error.name === 'AbortError';
|
|
291
|
+
if (!hasError && !isAbort) {
|
|
292
|
+
this._logLlmApiError(model, durationMs, userPromptId, provider, error);
|
|
293
|
+
}
|
|
244
294
|
throw error;
|
|
245
295
|
}
|
|
246
296
|
}
|
|
297
|
+
_logLlmApiResponse(model, durationMs, promptId, provider, usage) {
|
|
298
|
+
const defaultUsage = {
|
|
299
|
+
promptTokens: 0,
|
|
300
|
+
completionTokens: 0,
|
|
301
|
+
totalTokens: 0,
|
|
302
|
+
};
|
|
303
|
+
const event = createProviderApiResponseEvent({
|
|
304
|
+
model,
|
|
305
|
+
durationMs,
|
|
306
|
+
promptId,
|
|
307
|
+
usage: usage ?? defaultUsage,
|
|
308
|
+
provider,
|
|
309
|
+
});
|
|
310
|
+
logProviderApiResponse(this.config, event);
|
|
311
|
+
}
|
|
312
|
+
_logLlmApiError(model, durationMs, promptId, provider, error) {
|
|
313
|
+
const errorMsg = error instanceof Error ? error.message : String(error ?? 'Unknown error');
|
|
314
|
+
const event = createProviderApiErrorEvent({
|
|
315
|
+
model,
|
|
316
|
+
error: errorMsg,
|
|
317
|
+
durationMs,
|
|
318
|
+
promptId,
|
|
319
|
+
provider,
|
|
320
|
+
});
|
|
321
|
+
logProviderApiError(this.config, event);
|
|
322
|
+
}
|
|
247
323
|
async llmCountTokens(request) {
|
|
248
324
|
if (!this.wrapped.llmCountTokens) {
|
|
249
325
|
throw new Error('Wrapped generator does not support provider-independent API');
|