git-copilot-commit 0.5.2__tar.gz → 0.5.4__tar.gz
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.
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/PKG-INFO +1 -1
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/src/git_copilot_commit/cli.py +100 -13
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/src/git_copilot_commit/github_copilot.py +17 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/src/git_copilot_commit/split_commits.py +9 -3
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/tests/test_cli.py +112 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/tests/test_github_copilot_utils.py +31 -1
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/tests/test_split_commits.py +14 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/.github/dependabot.yml +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/.github/workflows/ci.yml +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/.gitignore +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/.justfile +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/.python-version +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/LICENSE +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/README.md +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/pyproject.toml +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/src/git_copilot_commit/__init__.py +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/src/git_copilot_commit/git.py +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/src/git_copilot_commit/prompts/commit-message-generator-prompt.md +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/src/git_copilot_commit/prompts/split-commit-planner-prompt.md +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/src/git_copilot_commit/py.typed +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/src/git_copilot_commit/settings.py +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/src/git_copilot_commit/version.py +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/tests/conftest.py +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/tests/test_git.py +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/tests/test_settings.py +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/uv.lock +0 -0
- {git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/vhs/demo.vhs +0 -0
|
@@ -268,7 +268,12 @@ def display_selected_model(model: github_copilot.CopilotModel) -> None:
|
|
|
268
268
|
console.print(f"[green]Using model:[/green] {model.id} ({', '.join(details)})")
|
|
269
269
|
|
|
270
270
|
|
|
271
|
-
def build_commit_message_prompt(
|
|
271
|
+
def build_commit_message_prompt(
|
|
272
|
+
status: GitStatus,
|
|
273
|
+
context: str = "",
|
|
274
|
+
*,
|
|
275
|
+
include_diff: bool = True,
|
|
276
|
+
) -> str:
|
|
272
277
|
"""Build the prompt used to generate a commit message."""
|
|
273
278
|
if not status.has_staged_changes:
|
|
274
279
|
console.print("[red]No staged changes to commit.[/red]")
|
|
@@ -277,10 +282,16 @@ def build_commit_message_prompt(status: GitStatus, context: str = "") -> str:
|
|
|
277
282
|
prompt_parts = [
|
|
278
283
|
"`git status`:\n",
|
|
279
284
|
f"```\n{status.get_porcelain_output()}\n```",
|
|
280
|
-
"\n\n`git diff --staged`:\n",
|
|
281
|
-
f"```\n{status.staged_diff}\n```",
|
|
282
285
|
]
|
|
283
286
|
|
|
287
|
+
if include_diff:
|
|
288
|
+
prompt_parts.extend(
|
|
289
|
+
[
|
|
290
|
+
"\n\n`git diff --staged`:\n",
|
|
291
|
+
f"```\n{status.staged_diff}\n```",
|
|
292
|
+
]
|
|
293
|
+
)
|
|
294
|
+
|
|
284
295
|
if context.strip():
|
|
285
296
|
prompt_parts.insert(0, f"User-provided context:\n\n{context.strip()}\n\n")
|
|
286
297
|
|
|
@@ -330,6 +341,30 @@ def generate_commit_message_for_prompt(
|
|
|
330
341
|
)
|
|
331
342
|
|
|
332
343
|
|
|
344
|
+
def should_retry_with_compact_prompt(exc: github_copilot.CopilotError) -> bool:
|
|
345
|
+
message_parts = [str(exc)]
|
|
346
|
+
if isinstance(exc, github_copilot.CopilotHttpError) and exc.detail:
|
|
347
|
+
message_parts.append(exc.detail)
|
|
348
|
+
|
|
349
|
+
haystack = " ".join(part.strip() for part in message_parts if part).lower()
|
|
350
|
+
indicators = (
|
|
351
|
+
"maximum context length",
|
|
352
|
+
"context_length_exceeded",
|
|
353
|
+
"context window",
|
|
354
|
+
"prompt is too long",
|
|
355
|
+
"input is too long",
|
|
356
|
+
"request is too large",
|
|
357
|
+
"too many tokens",
|
|
358
|
+
"token limit",
|
|
359
|
+
"max_prompt_tokens",
|
|
360
|
+
"max prompt tokens",
|
|
361
|
+
"input tokens",
|
|
362
|
+
"prompt tokens",
|
|
363
|
+
"prompt token count",
|
|
364
|
+
)
|
|
365
|
+
return any(indicator in haystack for indicator in indicators)
|
|
366
|
+
|
|
367
|
+
|
|
333
368
|
def generate_commit_message_for_status(
|
|
334
369
|
status: GitStatus,
|
|
335
370
|
model: str | None = None,
|
|
@@ -337,9 +372,27 @@ def generate_commit_message_for_status(
|
|
|
337
372
|
http_client_config: github_copilot.HttpClientConfig | None = None,
|
|
338
373
|
) -> str:
|
|
339
374
|
"""Generate a commit message for a staged status snapshot."""
|
|
340
|
-
|
|
375
|
+
full_prompt = build_commit_message_prompt(status, context=context)
|
|
376
|
+
try:
|
|
377
|
+
return generate_commit_message_for_prompt(
|
|
378
|
+
full_prompt,
|
|
379
|
+
model=model,
|
|
380
|
+
http_client_config=http_client_config,
|
|
381
|
+
)
|
|
382
|
+
except github_copilot.CopilotError as exc:
|
|
383
|
+
if not should_retry_with_compact_prompt(exc):
|
|
384
|
+
raise
|
|
385
|
+
|
|
386
|
+
console.print(
|
|
387
|
+
"[yellow]Staged diff exceeded the model context window; retrying with [bold]`git status`[/] only.[/yellow]"
|
|
388
|
+
)
|
|
389
|
+
fallback_prompt = build_commit_message_prompt(
|
|
390
|
+
status,
|
|
391
|
+
context=context,
|
|
392
|
+
include_diff=False,
|
|
393
|
+
)
|
|
341
394
|
return generate_commit_message_for_prompt(
|
|
342
|
-
|
|
395
|
+
fallback_prompt,
|
|
343
396
|
model=model,
|
|
344
397
|
http_client_config=http_client_config,
|
|
345
398
|
)
|
|
@@ -455,31 +508,65 @@ def request_split_commit_plan(
|
|
|
455
508
|
http_client_config: github_copilot.HttpClientConfig | None = None,
|
|
456
509
|
) -> SplitCommitPlan:
|
|
457
510
|
"""Request and validate a split-commit plan for the staged patch units."""
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
511
|
+
planner_system_prompt = load_named_prompt(SPLIT_COMMIT_PLANNER_PROMPT_FILENAME)
|
|
512
|
+
planner_prompt = build_split_plan_prompt(
|
|
513
|
+
status,
|
|
514
|
+
patch_units,
|
|
515
|
+
preferred_commits=preferred_commits,
|
|
516
|
+
context=context,
|
|
517
|
+
)
|
|
465
518
|
|
|
519
|
+
try:
|
|
466
520
|
with console.status(
|
|
467
521
|
"[yellow]Planning split commits from [bold]staged hunks[/] ...[/yellow]"
|
|
468
522
|
):
|
|
469
523
|
response = ask_copilot_with_system_prompt(
|
|
470
|
-
|
|
524
|
+
planner_system_prompt,
|
|
471
525
|
planner_prompt,
|
|
472
526
|
model=model,
|
|
473
527
|
http_client_config=http_client_config,
|
|
474
528
|
)
|
|
529
|
+
except github_copilot.CopilotError as exc:
|
|
530
|
+
if not should_retry_with_compact_prompt(exc):
|
|
531
|
+
print_copilot_error("Could not generate a split commit plan", exc)
|
|
532
|
+
raise typer.Exit(1)
|
|
533
|
+
|
|
534
|
+
console.print(
|
|
535
|
+
"[yellow]Staged patch units exceeded the model context window; retrying split planning with summaries only.[/yellow]"
|
|
536
|
+
)
|
|
537
|
+
else:
|
|
475
538
|
return parse_split_plan_response(
|
|
476
539
|
response,
|
|
477
540
|
patch_units,
|
|
478
541
|
)
|
|
542
|
+
|
|
543
|
+
compact_planner_prompt = build_split_plan_prompt(
|
|
544
|
+
status,
|
|
545
|
+
patch_units,
|
|
546
|
+
preferred_commits=preferred_commits,
|
|
547
|
+
context=context,
|
|
548
|
+
include_patches=False,
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
try:
|
|
552
|
+
with console.status(
|
|
553
|
+
"[yellow]Planning split commits from [bold]patch summaries[/] ...[/yellow]"
|
|
554
|
+
):
|
|
555
|
+
response = ask_copilot_with_system_prompt(
|
|
556
|
+
planner_system_prompt,
|
|
557
|
+
compact_planner_prompt,
|
|
558
|
+
model=model,
|
|
559
|
+
http_client_config=http_client_config,
|
|
560
|
+
)
|
|
479
561
|
except github_copilot.CopilotError as exc:
|
|
480
562
|
print_copilot_error("Could not generate a split commit plan", exc)
|
|
481
563
|
raise typer.Exit(1)
|
|
482
564
|
|
|
565
|
+
return parse_split_plan_response(
|
|
566
|
+
response,
|
|
567
|
+
patch_units,
|
|
568
|
+
)
|
|
569
|
+
|
|
483
570
|
|
|
484
571
|
def request_split_commit_messages(
|
|
485
572
|
plan: SplitCommitPlan,
|
{git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/src/git_copilot_commit/github_copilot.py
RENAMED
|
@@ -162,6 +162,7 @@ class CopilotModel:
|
|
|
162
162
|
name: str
|
|
163
163
|
vendor: str | None = None
|
|
164
164
|
family: str | None = None
|
|
165
|
+
max_context_window_tokens: int | None = None
|
|
165
166
|
supported_endpoints: tuple[str, ...] = ()
|
|
166
167
|
|
|
167
168
|
@classmethod
|
|
@@ -173,11 +174,18 @@ class CopilotModel:
|
|
|
173
174
|
supported_endpoints = payload.get("supported_endpoints")
|
|
174
175
|
|
|
175
176
|
family: str | None = None
|
|
177
|
+
max_context_window_tokens: int | None = None
|
|
176
178
|
if isinstance(capabilities, dict):
|
|
177
179
|
raw_family = capabilities.get("family")
|
|
178
180
|
if isinstance(raw_family, str) and raw_family:
|
|
179
181
|
family = raw_family
|
|
180
182
|
|
|
183
|
+
limits = capabilities.get("limits")
|
|
184
|
+
if isinstance(limits, dict):
|
|
185
|
+
raw_context_window = limits.get("max_context_window_tokens")
|
|
186
|
+
if isinstance(raw_context_window, int) and raw_context_window > 0:
|
|
187
|
+
max_context_window_tokens = raw_context_window
|
|
188
|
+
|
|
181
189
|
endpoints: list[str] = []
|
|
182
190
|
if isinstance(supported_endpoints, list):
|
|
183
191
|
for entry in supported_endpoints:
|
|
@@ -192,6 +200,7 @@ class CopilotModel:
|
|
|
192
200
|
name=name if isinstance(name, str) and name else model_id,
|
|
193
201
|
vendor=vendor if isinstance(vendor, str) and vendor else None,
|
|
194
202
|
family=family,
|
|
203
|
+
max_context_window_tokens=max_context_window_tokens,
|
|
195
204
|
supported_endpoints=tuple(endpoints),
|
|
196
205
|
)
|
|
197
206
|
|
|
@@ -891,6 +900,12 @@ def format_supported_endpoints(model: CopilotModel) -> str:
|
|
|
891
900
|
return "default"
|
|
892
901
|
|
|
893
902
|
|
|
903
|
+
def format_context_window(model: CopilotModel) -> str:
|
|
904
|
+
if model.max_context_window_tokens is None:
|
|
905
|
+
return "?"
|
|
906
|
+
return f"{model.max_context_window_tokens:,}"
|
|
907
|
+
|
|
908
|
+
|
|
894
909
|
def normalize_vendor_filter(value: str | None) -> str | None:
|
|
895
910
|
if value is None:
|
|
896
911
|
return None
|
|
@@ -1226,6 +1241,7 @@ def print_model_table(models: list[CopilotModel]) -> None:
|
|
|
1226
1241
|
table.add_column("#", justify="right", style="cyan")
|
|
1227
1242
|
table.add_column("Model", style="green")
|
|
1228
1243
|
table.add_column("Vendor", style="blue")
|
|
1244
|
+
table.add_column("Context", justify="right", style="bright_cyan")
|
|
1229
1245
|
table.add_column("Route", style="yellow")
|
|
1230
1246
|
table.add_column("Endpoints", style="magenta")
|
|
1231
1247
|
for index, model in enumerate(models, start=1):
|
|
@@ -1233,6 +1249,7 @@ def print_model_table(models: list[CopilotModel]) -> None:
|
|
|
1233
1249
|
str(index),
|
|
1234
1250
|
model.id,
|
|
1235
1251
|
model.vendor or "?",
|
|
1252
|
+
format_context_window(model),
|
|
1236
1253
|
infer_api_surface(model),
|
|
1237
1254
|
format_supported_endpoints(model),
|
|
1238
1255
|
)
|
{git_copilot_commit-0.5.2 → git_copilot_commit-0.5.4}/src/git_copilot_commit/split_commits.py
RENAMED
|
@@ -173,6 +173,7 @@ def build_split_plan_prompt(
|
|
|
173
173
|
*,
|
|
174
174
|
preferred_commits: int | None = None,
|
|
175
175
|
context: str = "",
|
|
176
|
+
include_patches: bool = True,
|
|
176
177
|
) -> str:
|
|
177
178
|
"""Build the user prompt used to request a split-commit plan."""
|
|
178
179
|
if not patch_units:
|
|
@@ -193,7 +194,7 @@ def build_split_plan_prompt(
|
|
|
193
194
|
"`git status`:",
|
|
194
195
|
f"```\n{status.get_porcelain_output()}\n```",
|
|
195
196
|
"",
|
|
196
|
-
"Patch units:",
|
|
197
|
+
"Patch units:" if include_patches else "Patch units (summaries only):",
|
|
197
198
|
]
|
|
198
199
|
)
|
|
199
200
|
|
|
@@ -204,10 +205,15 @@ def build_split_plan_prompt(
|
|
|
204
205
|
f"Path: {unit.path}",
|
|
205
206
|
f"Kind: {unit.kind}",
|
|
206
207
|
f"Summary: {unit.summary}",
|
|
207
|
-
"Patch:",
|
|
208
|
-
f"```diff\n{unit.patch}\n```",
|
|
209
208
|
]
|
|
210
209
|
)
|
|
210
|
+
if include_patches:
|
|
211
|
+
prompt_parts.extend(
|
|
212
|
+
[
|
|
213
|
+
"Patch:",
|
|
214
|
+
f"```diff\n{unit.patch}\n```",
|
|
215
|
+
]
|
|
216
|
+
)
|
|
211
217
|
|
|
212
218
|
return "\n".join(prompt_parts)
|
|
213
219
|
|
|
@@ -62,6 +62,19 @@ def test_build_commit_message_prompt_includes_context_status_and_diff() -> None:
|
|
|
62
62
|
assert "+print('hi')" in prompt
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
def test_build_commit_message_prompt_can_omit_diff() -> None:
|
|
66
|
+
status = make_status(
|
|
67
|
+
staged_diff="diff --git a/src/example.py b/src/example.py\n+print('hi')\n"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
prompt = build_commit_message_prompt(status, include_diff=False)
|
|
71
|
+
|
|
72
|
+
assert "`git status`" in prompt
|
|
73
|
+
assert "M src/example.py" in prompt
|
|
74
|
+
assert "`git diff --staged`" not in prompt
|
|
75
|
+
assert "+print('hi')" not in prompt
|
|
76
|
+
|
|
77
|
+
|
|
65
78
|
def test_build_commit_message_prompt_requires_staged_changes() -> None:
|
|
66
79
|
status = make_status(staged_diff=" \n")
|
|
67
80
|
|
|
@@ -94,6 +107,105 @@ def test_generate_commit_message_for_status_normalizes_model_prefix(
|
|
|
94
107
|
assert "diff --git a/src/example.py b/src/example.py" in rendered_prompt
|
|
95
108
|
|
|
96
109
|
|
|
110
|
+
def test_generate_commit_message_for_status_retries_without_diff_on_context_overflow(
|
|
111
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
112
|
+
) -> None:
|
|
113
|
+
status = make_status(
|
|
114
|
+
staged_diff="diff --git a/src/example.py b/src/example.py\n+print('hi')\n"
|
|
115
|
+
)
|
|
116
|
+
mock_print = Mock()
|
|
117
|
+
mock_ask = Mock(
|
|
118
|
+
side_effect=[
|
|
119
|
+
github_copilot.CopilotHttpError(
|
|
120
|
+
400,
|
|
121
|
+
"Bad Request",
|
|
122
|
+
"This model's maximum context length was exceeded.",
|
|
123
|
+
),
|
|
124
|
+
"feat: add example",
|
|
125
|
+
]
|
|
126
|
+
)
|
|
127
|
+
monkeypatch.setattr(cli.console, "print", mock_print)
|
|
128
|
+
monkeypatch.setattr(cli, "load_system_prompt", Mock(return_value="system prompt"))
|
|
129
|
+
monkeypatch.setattr(cli.github_copilot, "ask", mock_ask)
|
|
130
|
+
|
|
131
|
+
message = generate_commit_message_for_status(status)
|
|
132
|
+
|
|
133
|
+
assert message == "feat: add example"
|
|
134
|
+
assert mock_ask.call_count == 2
|
|
135
|
+
first_prompt = mock_ask.call_args_list[0].args[0]
|
|
136
|
+
second_prompt = mock_ask.call_args_list[1].args[0]
|
|
137
|
+
assert "`git diff --staged`" in first_prompt
|
|
138
|
+
assert "diff --git a/src/example.py b/src/example.py" in first_prompt
|
|
139
|
+
assert "`git diff --staged`" not in second_prompt
|
|
140
|
+
assert "diff --git a/src/example.py b/src/example.py" not in second_prompt
|
|
141
|
+
assert "M src/example.py" in second_prompt
|
|
142
|
+
mock_print.assert_called()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_request_split_commit_plan_retries_without_patches_on_context_overflow(
|
|
146
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
147
|
+
) -> None:
|
|
148
|
+
status = GitStatus(
|
|
149
|
+
files=[
|
|
150
|
+
GitFile(path="src/example.py", status=" ", staged_status="M"),
|
|
151
|
+
GitFile(path="README.md", status=" ", staged_status="A"),
|
|
152
|
+
],
|
|
153
|
+
staged_diff=(
|
|
154
|
+
"diff --git a/src/example.py b/src/example.py\n+print('hi')\n"
|
|
155
|
+
"diff --git a/README.md b/README.md\n+# hi\n"
|
|
156
|
+
),
|
|
157
|
+
unstaged_diff="",
|
|
158
|
+
)
|
|
159
|
+
patch_units = (
|
|
160
|
+
PatchUnit(
|
|
161
|
+
id="u1",
|
|
162
|
+
order=0,
|
|
163
|
+
path="src/example.py",
|
|
164
|
+
staged_status="M",
|
|
165
|
+
kind="hunk",
|
|
166
|
+
patch="diff --git a/src/example.py b/src/example.py\n+print('hi')\n",
|
|
167
|
+
summary="src/example.py hunk 1/1 @@ -1 +1 @@ (+1/-0)",
|
|
168
|
+
),
|
|
169
|
+
PatchUnit(
|
|
170
|
+
id="u2",
|
|
171
|
+
order=1,
|
|
172
|
+
path="README.md",
|
|
173
|
+
staged_status="A",
|
|
174
|
+
kind="new_file",
|
|
175
|
+
patch="diff --git a/README.md b/README.md\n+# hi\n",
|
|
176
|
+
summary="add README.md (+1/-0)",
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
mock_print = Mock()
|
|
180
|
+
mock_ask = Mock(
|
|
181
|
+
side_effect=[
|
|
182
|
+
github_copilot.CopilotHttpError(
|
|
183
|
+
400,
|
|
184
|
+
"Bad Request",
|
|
185
|
+
(
|
|
186
|
+
'{"error":{"message":"prompt token count of 1719062 exceeds '
|
|
187
|
+
'the limit of 128000","code":"model_max_prompt_tokens_exceeded"}}'
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
'{"commits":[{"unit_ids":["u1"]},{"unit_ids":["u2"]}]}',
|
|
191
|
+
]
|
|
192
|
+
)
|
|
193
|
+
monkeypatch.setattr(cli.console, "print", mock_print)
|
|
194
|
+
monkeypatch.setattr(cli, "load_named_prompt", Mock(return_value="system prompt"))
|
|
195
|
+
monkeypatch.setattr(cli, "ask_copilot_with_system_prompt", mock_ask)
|
|
196
|
+
|
|
197
|
+
plan = cli.request_split_commit_plan(status, patch_units)
|
|
198
|
+
|
|
199
|
+
assert [commit.unit_ids for commit in plan.commits] == [("u1",), ("u2",)]
|
|
200
|
+
assert mock_ask.call_count == 2
|
|
201
|
+
first_prompt = mock_ask.call_args_list[0].args[1]
|
|
202
|
+
second_prompt = mock_ask.call_args_list[1].args[1]
|
|
203
|
+
assert "```diff" in first_prompt
|
|
204
|
+
assert "```diff" not in second_prompt
|
|
205
|
+
assert "Patch units (summaries only):" in second_prompt
|
|
206
|
+
mock_print.assert_called()
|
|
207
|
+
|
|
208
|
+
|
|
97
209
|
def test_display_split_commit_plan_shows_files_not_hunk_summaries(
|
|
98
210
|
monkeypatch: pytest.MonkeyPatch,
|
|
99
211
|
) -> None:
|
|
@@ -15,6 +15,7 @@ def make_model(
|
|
|
15
15
|
*,
|
|
16
16
|
vendor: str | None = None,
|
|
17
17
|
family: str | None = None,
|
|
18
|
+
context_window_tokens: int | None = None,
|
|
18
19
|
endpoints: tuple[str, ...] = (),
|
|
19
20
|
) -> github_copilot.CopilotModel:
|
|
20
21
|
return github_copilot.CopilotModel(
|
|
@@ -22,6 +23,7 @@ def make_model(
|
|
|
22
23
|
name=model_id,
|
|
23
24
|
vendor=vendor,
|
|
24
25
|
family=family,
|
|
26
|
+
max_context_window_tokens=context_window_tokens,
|
|
25
27
|
supported_endpoints=endpoints,
|
|
26
28
|
)
|
|
27
29
|
|
|
@@ -136,11 +138,15 @@ def test_credentials_and_payload_parsers(monkeypatch: pytest.MonkeyPatch) -> Non
|
|
|
136
138
|
"id": "gpt-5.4",
|
|
137
139
|
"name": "GPT-5.4",
|
|
138
140
|
"vendor": "openai",
|
|
139
|
-
"capabilities": {
|
|
141
|
+
"capabilities": {
|
|
142
|
+
"family": "gpt-5",
|
|
143
|
+
"limits": {"max_context_window_tokens": 272000},
|
|
144
|
+
},
|
|
140
145
|
"supported_endpoints": ["/responses", "", 123],
|
|
141
146
|
}
|
|
142
147
|
)
|
|
143
148
|
assert model.family == "gpt-5"
|
|
149
|
+
assert model.max_context_window_tokens == 272000
|
|
144
150
|
assert model.supported_endpoints == ("/responses",)
|
|
145
151
|
|
|
146
152
|
with pytest.raises(github_copilot.CopilotError):
|
|
@@ -233,6 +239,7 @@ def test_infer_api_surface_and_vendor_filtering() -> None:
|
|
|
233
239
|
assert github_copilot.infer_api_surface(google_model) == "chat_completions"
|
|
234
240
|
assert github_copilot.format_supported_endpoints(chat_model) == "/chat/completions"
|
|
235
241
|
assert github_copilot.format_supported_endpoints(google_model) == "default"
|
|
242
|
+
assert github_copilot.format_context_window(gpt5_model) == "?"
|
|
236
243
|
|
|
237
244
|
assert github_copilot.normalize_vendor_filter(" Gemini ") == "google"
|
|
238
245
|
assert github_copilot.normalize_vendor_filter("claude") == "anthropic"
|
|
@@ -483,3 +490,26 @@ def test_render_model_selection_error_and_time_formatting(
|
|
|
483
490
|
assert github_copilot.format_relative_duration(-59) == "59s ago"
|
|
484
491
|
assert github_copilot.format_unix_timestamp(1_700_000_061).endswith("(in 1m 1s)")
|
|
485
492
|
assert github_copilot.format_unix_timestamp(10**20) == str(10**20)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def test_print_model_table_shows_context_window(
|
|
496
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
497
|
+
) -> None:
|
|
498
|
+
table_console = Console(record=True, width=140)
|
|
499
|
+
monkeypatch.setattr(github_copilot, "console", table_console)
|
|
500
|
+
|
|
501
|
+
github_copilot.print_model_table(
|
|
502
|
+
[
|
|
503
|
+
make_model(
|
|
504
|
+
"gpt-5.4",
|
|
505
|
+
vendor="openai",
|
|
506
|
+
context_window_tokens=272000,
|
|
507
|
+
endpoints=("/responses",),
|
|
508
|
+
)
|
|
509
|
+
]
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
rendered = table_console.export_text()
|
|
513
|
+
|
|
514
|
+
assert "Context" in rendered
|
|
515
|
+
assert "272,000" in rendered
|
|
@@ -99,6 +99,20 @@ def test_build_split_plan_prompt_includes_unit_details() -> None:
|
|
|
99
99
|
assert "Kind: new_file" in prompt
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
def test_build_split_plan_prompt_can_omit_raw_patches() -> None:
|
|
103
|
+
units = extract_patch_units(MULTI_FILE_DIFF)
|
|
104
|
+
|
|
105
|
+
prompt = build_split_plan_prompt(
|
|
106
|
+
make_status(),
|
|
107
|
+
units,
|
|
108
|
+
include_patches=False,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
assert "Patch units (summaries only):" in prompt
|
|
112
|
+
assert "Summary: src/app.py hunk 1/2" in prompt
|
|
113
|
+
assert "```diff" not in prompt
|
|
114
|
+
|
|
115
|
+
|
|
102
116
|
def test_build_split_plan_prompt_supports_preferred_commit_count() -> None:
|
|
103
117
|
units = extract_patch_units(MULTI_FILE_DIFF)
|
|
104
118
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|