copilot-session-usage 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,282 @@
1
+ """Click CLI entry point for copilot-session-usage."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from copilot_session_usage._internal import core, vscode
11
+
12
+ CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
13
+
14
+
15
+ def _resolve_agent(agent: str) -> str:
16
+ """Validate agent choice and return it."""
17
+ if agent == "cli":
18
+ raise click.ClickException(
19
+ "Copilot-CLI session discovery is not yet implemented. "
20
+ "Use --agent vscode (the default)."
21
+ )
22
+ return agent
23
+
24
+
25
+ @click.group(context_settings=CONTEXT_SETTINGS)
26
+ @click.option(
27
+ "--workspace-storage",
28
+ "workspace_storage",
29
+ metavar="PATH",
30
+ help=(
31
+ "Override workspaceStorage directory (auto-detected by default). "
32
+ "Required on WSL2 when VS Code runs on the Windows host."
33
+ ),
34
+ )
35
+ @click.option(
36
+ "--agent",
37
+ "agent",
38
+ type=click.Choice(("vscode", "cli")),
39
+ default="vscode",
40
+ show_default=True,
41
+ help="Provider to use for session discovery. 'cli' is not yet implemented.",
42
+ )
43
+ @click.pass_context
44
+ def cli(
45
+ ctx: click.Context,
46
+ workspace_storage: str | None,
47
+ agent: str,
48
+ ) -> None:
49
+ """Extract VS Code Copilot session cost KPIs from local debug logs.
50
+
51
+ Reads JSONL debug logs written by the VS Code Copilot Chat extension to
52
+ compute per-session token counts, estimated USD spend, model breakdowns,
53
+ duration, and subagent attribution.
54
+
55
+ Sessions are auto-discovered from the VS Code workspaceStorage directory
56
+ (override with --workspace-storage). Use the subcommands below to analyze
57
+ individual sessions, the latest session, or batches.
58
+
59
+ Output is controlled by --detail (minimal / compact / full) and
60
+ --format (table / json / detailed). Key capabilities include per-model
61
+ pricing with cache-hit discounts, threshold-aware tier switching for
62
+ long-context models, multi-model session handling, subagent cost
63
+ attribution, and cross-platform support (macOS, Linux, Windows, WSL2).
64
+ """
65
+ ctx.ensure_object(dict)
66
+ ctx.obj["workspace_storage"] = workspace_storage
67
+ ctx.obj["agent"] = _resolve_agent(agent)
68
+
69
+
70
+ @cli.command()
71
+ @core.analysis_options
72
+ @click.argument("log_dir", metavar="PATH")
73
+ def analyze(
74
+ log_dir: str,
75
+ detail: str,
76
+ format_: str,
77
+ output_path: str | None,
78
+ ) -> None:
79
+ """Analyze a single session by its debug-log directory PATH.
80
+
81
+ PATH is typically the VS Code Copilot session debug log directory —
82
+ this is the fastest path, no discovery needed.
83
+ """
84
+ session_dir = Path(log_dir)
85
+ if not session_dir.exists():
86
+ msg = f"log directory not found: {session_dir}"
87
+ raise click.ClickException(msg)
88
+ pricing = core.load_pricing()
89
+ result = core.analyze_session(session_dir, pricing)
90
+ detail = core.resolve_detail(detail, format_)
91
+ out_path = Path(output_path) if output_path else None
92
+ core.emit(core.shape_session(result, detail), core.normalize_format(format_), out_path)
93
+
94
+
95
+ @cli.command()
96
+ @core.analysis_options
97
+ @click.option(
98
+ "--workspace", metavar="PATH", help="Only consider sessions from this workspace folder."
99
+ )
100
+ @click.pass_context
101
+ def latest(
102
+ ctx: click.Context,
103
+ workspace: str | None,
104
+ detail: str,
105
+ format_: str,
106
+ output_path: str | None,
107
+ ) -> None:
108
+ """Analyze the most recently modified session across all workspaces."""
109
+ ws_roots = vscode.resolve_ws_roots(ctx.obj.get("workspace_storage"))
110
+ session_dir = vscode.find_latest_session_dir(ws_roots, workspace_filter=workspace)
111
+ if not session_dir:
112
+ msg = "no session debug logs found in workspace storage."
113
+ raise click.ClickException(msg)
114
+ pricing = core.load_pricing()
115
+ result = core.analyze_session(session_dir, pricing)
116
+ detail = core.resolve_detail(detail, format_)
117
+ out_path = Path(output_path) if output_path else None
118
+ core.emit(
119
+ core.shape_session(result, detail),
120
+ core.normalize_format(format_),
121
+ out_path,
122
+ )
123
+
124
+
125
+ @cli.command(name="find")
126
+ @core.analysis_options
127
+ @click.option(
128
+ "--workspace",
129
+ metavar="PATH",
130
+ help="Only consider sessions from this workspace folder.",
131
+ )
132
+ @click.argument("title")
133
+ @click.pass_context
134
+ def find_by_title(
135
+ ctx: click.Context,
136
+ title: str,
137
+ workspace: str | None,
138
+ detail: str,
139
+ format_: str,
140
+ output_path: str | None,
141
+ ) -> None:
142
+ """Find and analyze a session by TITLE (case-insensitive substring match).
143
+
144
+ If more than one session matches, candidates are printed and the command
145
+ exits with an error — re-run with `id <SESSION_ID>` to pick one.
146
+ """
147
+ ws_roots = vscode.resolve_ws_roots(ctx.obj.get("workspace_storage"))
148
+ matches = vscode.find_sessions_by_title(title, ws_roots)
149
+ if workspace:
150
+ matches = [m for m in matches if workspace in m.get("workspace_folder", "")]
151
+ if not matches:
152
+ msg = f"no sessions found matching title: {title!r}"
153
+ raise click.ClickException(msg)
154
+ if len(matches) > 1:
155
+ click.echo(f"Multiple sessions match {title!r}:", err=True)
156
+ for m in matches[:10]:
157
+ ts = core.ts_to_iso(m.get("created_ms")) or "unknown"
158
+ click.echo(f" {ts} {m['title']!r} (id: {m['session_id']})", err=True)
159
+ click.echo("Re-run with: copilot-session-usage id <SESSION_ID>", err=True)
160
+ sys.exit(1)
161
+ match = matches[0]
162
+ session_dir = Path(match["debug_log_dir"])
163
+ if not session_dir.exists():
164
+ msg = f"debug logs not present at: {session_dir}"
165
+ raise click.ClickException(msg)
166
+ pricing = core.load_pricing()
167
+ result = core.analyze_session(session_dir, pricing)
168
+ result["title"] = match.get("title") or result.get("title")
169
+ detail = core.resolve_detail(detail, format_)
170
+ out_path = Path(output_path) if output_path else None
171
+ core.emit(
172
+ core.shape_session(result, detail),
173
+ core.normalize_format(format_),
174
+ out_path,
175
+ )
176
+
177
+
178
+ @cli.command(name="id")
179
+ @core.analysis_options
180
+ @click.argument("session_id")
181
+ @click.pass_context
182
+ def analyze_by_id(
183
+ ctx: click.Context,
184
+ session_id: str,
185
+ detail: str,
186
+ format_: str,
187
+ output_path: str | None,
188
+ ) -> None:
189
+ """Analyze a session by its exact SESSION_ID (UUID)."""
190
+ ws_roots = vscode.resolve_ws_roots(ctx.obj.get("workspace_storage"))
191
+ session_dir = vscode.find_session_dir_by_id(session_id, ws_roots)
192
+ if not session_dir:
193
+ msg = f"no debug logs found for session ID: {session_id}"
194
+ raise click.ClickException(msg)
195
+ pricing = core.load_pricing()
196
+ result = core.analyze_session(session_dir, pricing)
197
+ detail = core.resolve_detail(detail, format_)
198
+ out_path = Path(output_path) if output_path else None
199
+ core.emit(core.shape_session(result, detail), core.normalize_format(format_), out_path)
200
+
201
+
202
+ @cli.command(name="list")
203
+ @core.format_option
204
+ @core.output_option
205
+ @click.option(
206
+ "--limit",
207
+ type=int,
208
+ default=20,
209
+ show_default=True,
210
+ help="Max sessions to return.",
211
+ )
212
+ @click.option(
213
+ "--since", metavar="DATE", help="Only sessions created after DATE (YYYY-MM-DD or ISO 8601)."
214
+ )
215
+ @click.option(
216
+ "--workspace", metavar="PATH", help="Only consider sessions from this workspace folder."
217
+ )
218
+ @click.pass_context
219
+ def list_sessions(
220
+ ctx: click.Context,
221
+ limit: int,
222
+ since: str | None,
223
+ workspace: str | None,
224
+ format_: str,
225
+ output_path: str | None,
226
+ ) -> None:
227
+ """List recent sessions (metadata only — no cost analysis)."""
228
+ ws_roots = vscode.resolve_ws_roots(ctx.obj.get("workspace_storage"))
229
+ since_ms = core.parse_since_to_ms(since) if since else None
230
+ sessions = vscode.list_recent_sessions(
231
+ ws_roots, limit=limit, since_ms=since_ms, workspace_filter=workspace
232
+ )
233
+ out_path = Path(output_path) if output_path else None
234
+ core.emit(sessions, core.normalize_format(format_), out_path)
235
+
236
+
237
+ @cli.command()
238
+ @core.analysis_options
239
+ @click.option(
240
+ "--since", metavar="DATE", help="Only sessions created after DATE (YYYY-MM-DD or ISO 8601)."
241
+ )
242
+ @click.option(
243
+ "--workspace", metavar="PATH", help="Only consider sessions from this workspace folder."
244
+ )
245
+ @click.argument("count", type=int, metavar="N")
246
+ @click.pass_context
247
+ def batch(
248
+ ctx: click.Context,
249
+ count: int,
250
+ since: str | None,
251
+ workspace: str | None,
252
+ detail: str,
253
+ format_: str,
254
+ output_path: str | None,
255
+ ) -> None:
256
+ """Analyze the N most recent sessions in one invocation.
257
+
258
+ Always returns {"summary": {...}, "sessions": [...]}: a pre-computed
259
+ aggregate across all N sessions plus a per-session array shaped by
260
+ --detail. Much faster than N separate `id` invocations.
261
+ """
262
+ ws_roots = vscode.resolve_ws_roots(ctx.obj.get("workspace_storage"))
263
+ since_ms = core.parse_since_to_ms(since) if since else None
264
+ sessions = vscode.list_recent_sessions(
265
+ ws_roots,
266
+ limit=count,
267
+ since_ms=since_ms,
268
+ workspace_filter=workspace,
269
+ require_logs=True,
270
+ )
271
+ pricing = core.load_pricing()
272
+ results: list[dict] = []
273
+ for session in sessions:
274
+ session_dir = Path(session["debug_log_dir"])
275
+ if not session_dir.exists():
276
+ continue
277
+ result = core.analyze_session(session_dir, pricing)
278
+ result["title"] = session.get("title") or result.get("title")
279
+ results.append(result)
280
+ detail = core.resolve_detail(detail, format_)
281
+ out_path = Path(output_path) if output_path else None
282
+ core.emit(core.shape_batch(results, detail), core.normalize_format(format_), out_path)
File without changes
@@ -0,0 +1,22 @@
1
+ # Custom model pricing for models not included in the GitHub Copilot plan.
2
+ # Prices are per 1 million tokens.
3
+ #
4
+ # Use this file to define pricing for models that appear in debug logs
5
+ # but are not part of the standard Copilot model catalog.
6
+ #
7
+ # Column keys:
8
+ # - model: The model name as it appears in the debug log.
9
+ # - provider: The model provider (optional, defaults to "custom").
10
+ # - input: Input token price per 1M tokens.
11
+ # - cached_input: Cached input token price per 1M tokens.
12
+ # - output: Output token price per 1M tokens.
13
+ # - notes: Optional notes about the model.
14
+
15
+ - model: 'Kimi-K2.6-azure'
16
+ provider: custom
17
+ input: $0.00
18
+ cached_input: $0.00
19
+ output: $0.00
20
+ notes: >
21
+ Azure-hosted Kimi K2.6 model. Pricing not tracked in Copilot plan;
22
+ set to $0 as a placeholder. Update if actual per-token pricing becomes available.
@@ -0,0 +1,7 @@
1
+ {
2
+ "_captured": "2026-07-01T13:17:03.382420+00:00",
3
+ "_source": "https://raw.githubusercontent.com/github/docs/main/data/tables/copilot/models-and-pricing.yml",
4
+ "_yaml_path": "references/models-and-pricing.yml",
5
+ "model_count": 26,
6
+ "checksum": "0d398b5cf45bf9ff"
7
+ }
@@ -0,0 +1,269 @@
1
+ # This file defines per-token pricing for AI models available in GitHub Copilot.
2
+ # Prices are per 1 million tokens.
3
+ #
4
+ # Please keep this list sorted by provider, then alphabetically by model name.
5
+ # Providers in this order: OpenAI, Anthropic, Google, xAI, Fine-tuned (GitHub).
6
+ #
7
+ # Column keys:
8
+ # - model: The model name.
9
+ # - provider: The model provider (openai, anthropic, google, xai, github).
10
+ # - release_status: "GA" or "Public preview".
11
+ # - category: The model category or task area.
12
+ # - input: Input token price per 1M tokens.
13
+ # - cached_input: Cached input token price per 1M tokens.
14
+ # - output: Output token price per 1M tokens.
15
+ # - threshold: Input token threshold for pricing tier (OpenAI and Google only).
16
+ # - tier: Pricing tier label (OpenAI and Google only).
17
+ # - cache_write: Cache write price per 1M tokens (Anthropic only).
18
+ # - notes: Optional notes about the model.
19
+
20
+ # OpenAI
21
+ - model: 'GPT-5 mini'
22
+ provider: openai
23
+ release_status: GA
24
+ category: Lightweight
25
+ threshold: Not applicable
26
+ tier: Default
27
+ input: $0.25
28
+ cached_input: $0.025
29
+ output: $2.00
30
+
31
+ - model: GPT-5.3-Codex
32
+ provider: openai
33
+ release_status: GA
34
+ category: Powerful
35
+ threshold: Not applicable
36
+ tier: Default
37
+ input: $1.75
38
+ cached_input: $0.175
39
+ output: $14.00
40
+
41
+ - model: 'GPT-5.4'
42
+ provider: openai
43
+ release_status: GA
44
+ category: Versatile
45
+ threshold: '≤ 272K'
46
+ tier: Default
47
+ input: $2.50
48
+ cached_input: $0.25
49
+ output: $15.00
50
+
51
+ - model: GPT-5.4
52
+ provider: openai
53
+ release_status: GA
54
+ category: Versatile
55
+ threshold: '> 272K'
56
+ tier: Long context
57
+ input: $5.00
58
+ cached_input: $0.50
59
+ output: $22.50
60
+
61
+ - model: GPT-5.4 mini
62
+ provider: openai
63
+ release_status: GA
64
+ category: Lightweight
65
+ threshold: Not applicable
66
+ tier: Default
67
+ input: $0.75
68
+ cached_input: $0.075
69
+ output: $4.50
70
+
71
+ - model: GPT-5.4 nano
72
+ provider: openai
73
+ release_status: GA
74
+ category: Lightweight
75
+ threshold: Not applicable
76
+ tier: Default
77
+ input: $0.20
78
+ cached_input: $0.02
79
+ output: $1.25
80
+
81
+ - model: GPT-5.5
82
+ provider: openai
83
+ release_status: GA
84
+ category: Powerful
85
+ threshold: '≤ 272K'
86
+ tier: Default
87
+ input: $5.00
88
+ cached_input: $0.50
89
+ output: $30.00
90
+
91
+ - model: GPT-5.5
92
+ provider: openai
93
+ release_status: GA
94
+ category: Powerful
95
+ threshold: '> 272K'
96
+ tier: 'Long context'
97
+ input: $10.00
98
+ cached_input: $1.00
99
+ output: $45.00
100
+
101
+ # Anthropic
102
+ - model: Claude Haiku 4.5
103
+ provider: anthropic
104
+ release_status: GA
105
+ category: Versatile
106
+ input: $1.00
107
+ cached_input: $0.10
108
+ output: $5.00
109
+ cache_write: $1.25
110
+
111
+ - model: Claude Sonnet 4
112
+ provider: anthropic
113
+ release_status: GA
114
+ category: Versatile
115
+ input: $3.00
116
+ cached_input: $0.30
117
+ output: $15.00
118
+ cache_write: $3.75
119
+
120
+ - model: Claude Sonnet 4.5
121
+ provider: anthropic
122
+ release_status: GA
123
+ category: Versatile
124
+ input: $3.00
125
+ cached_input: $0.30
126
+ output: $15.00
127
+ cache_write: $3.75
128
+
129
+ - model: Claude Sonnet 4.6
130
+ provider: anthropic
131
+ release_status: GA
132
+ category: Versatile
133
+ input: $3.00
134
+ cached_input: $0.30
135
+ output: $15.00
136
+ cache_write: $3.75
137
+
138
+ - model: Claude Opus 4.5
139
+ provider: anthropic
140
+ release_status: GA
141
+ category: Powerful
142
+ input: $5.00
143
+ cached_input: $0.50
144
+ output: $25.00
145
+ cache_write: $6.25
146
+
147
+ - model: Claude Opus 4.6
148
+ provider: anthropic
149
+ release_status: GA
150
+ category: Powerful
151
+ input: $5.00
152
+ cached_input: $0.50
153
+ output: $25.00
154
+ cache_write: $6.25
155
+
156
+ - model: Claude Opus 4.7
157
+ provider: anthropic
158
+ release_status: GA
159
+ category: Powerful
160
+ input: $5.00
161
+ cached_input: $0.50
162
+ output: $25.00
163
+ cache_write: $6.25
164
+
165
+ - model: Claude Opus 4.8
166
+ provider: anthropic
167
+ release_status: GA
168
+ category: Powerful
169
+ input: $5.00
170
+ cached_input: $0.50
171
+ output: $25.00
172
+ cache_write: $6.25
173
+
174
+ - model: Claude Sonnet 5[^sonnet-5-promo]
175
+ provider: anthropic
176
+ release_status: GA
177
+ category: Versatile
178
+ input: $2.00
179
+ cached_input: $0.20
180
+ output: $10.00
181
+ cache_write: $2.50
182
+
183
+ - model: Claude Opus 4.8 (fast mode) (preview)
184
+ provider: anthropic
185
+ release_status: GA
186
+ category: Powerful
187
+ input: $10.00
188
+ cached_input: $1.00
189
+ output: $50.00
190
+ cache_write: $12.50
191
+
192
+ - model: Claude Fable 5
193
+ provider: anthropic
194
+ release_status: GA
195
+ category: Powerful
196
+ input: $10.00
197
+ cached_input: $1.00
198
+ output: $50.00
199
+ cache_write: $12.50
200
+
201
+ # Google
202
+ - model: 'Gemini 2.5 Pro'
203
+ provider: google
204
+ release_status: GA
205
+ category: Powerful
206
+ threshold: 'Not applicable'
207
+ tier: 'Default'
208
+ input: $1.25
209
+ cached_input: $0.125
210
+ output: $10.00
211
+
212
+ - model: 'Gemini 3 Flash'
213
+ provider: google
214
+ release_status: Public preview
215
+ category: Lightweight
216
+ threshold: 'Not applicable'
217
+ tier: 'Default'
218
+ input: $0.50
219
+ cached_input: $0.05
220
+ output: $3.00
221
+
222
+ - model: 'Gemini 3.1 Pro'
223
+ provider: google
224
+ release_status: Public preview
225
+ category: Powerful
226
+ threshold: '≤ 200K'
227
+ tier: 'Default'
228
+ input: $2.00
229
+ cached_input: $0.20
230
+ output: $12.00
231
+
232
+ - model: 'Gemini 3.1 Pro'
233
+ provider: google
234
+ release_status: Public preview
235
+ category: Powerful
236
+ threshold: '> 200K'
237
+ tier: 'Long context'
238
+ input: $4.00
239
+ cached_input: $0.40
240
+ output: $18.00
241
+
242
+ - model: 'Gemini 3.5 Flash'
243
+ provider: google
244
+ release_status: GA
245
+ category: Lightweight
246
+ threshold: 'Not applicable'
247
+ tier: 'Default'
248
+ input: $1.50
249
+ cached_input: $0.15
250
+ output: $9.00
251
+
252
+ # Microsoft
253
+ - model: 'MAI-Code-1-Flash'
254
+ provider: microsoft
255
+ release_status: GA
256
+ category: Lightweight
257
+ input: $0.75
258
+ cached_input: $0.075
259
+ output: $4.50
260
+
261
+ # Fine-tuned (GitHub)
262
+ - model: 'Raptor mini'
263
+ provider: github
264
+ release_status: GA
265
+ category: Versatile
266
+ input: $0.25
267
+ cached_input: $0.025
268
+ output: $2.00
269
+ notes: Uses GPT-5 mini pricing