coderouter-cli 2.9.4__py3-none-any.whl → 2.11.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,575 @@
1
+ """v2.10-A: ``coderouter vscode-init`` — one-shot VSCode workspace scaffolder.
2
+
3
+ Writes ``.vscode/settings.json`` with ``terminal.integrated.env.*`` so a
4
+ Claude Code session launched from VSCode's integrated terminal
5
+ auto-points at CodeRouter. Optionally emits a direnv ``.envrc`` in the
6
+ same run.
7
+
8
+ The design contract:
9
+
10
+ * stdlib only — no new runtime deps (keeps the strict-5 rule in
11
+ ``pyproject.toml`` intact).
12
+ * Never overwrite unrelated keys in an existing ``settings.json``. Only
13
+ ``terminal.integrated.env.osx`` / ``.linux`` / ``.windows`` are
14
+ touched, and inside them only the CodeRouter-managed env vars.
15
+ * When an existing target value differs from the desired one, refuse
16
+ without ``--force`` and print a unified diff so the operator decides.
17
+ * ``--dry-run`` is byte-identical to the write path minus the actual
18
+ ``os.replace`` at the end.
19
+ * All writes are atomic (tmp file + ``os.replace``) so a partial write
20
+ cannot corrupt an existing ``settings.json``. This matters more here
21
+ than in most CodeRouter modules because the file we're editing is
22
+ workspace-critical: a broken JSON blob would silently break every
23
+ VSCode terminal env until a human debugged it.
24
+
25
+ Files touched (opt-in per flag):
26
+
27
+ * ``{target}/.vscode/settings.json`` — always
28
+ * ``{target}/.envrc`` — ``--with-envrc``
29
+
30
+ Non-Claude-Code extensions (Cline / Roo / Kilo / Continue) are handled
31
+ by the cheat sheet printed to stdout at end of run and the fuller
32
+ ``docs/guides/vscode.md`` — this CLI deliberately does NOT reach into
33
+ those extensions' settings, because each ships its own config schema
34
+ that changes with its own release cadence.
35
+
36
+ CLI plumbing lives in :mod:`coderouter.cli` (``_run_vscode_init``); this
37
+ module is import-clean so tests can exercise ``run_vscode_init``
38
+ directly without argparse in the way.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import json
44
+ import os
45
+ from dataclasses import dataclass
46
+ from difflib import unified_diff
47
+ from pathlib import Path
48
+
49
+ # v2.10-A: The README, quickstart, and every backend guide teach 8088 as
50
+ # the CodeRouter port. ``coderouter serve`` still defaults to 4000 (a
51
+ # pre-README default that predates the docs unification); vscode-init
52
+ # sides with the docs so that the settings.json it writes matches every
53
+ # tutorial the user is likely to be reading. Operators who deviate can
54
+ # pass ``--port`` explicitly.
55
+ DEFAULT_PORT = 8088
56
+
57
+ # Bogus token — CodeRouter's chat ingress does not validate ANTHROPIC_AUTH_TOKEN
58
+ # (real API keys are managed on the ``providers.yaml`` side). Using a
59
+ # fixed sentinel here means we never accidentally embed the operator's
60
+ # real token into a workspace file. (Not annotated with `noqa: S105`
61
+ # because this project doesn't enable that ruff rule — RUF100 would
62
+ # then flag the unused directive.)
63
+ DEFAULT_TOKEN = "dummy"
64
+
65
+ # VSCode reads terminal env from a separate key per host OS. Writing all
66
+ # three means the same ``.vscode/`` folder works whether the developer
67
+ # opens it on macOS, Linux, or WSL/Windows without a second scaffolder
68
+ # run. The keys are stable across VSCode versions (documented public
69
+ # API since 1.x).
70
+ _TERMINAL_ENV_KEYS: tuple[str, ...] = (
71
+ "terminal.integrated.env.osx",
72
+ "terminal.integrated.env.linux",
73
+ "terminal.integrated.env.windows",
74
+ )
75
+
76
+ # Env vars this scaffolder owns inside the terminal-env blocks. Anything
77
+ # NOT in this set that already exists under one of the terminal-env
78
+ # keys is preserved verbatim on merge (e.g. the user's PATH tweak).
79
+ _MANAGED_ENV_KEYS: tuple[str, ...] = (
80
+ "ANTHROPIC_BASE_URL",
81
+ "ANTHROPIC_AUTH_TOKEN",
82
+ "CODEROUTER_MODE",
83
+ )
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class FileOutcome:
88
+ """The outcome of touching (or planning to touch) one file.
89
+
90
+ ``action`` values:
91
+
92
+ * ``created`` — file didn't exist, we wrote it
93
+ * ``updated`` — file existed, we changed it
94
+ * ``unchanged`` — file already had the desired content
95
+ * ``conflict`` — existing content differs and ``--force`` was
96
+ not set; the file was NOT written
97
+ * ``skipped`` — hard error prevented processing (unparseable
98
+ JSON, wrong type, permission denied); NOT written
99
+ """
100
+
101
+ path: Path
102
+ action: str
103
+ diff: str = ""
104
+ reason: str = ""
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class VSCodeInitResult:
109
+ outcomes: list[FileOutcome]
110
+
111
+ @property
112
+ def has_conflicts(self) -> bool:
113
+ return any(o.action == "conflict" for o in self.outcomes)
114
+
115
+ @property
116
+ def has_skipped(self) -> bool:
117
+ return any(o.action == "skipped" for o in self.outcomes)
118
+
119
+
120
+ def run_vscode_init(
121
+ target: Path,
122
+ *,
123
+ port: int = DEFAULT_PORT,
124
+ profile: str | None = None,
125
+ with_envrc: bool = False,
126
+ dry_run: bool = False,
127
+ force: bool = False,
128
+ ) -> VSCodeInitResult:
129
+ """Scaffold VSCode workspace settings for CodeRouter.
130
+
131
+ Parameters
132
+ ----------
133
+ target:
134
+ Workspace root — the folder that VSCode opens. ``.vscode/`` and
135
+ (if ``with_envrc``) ``.envrc`` are placed under this directory.
136
+ Must already exist as a directory; we do not create the
137
+ workspace root itself.
138
+ port:
139
+ CodeRouter port for ``ANTHROPIC_BASE_URL``. Defaults to 8088
140
+ (docs-standard); override to match a non-default ``serve``.
141
+ profile:
142
+ Optional ``CODEROUTER_MODE`` value to preload. When set, the
143
+ VSCode terminal gets ``CODEROUTER_MODE=<profile>`` too.
144
+ with_envrc:
145
+ When true, also write ``.envrc`` (direnv). Idempotent and
146
+ conflict-aware just like ``settings.json``.
147
+ dry_run:
148
+ When true, compute all the outcomes and diffs but skip the
149
+ actual ``os.replace``. Guarantees byte-parity with the write
150
+ path — see the module docstring for the invariant.
151
+ force:
152
+ When true, existing conflicting values are overwritten. Without
153
+ this flag, a conflict is reported with a diff and the file is
154
+ left untouched.
155
+
156
+ Raises
157
+ ------
158
+ FileNotFoundError:
159
+ The target directory does not exist. All other issues (bad
160
+ JSON, permission denied, existing conflicts) are captured as
161
+ ``FileOutcome`` entries with the appropriate action.
162
+ """
163
+ target = Path(target).expanduser().resolve()
164
+ if not target.is_dir():
165
+ raise FileNotFoundError(f"target directory does not exist: {target}")
166
+
167
+ base_url = f"http://localhost:{port}"
168
+ outcomes: list[FileOutcome] = []
169
+
170
+ # 1. .vscode/settings.json (always).
171
+ outcomes.append(
172
+ _apply_settings_json(
173
+ target / ".vscode" / "settings.json",
174
+ base_url=base_url,
175
+ profile=profile,
176
+ force=force,
177
+ dry_run=dry_run,
178
+ )
179
+ )
180
+
181
+ # 2. .envrc (opt-in via --with-envrc).
182
+ if with_envrc:
183
+ outcomes.append(
184
+ _apply_envrc(
185
+ target / ".envrc",
186
+ base_url=base_url,
187
+ profile=profile,
188
+ force=force,
189
+ dry_run=dry_run,
190
+ )
191
+ )
192
+
193
+ return VSCodeInitResult(outcomes=outcomes)
194
+
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # settings.json
198
+ # ---------------------------------------------------------------------------
199
+
200
+
201
+ def _apply_settings_json(
202
+ path: Path,
203
+ *,
204
+ base_url: str,
205
+ profile: str | None,
206
+ force: bool,
207
+ dry_run: bool,
208
+ ) -> FileOutcome:
209
+ desired_env: dict[str, str] = {
210
+ "ANTHROPIC_BASE_URL": base_url,
211
+ "ANTHROPIC_AUTH_TOKEN": DEFAULT_TOKEN,
212
+ }
213
+ if profile:
214
+ desired_env["CODEROUTER_MODE"] = profile
215
+
216
+ old_text = ""
217
+ if path.exists():
218
+ try:
219
+ old_text = path.read_text(encoding="utf-8")
220
+ except OSError as exc:
221
+ return FileOutcome(
222
+ path=path,
223
+ action="skipped",
224
+ reason=f"cannot read existing file: {exc}",
225
+ )
226
+ try:
227
+ existing = json.loads(old_text) if old_text.strip() else {}
228
+ except json.JSONDecodeError as exc:
229
+ return FileOutcome(
230
+ path=path,
231
+ action="skipped",
232
+ reason=(
233
+ "existing settings.json is not valid JSON "
234
+ f"({exc.msg} at line {exc.lineno}). Fix or move it "
235
+ "aside and re-run."
236
+ ),
237
+ )
238
+ if not isinstance(existing, dict):
239
+ return FileOutcome(
240
+ path=path,
241
+ action="skipped",
242
+ reason=(
243
+ "existing settings.json is not a JSON object "
244
+ f"(got {type(existing).__name__}). Fix or move it "
245
+ "aside and re-run."
246
+ ),
247
+ )
248
+ else:
249
+ existing = {}
250
+
251
+ merged, conflicts = _merge_terminal_env(existing, desired_env, force=force)
252
+
253
+ if conflicts and not force:
254
+ # Report the diff between existing and what --force WOULD write
255
+ # so the operator can decide whether the change is desired.
256
+ force_merged, _ = _merge_terminal_env(existing, desired_env, force=True)
257
+ diff = _json_diff(existing, force_merged, path)
258
+ return FileOutcome(
259
+ path=path,
260
+ action="conflict",
261
+ diff=diff,
262
+ reason=(
263
+ "existing settings.json has different value(s) for "
264
+ f"{', '.join(sorted(conflicts))}. Re-run with --force to overwrite."
265
+ ),
266
+ )
267
+
268
+ new_text = _dump_json(merged)
269
+ if new_text == old_text:
270
+ return FileOutcome(path=path, action="unchanged")
271
+
272
+ diff = _text_diff(old_text, new_text, path)
273
+ if not dry_run:
274
+ try:
275
+ path.parent.mkdir(parents=True, exist_ok=True)
276
+ _atomic_write(path, new_text)
277
+ except OSError as exc:
278
+ return FileOutcome(
279
+ path=path,
280
+ action="skipped",
281
+ reason=f"write failed: {exc}",
282
+ diff=diff,
283
+ )
284
+
285
+ return FileOutcome(
286
+ path=path,
287
+ action="updated" if old_text else "created",
288
+ diff=diff,
289
+ )
290
+
291
+
292
+ def _merge_terminal_env(
293
+ existing: dict,
294
+ desired_env: dict[str, str],
295
+ *,
296
+ force: bool,
297
+ ) -> tuple[dict, set[str]]:
298
+ """Merge our env vars into a settings.json dict.
299
+
300
+ Preserves unrelated top-level keys (``editor.fontSize`` etc.) AND
301
+ unrelated env keys inside the three ``terminal.integrated.env.*``
302
+ blocks. When ``force`` is false, an existing conflicting value for
303
+ one of our managed keys is left in place and its dotted path
304
+ (e.g. ``terminal.integrated.env.osx.ANTHROPIC_BASE_URL``) is added
305
+ to the returned conflict set.
306
+
307
+ Returns
308
+ -------
309
+ merged:
310
+ The merged dict, safe to serialize with :func:`_dump_json`.
311
+ conflicts:
312
+ Set of dotted paths whose existing value differs from the
313
+ desired one and were NOT overwritten (because ``force=False``).
314
+ """
315
+ # Deep copy via json round-trip — the input dict may share nested
316
+ # references with the caller and we must not mutate it.
317
+ merged = json.loads(json.dumps(existing))
318
+ conflicts: set[str] = set()
319
+
320
+ for os_key in _TERMINAL_ENV_KEYS:
321
+ sub = merged.get(os_key)
322
+ if not isinstance(sub, dict):
323
+ # Overwrite any non-dict value at this key — VSCode would
324
+ # ignore it anyway. Preserve nothing since the previous
325
+ # value was already broken.
326
+ sub = {}
327
+
328
+ for env_key, env_val in desired_env.items():
329
+ if env_key in sub and sub[env_key] != env_val and not force:
330
+ conflicts.add(f"{os_key}.{env_key}")
331
+ # Leave sub[env_key] unchanged.
332
+ continue
333
+ sub[env_key] = env_val
334
+
335
+ merged[os_key] = sub
336
+
337
+ return merged, conflicts
338
+
339
+
340
+ # ---------------------------------------------------------------------------
341
+ # .envrc
342
+ # ---------------------------------------------------------------------------
343
+
344
+
345
+ def _apply_envrc(
346
+ path: Path,
347
+ *,
348
+ base_url: str,
349
+ profile: str | None,
350
+ force: bool,
351
+ dry_run: bool,
352
+ ) -> FileOutcome:
353
+ new_text = _render_envrc(base_url=base_url, profile=profile)
354
+ old_text = ""
355
+ if path.exists():
356
+ try:
357
+ old_text = path.read_text(encoding="utf-8")
358
+ except OSError as exc:
359
+ return FileOutcome(
360
+ path=path,
361
+ action="skipped",
362
+ reason=f"cannot read existing file: {exc}",
363
+ )
364
+
365
+ if new_text == old_text:
366
+ return FileOutcome(path=path, action="unchanged")
367
+
368
+ diff = _text_diff(old_text, new_text, path)
369
+
370
+ if old_text and not force:
371
+ return FileOutcome(
372
+ path=path,
373
+ action="conflict",
374
+ diff=diff,
375
+ reason=(
376
+ ".envrc already exists and its contents differ. "
377
+ "Re-run with --force to overwrite. "
378
+ "(Tip: keep secrets in a separate .envrc.local and "
379
+ "``source_env_if_exists`` from .envrc.)"
380
+ ),
381
+ )
382
+
383
+ if not dry_run:
384
+ try:
385
+ _atomic_write(path, new_text)
386
+ except OSError as exc:
387
+ return FileOutcome(
388
+ path=path,
389
+ action="skipped",
390
+ reason=f"write failed: {exc}",
391
+ diff=diff,
392
+ )
393
+
394
+ return FileOutcome(
395
+ path=path,
396
+ action="updated" if old_text else "created",
397
+ diff=diff,
398
+ )
399
+
400
+
401
+ def _render_envrc(*, base_url: str, profile: str | None) -> str:
402
+ """Build the .envrc contents. Kept split for test targeting."""
403
+ lines = [
404
+ "# Auto-generated by `coderouter vscode-init`. Safe to edit.",
405
+ "# direnv will export these when you `cd` into this directory.",
406
+ "# Run `direnv allow` once after generation.",
407
+ "",
408
+ f'export ANTHROPIC_BASE_URL="{base_url}"',
409
+ f'export ANTHROPIC_AUTH_TOKEN="{DEFAULT_TOKEN}"',
410
+ ]
411
+ if profile:
412
+ lines.append(f'export CODEROUTER_MODE="{profile}"')
413
+ lines.append("") # trailing newline
414
+ return "\n".join(lines)
415
+
416
+
417
+ # ---------------------------------------------------------------------------
418
+ # Rendering helpers
419
+ # ---------------------------------------------------------------------------
420
+
421
+
422
+ def format_result(result: VSCodeInitResult, *, dry_run: bool, port: int) -> str:
423
+ """Human-readable rendering of the outcomes for CLI stdout.
424
+
425
+ The output has three sections:
426
+
427
+ 1. Per-file outcome lines (with unified diffs where relevant).
428
+ 2. A cheat sheet for the extensions this scaffolder does NOT wire
429
+ (Cline / Roo / Kilo / Continue) — the user copy-pastes.
430
+ 3. A trailing hint about the fuller ``docs/guides/vscode.md``.
431
+
432
+ ``dry_run`` toggles the "(dry-run — nothing written)" reminder.
433
+ """
434
+ lines: list[str] = []
435
+ marker_for = {
436
+ "created": " new ",
437
+ "updated": " diff ",
438
+ "unchanged": " nop ",
439
+ "conflict": "CONFLICT",
440
+ "skipped": " SKIP ",
441
+ }
442
+ for o in result.outcomes:
443
+ marker = marker_for.get(o.action, f" {o.action} ")
444
+ lines.append(f"[{marker}] {o.path}")
445
+ if o.reason:
446
+ lines.append(f" → {o.reason}")
447
+ if o.diff and o.action in ("created", "updated", "conflict"):
448
+ lines.append("")
449
+ lines.extend(o.diff.rstrip("\n").splitlines())
450
+ lines.append("")
451
+
452
+ if dry_run:
453
+ lines.append("")
454
+ lines.append("(dry-run — no files were written. Re-run without --dry-run to apply.)")
455
+
456
+ lines.append("")
457
+ lines.append(_render_cheat_sheet(port))
458
+ return "\n".join(lines)
459
+
460
+
461
+ def _render_cheat_sheet(port: int) -> str:
462
+ """Emit the copy-paste snippets shown after every ``vscode-init`` run.
463
+
464
+ The Continue.dev snippet is built with :func:`json.dumps` rather
465
+ than hand-formatted string concatenation to guarantee the output
466
+ is syntactically valid JSON. An earlier hand-formatted version
467
+ accidentally leaked an extra ``}`` from mismatched f-string /
468
+ plain-string brace escaping — the JSON round-trip closes that
469
+ class of bug and the test in ``tests/test_vscode_init.py`` pins
470
+ the invariant.
471
+ """
472
+ continue_snippet = _render_continue_snippet(port)
473
+ return (
474
+ "─── Other extensions (copy/paste) ─────────────────────────────\n"
475
+ "\n"
476
+ " Cline / Roo Code / Kilo Code:\n"
477
+ " API Provider: OpenAI Compatible\n"
478
+ f" Base URL: http://localhost:{port}/v1\n"
479
+ " API Key: dummy (any non-empty value)\n"
480
+ " Model ID: anything (CodeRouter routes via default_profile)\n"
481
+ "\n"
482
+ " Continue.dev — add to the models array in ~/.continue/config.json:\n"
483
+ f"{continue_snippet}\n"
484
+ "\n"
485
+ "Full guide: docs/guides/vscode.md in the CodeRouter repo.\n"
486
+ )
487
+
488
+
489
+ def _render_continue_snippet(port: int) -> str:
490
+ """Return one indented pretty-printed JSON block for Continue.dev.
491
+
492
+ Split out from :func:`_render_cheat_sheet` so tests can
493
+ ``json.loads`` the exact bytes users copy-paste and verify the
494
+ snippet is valid.
495
+ """
496
+ model_entry: dict[str, str] = {
497
+ "title": "CodeRouter",
498
+ "provider": "openai",
499
+ "model": "any-id",
500
+ "apiBase": f"http://localhost:{port}/v1",
501
+ "apiKey": "dummy",
502
+ }
503
+ pretty = json.dumps(model_entry, indent=2, ensure_ascii=False)
504
+ # Indent every line by 4 spaces so it aligns under the "─── Other
505
+ # extensions" header exactly like the Cline block above.
506
+ return "\n".join(" " + line for line in pretty.splitlines())
507
+
508
+
509
+ def exit_code_for(result: VSCodeInitResult) -> int:
510
+ """Map the outcome set to a shell exit code.
511
+
512
+ * 0 — every file is clean or a no-op.
513
+ * 2 — at least one conflict (needs ``--force``); nothing was
514
+ written for the conflicting file(s), but other files may
515
+ have been.
516
+ * 1 — a hard error prevented processing (unparseable JSON, write
517
+ failure). Treated as more severe than a conflict.
518
+ """
519
+ if result.has_skipped:
520
+ return 1
521
+ if result.has_conflicts:
522
+ return 2
523
+ return 0
524
+
525
+
526
+ # ---------------------------------------------------------------------------
527
+ # Low-level I/O helpers
528
+ # ---------------------------------------------------------------------------
529
+
530
+
531
+ def _dump_json(obj: object) -> str:
532
+ """Deterministic pretty JSON for settings.json.
533
+
534
+ ``indent=2`` matches VSCode's own formatter default, ``ensure_ascii=False``
535
+ keeps CJK env values (e.g. a profile name) readable in the file.
536
+ A trailing newline is appended so POSIX tools don't complain.
537
+ """
538
+ return json.dumps(obj, indent=2, ensure_ascii=False) + "\n"
539
+
540
+
541
+ def _atomic_write(path: Path, text: str) -> None:
542
+ """Write ``text`` to ``path`` atomically (tmp + os.replace).
543
+
544
+ A partial write cannot corrupt the target file: the tmp file lives
545
+ on the same filesystem (same parent directory) so ``os.replace`` is
546
+ an atomic inode swap on POSIX. On Windows ``os.replace`` is also
547
+ atomic since Python 3.3.
548
+
549
+ The tmp file uses the parent + ``<name>.tmp`` pattern rather than
550
+ ``Path.with_suffix`` because ``.envrc`` has no stem/suffix in the
551
+ ``Path`` sense and ``with_suffix`` would silently misbehave.
552
+ """
553
+ tmp = path.parent / (path.name + ".tmp")
554
+ tmp.write_text(text, encoding="utf-8")
555
+ os.replace(tmp, path)
556
+
557
+
558
+ def _text_diff(old: str, new: str, path: Path) -> str:
559
+ """Unified diff between two text buffers, labelled by file name."""
560
+ return "".join(
561
+ unified_diff(
562
+ old.splitlines(keepends=True),
563
+ new.splitlines(keepends=True),
564
+ fromfile=f"a/{path.name}",
565
+ tofile=f"b/{path.name}",
566
+ n=3,
567
+ )
568
+ )
569
+
570
+
571
+ def _json_diff(old: dict, new: dict, path: Path) -> str:
572
+ """Diff between two JSON dicts, formatted as ``_dump_json`` would."""
573
+ old_text = _dump_json(old) if old else ""
574
+ new_text = _dump_json(new)
575
+ return _text_diff(old_text, new_text, path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.9.4
3
+ Version: 2.11.0
4
4
  Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
5
5
  Project-URL: Homepage, https://github.com/zephel01/CodeRouter
6
6
  Project-URL: Repository, https://github.com/zephel01/CodeRouter
@@ -132,6 +132,8 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
132
132
 
133
133
  これだけ。Claude Code はいつも通り動きますが、裏ではローカルの Ollama が答えています。
134
134
 
135
+ **VSCode の統合ターミナルから使う場合** は、環境変数の書き忘れを避けるため `coderouter vscode-init` が便利です。プロジェクトルートで 1 回叩くと `.vscode/settings.json` に `terminal.integrated.env.*` をマージ書き込みするので、以後 VSCode のターミナルで `claude` と打つだけで通ります。Cline / Roo Code / Continue.dev の設定コピペも含めて → [VSCode 連携ガイド](./docs/guides/vscode.md)
136
+
135
137
  ---
136
138
 
137
139
  ## 自分に必要?
@@ -222,6 +224,7 @@ providers:
222
224
  | **ログビューア** | 各プロセスの stdout/stderr をブラウザ内でリアルタイム確認 |
223
225
  | **provider 自動同期** (v2.7.4) | 起動したバックエンドを provider として自動登録(`launcher-llamacpp-8085` 等)。providers.yaml 無編集で `X-CodeRouter-Profile: launcher` からルーティング可能。メモリ内のみ・serve と同寿命 |
224
226
  | **モデル名パススルー** (v2.7.4) | `model: ""` の provider は `/v1/models` が上流のロード中モデル ID(gguf 名)をそのまま返す。gguf を差し替えても config 編集不要 — 外部ベンチからモデルを識別できる |
227
+ | **特化ビルドの切り替え** (v2.11.0) | `llama.cpp-cuda` / `-vulkan` / `-rocm` を `backends` に登録すると、起動ごとにどのビルドの `llama-server` を使うか選べる。ビルド別にデバイス検出・option_profiles・ベンチスイープが独立(書かなければ従来どおり) |
225
228
 
226
229
  ```yaml
227
230
  # providers.yaml に追記するだけで有効になる
@@ -281,6 +284,7 @@ providers:
281
284
  | 無料で回す | [無料枠ガイド](./docs/guides/free-tier-guide.md) |
282
285
  | llama.cpp / vllm を GUI で起動 | [Launcher ガイド](./docs/backends/launcher.md) |
283
286
  | 言語税を計測・回避する | [言語税ガイド](./docs/guides/language-tax.md) |
287
+ | VSCode / Cline / Continue から使う | [VSCode 連携ガイド](./docs/guides/vscode.md) |
284
288
  | 別の PC から安全に繋ぐ | [リモートアクセスガイド](./docs/guides/remote-access.md) |
285
289
  | 詰まった | [トラブルシューティング](./docs/guides/troubleshooting.md) |
286
290
  | 設計を知りたい | [アーキテクチャ詳細](./docs/concepts/architecture.md) |
@@ -1,6 +1,6 @@
1
1
  coderouter/__init__.py,sha256=ghdjPrLtnRzY8fyQ4CJZI1UJKADyNTLtA3G7se8H7Ns,696
2
2
  coderouter/__main__.py,sha256=-LCgxJnvgUV240HjQKv7ly-mn2NuKHpC4nCpvTHjeSU,130
3
- coderouter/cli.py,sha256=7zxt6riKPTpHGiDms9SVaPbe-n4ytfCrhIVtuPII-oU,30173
3
+ coderouter/cli.py,sha256=rEo-G9fyIBXnRWmVCHxWBlfzII2CWMcFvhRJW5h2oBU,34650
4
4
  coderouter/cli_stats.py,sha256=CCjzc1G4hTRHZ2gG1XhxhDpUkJnnl3NXbcbp1T18jpg,29894
5
5
  coderouter/cost.py,sha256=32h6uzb4nxh2eA5d2Hn3kD9yJbtis6CFDAbeIy5KRkM,7431
6
6
  coderouter/doctor.py,sha256=x0OqWUMGPzYaX2poWSH2UA90sEr4Z69-gVYLXYQFCFU,85483
@@ -10,13 +10,14 @@ coderouter/errors.py,sha256=Xmq67lheyw8iv3Ox39jh2c4tvNI5RcUR4QkoxVDN6l4,1130
10
10
  coderouter/gguf_introspect.py,sha256=KeE00CfbYRa4gSTrzuwC6zVuHi20IzLC4nUQa98BEXI,10387
11
11
  coderouter/hardware.py,sha256=gn3_9qbVcGRR81yKMn1lJE_8-YDRau0LxIH_M-f7pxE,8356
12
12
  coderouter/language_tax.py,sha256=LTbE3tIfoJuV2O3T0NixRKhzq_dEOTUuPEerJv2q9uk,9360
13
- coderouter/launcher_devices.py,sha256=SoGpzknRqvnmvg_IeXy1TkeE7aLc_MOFqLzXdd3CjVs,19275
14
- coderouter/launcher_speculative.py,sha256=kWaHNmhzBsbWuM_lRGDlbWkgJ1suuxBWQNTKhJOC-yg,12644
15
- coderouter/launcher_swap.py,sha256=W7qpdB-8Npr8yHGfx0N3SgZ2vDJ6BR9JuxW0I3TaZ-4,23783
13
+ coderouter/launcher_devices.py,sha256=_5R7dA8tHEhwLn9VkXccLEXPJREfpHu90nYd6-cm8Xs,28180
14
+ coderouter/launcher_speculative.py,sha256=h0RTGsD1Y56sJVywDJwybrT75_Ta3GF1Jz_B1d42aPg,12931
15
+ coderouter/launcher_swap.py,sha256=tMW5ZElDdf9T1VQGsUkvfLiSCdj_oXN_UtkV7DE8ySY,24175
16
16
  coderouter/logging.py,sha256=5rfi4_R4aoeeUYDc642dgs61wA0aBX7wvubyB-k3Uyo,58074
17
17
  coderouter/output_filters.py,sha256=0ry_rPiS_kC-FnHgaNVP6v7e6Al2djxzu9vBzZ8kEkE,25314
18
18
  coderouter/token_estimation.py,sha256=iz22vZEEW2P7uKLB2pYvPNpIbZGbgXRO5MtfkS_-9Sk,7531
19
19
  coderouter/token_estimation_accurate.py,sha256=GTfzrBVnvAGjeVzmzAeUdOYZvWZKLAxcxPpFiJGlzjk,4609
20
+ coderouter/vscode_init.py,sha256=ATQ1FzeQRAXfBBNcPwpEHRP4l4Yjn4hYgE-TjkcVvck,20034
20
21
  coderouter/adapters/__init__.py,sha256=7dIDSZ-FE_0iSqLSDc_lK1idRdLTKcM2hP9tCJipgPI,463
21
22
  coderouter/adapters/anthropic_native.py,sha256=CT9Hitun-c3z83YHT2zZXgwZpY3_t6eRnOfAmNud2aw,23610
22
23
  coderouter/adapters/base.py,sha256=ykKMaiIVVGr23oFFXilK2iP_YhI8-CtsEiJyFXaC9bE,10396
@@ -26,7 +27,7 @@ coderouter/config/__init__.py,sha256=FODEn74fN-qZnt4INPSHswqhOlEgpL6-_onxsitSx8g
26
27
  coderouter/config/capability_registry.py,sha256=RZgzHF54dXy0CCDocRCz2ee21qXGbbgTSKTkRI9iLL4,16936
27
28
  coderouter/config/env_file.py,sha256=CoMK27fuAXm-NtoLzXb8yN2E-wDFjHQuFwiIlmgTBQw,10356
28
29
  coderouter/config/loader.py,sha256=FUEe8m4Tnmj_aul0vSctD8vKvNW-oLRoMRbTpSKqSmc,4077
29
- coderouter/config/schemas.py,sha256=wHaOqbehppjtL-01eLh8DNmm0A3rZXdJ-cNbBcS2yM8,120984
30
+ coderouter/config/schemas.py,sha256=nPwaG4uIdhqoOs74nay4fPTNvEHxpsx7RhqFTZzBX-E,127468
30
31
  coderouter/data/__init__.py,sha256=uNyfD9jaCvTWsBAWtaw1Fr25OSxzv3psGMfBjT1z0Cc,328
31
32
  coderouter/data/model-capabilities.yaml,sha256=S9jt6SC6-3s2-icZ_n-a14iEMnc2yB1C2R6q-N_tZWQ,19309
32
33
  coderouter/guards/__init__.py,sha256=5qliYBqygvVPneej7nx0uSjxDKsz7t8VzvrDgVBJlvU,1170
@@ -44,7 +45,7 @@ coderouter/ingress/__init__.py,sha256=WQsCH2CGJCAhy0mS6GSEdeYZRkkQu2OHDsP4CJWTLu
44
45
  coderouter/ingress/anthropic_routes.py,sha256=1ThXzx8VgxRV7UCi3NF5ey7kbr0ADPKTXyQuYS5gK1w,27063
45
46
  coderouter/ingress/app.py,sha256=6e_qNlYZIyjLKhY20t0ZNL-x5Fv8R8oaIH_HCdxa9Ic,20921
46
47
  coderouter/ingress/dashboard_routes.py,sha256=1SqNNrVTfQiZcx5n2MvjAzWirittX0e61tCt-MKp4Ag,25617
47
- coderouter/ingress/launcher_routes.py,sha256=lU2EILVAi-lvHBHIxyixsMTRZlp-Rv7-2sw-eW4ccYg,119687
48
+ coderouter/ingress/launcher_routes.py,sha256=CGD_Uq8jz1qwFPQD-HXfHdB7-nD1rvPaHkpXzmTzB1w,133287
48
49
  coderouter/ingress/metrics_routes.py,sha256=M22dwOGn24P05Ge4W3c7d7mYytSGWjIR-pPSPOAiHJY,3965
49
50
  coderouter/ingress/openai_routes.py,sha256=TrugsQNJfNPgKKLzfT1aXzrs4emWYGE4XebEBuaZoWk,12505
50
51
  coderouter/metrics/__init__.py,sha256=7Es351DPS7yLM0yVF_F0eesmiD83n7Zzhie44chht38,1465
@@ -70,8 +71,8 @@ coderouter/translation/__init__.py,sha256=PYXN7XVEwpG1uC8RLy6fvnGbzEZhhrEuUapH8I
70
71
  coderouter/translation/anthropic.py,sha256=uEScq-icLn6ZXFWaxsKROv7feaq3zD0ZogjbTJ3Y7gQ,12578
71
72
  coderouter/translation/convert.py,sha256=VV4kpu4dvLmGBmJfQnCLY5ryy_AnQcV2NuwjqRtaR8Q,52633
72
73
  coderouter/translation/tool_repair.py,sha256=DGOKArM53yfVbIfiAveoul1vGq9NEKmjagf43Qh4IkM,57630
73
- coderouter_cli-2.9.4.dist-info/METADATA,sha256=E7EiSxxEYfkchQLvqlvdQzADFsnwbReGwi0Patjfk8I,15996
74
- coderouter_cli-2.9.4.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
75
- coderouter_cli-2.9.4.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
76
- coderouter_cli-2.9.4.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
77
- coderouter_cli-2.9.4.dist-info/RECORD,,
74
+ coderouter_cli-2.11.0.dist-info/METADATA,sha256=KIJ1syHgU0_eFQlBpZvaasysCEzQE5LLsITNQYP9x3o,16897
75
+ coderouter_cli-2.11.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
76
+ coderouter_cli-2.11.0.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
77
+ coderouter_cli-2.11.0.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
78
+ coderouter_cli-2.11.0.dist-info/RECORD,,