coderouter-cli 2.10.0__py3-none-any.whl → 2.11.1__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.
- coderouter/adapters/anthropic_native.py +23 -5
- coderouter/cli.py +49 -0
- coderouter/config/loader.py +65 -0
- coderouter/config/schemas.py +228 -12
- coderouter/doctor.py +98 -19
- coderouter/doctor_apply.py +72 -3
- coderouter/gguf_introspect.py +132 -25
- coderouter/guards/drift_detection.py +13 -1
- coderouter/ingress/launcher_routes.py +289 -22
- coderouter/launcher_devices.py +200 -4
- coderouter/launcher_speculative.py +5 -1
- coderouter/launcher_swap.py +11 -2
- coderouter/routing/fallback.py +14 -2
- coderouter/translation/anthropic.py +14 -1
- coderouter/translation/convert.py +7 -0
- coderouter/translation/tool_repair.py +193 -76
- coderouter/vscode_init.py +4 -2
- {coderouter_cli-2.10.0.dist-info → coderouter_cli-2.11.1.dist-info}/METADATA +2 -1
- {coderouter_cli-2.10.0.dist-info → coderouter_cli-2.11.1.dist-info}/RECORD +22 -22
- {coderouter_cli-2.10.0.dist-info → coderouter_cli-2.11.1.dist-info}/WHEEL +0 -0
- {coderouter_cli-2.10.0.dist-info → coderouter_cli-2.11.1.dist-info}/entry_points.txt +0 -0
- {coderouter_cli-2.10.0.dist-info → coderouter_cli-2.11.1.dist-info}/licenses/LICENSE +0 -0
coderouter/launcher_devices.py
CHANGED
|
@@ -23,13 +23,14 @@ import socket
|
|
|
23
23
|
import subprocess # controlled: fixed argv, no shell
|
|
24
24
|
import threading
|
|
25
25
|
import time
|
|
26
|
-
from collections.abc import Callable, Sequence
|
|
26
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
27
27
|
from dataclasses import dataclass, field
|
|
28
28
|
from enum import StrEnum
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
from typing import Any
|
|
31
31
|
|
|
32
32
|
__all__ = [
|
|
33
|
+
"KNOWN_BASE_BACKENDS",
|
|
33
34
|
"DeviceProbe",
|
|
34
35
|
"DeviceSelection",
|
|
35
36
|
"LlamaDevice",
|
|
@@ -37,21 +38,140 @@ __all__ = [
|
|
|
37
38
|
"SweepState",
|
|
38
39
|
"SweepStep",
|
|
39
40
|
"backend_of",
|
|
41
|
+
"base_backend",
|
|
40
42
|
"build_auto_sweep_configs",
|
|
43
|
+
"build_cross_variant_sweep_configs",
|
|
41
44
|
"build_sweep_steps",
|
|
42
45
|
"detect_llama_devices",
|
|
46
|
+
"foreign_device_ids",
|
|
43
47
|
"group_by_backend",
|
|
44
48
|
"is_port_free",
|
|
49
|
+
"is_valid_backend_name",
|
|
50
|
+
"is_variant",
|
|
45
51
|
"load_latest_results",
|
|
46
52
|
"parse_list_devices",
|
|
47
53
|
"render_bench_command",
|
|
48
54
|
"reset_device_cache",
|
|
55
|
+
"resolve_option_profiles",
|
|
49
56
|
"selectable_devices",
|
|
50
57
|
"suggest_tensor_split",
|
|
51
58
|
"summarize_results",
|
|
59
|
+
"variant_of",
|
|
52
60
|
]
|
|
53
61
|
|
|
54
62
|
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# 2.0 バックエンド名の正規化 (基底名 + バリアント)
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
# llama.cpp を CUDA / Vulkan / ROCm 向けに個別ビルドしている環境では、同じ
|
|
68
|
+
# "llama.cpp" でもビルドごとに実行ファイルも列挙されるデバイスも違う。これを
|
|
69
|
+
# 設定・UI・API の全面で扱うため、バックエンド名に "<基底名>-<バリアント>"
|
|
70
|
+
# 形式を認める:
|
|
71
|
+
#
|
|
72
|
+
# llama.cpp 基底名 (素のビルド / PATH の llama-server)
|
|
73
|
+
# llama.cpp-cuda build-cuda/bin/llama-server
|
|
74
|
+
# llama.cpp-vulkan build-vulkan/bin/llama-server
|
|
75
|
+
# llama.cpp-rocm build-rocm/bin/llama-server
|
|
76
|
+
#
|
|
77
|
+
# ★重要: バックエンド名は実行ファイルの選択だけでなく「どのフラグ体系か」
|
|
78
|
+
# 「readiness を /health で見るか」「MTP を使えるか」といった挙動の分岐にも
|
|
79
|
+
# 使われている。それらの分岐は必ず :func:`base_backend` を通して判定すること。
|
|
80
|
+
# 素の文字列比較のままバリアント名が届くと、モデル上書きガードが無効化され
|
|
81
|
+
# たり readiness が TCP connect に退行したりと、例外を出さずに壊れる
|
|
82
|
+
# (docs/designs/launcher-multi-build.md §4 に全分岐の一覧がある)。
|
|
83
|
+
KNOWN_BASE_BACKENDS: tuple[str, ...] = ("llama.cpp", "vllm", "mlx")
|
|
84
|
+
|
|
85
|
+
# バリアント部分に許す文字。プロバイダ名・プロファイル名・ログに混ざっても
|
|
86
|
+
# 壊れない集合に限定し、パス区切りやシェルメタ文字を通さない。
|
|
87
|
+
_VARIANT_RE = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def base_backend(name: str) -> str:
|
|
91
|
+
"""``"llama.cpp-cuda"`` → ``"llama.cpp"``。基底名そのものはそのまま返す。
|
|
92
|
+
|
|
93
|
+
既知基底名との**最長一致**で判定する。``name.split("-", 1)[0]`` にしない
|
|
94
|
+
のは意図的で、基底名自体がハイフンを含むようになった日に静かに誤判定する
|
|
95
|
+
のを防ぐため。既知基底名で始まらない名前は加工せずそのまま返し、呼び出し
|
|
96
|
+
側の既存 "Unknown backend" 経路に委ねる。
|
|
97
|
+
"""
|
|
98
|
+
best = ""
|
|
99
|
+
for base in KNOWN_BASE_BACKENDS:
|
|
100
|
+
if name == base:
|
|
101
|
+
return base
|
|
102
|
+
if name.startswith(base + "-") and len(base) > len(best):
|
|
103
|
+
best = base
|
|
104
|
+
return best or name
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def variant_of(name: str) -> str | None:
|
|
108
|
+
"""``"llama.cpp-cuda"`` → ``"cuda"``。基底名そのものなら ``None``。
|
|
109
|
+
|
|
110
|
+
``"llama.cpp-"`` のようにバリアント部が空の場合も ``None`` を返す
|
|
111
|
+
(妥当性判定は :func:`is_valid_backend_name` が担う)。
|
|
112
|
+
"""
|
|
113
|
+
base = base_backend(name)
|
|
114
|
+
if base == name or not name.startswith(base + "-"):
|
|
115
|
+
return None
|
|
116
|
+
return name[len(base) + 1 :] or None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def is_variant(name: str) -> bool:
|
|
120
|
+
"""``name`` が ``<既知基底名>-<バリアント>`` 形式かどうか。"""
|
|
121
|
+
return variant_of(name) is not None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def is_valid_backend_name(name: str) -> bool:
|
|
125
|
+
"""既知基底名そのもの、または既知基底名 + 妥当なバリアントかどうか。
|
|
126
|
+
|
|
127
|
+
``launcher.backends`` のキー検証に使う。``"llamacpp"`` (typo) や
|
|
128
|
+
``"llama.cpp-CUDA"`` (大文字) / ``"llama.cpp-"`` (空) は False。
|
|
129
|
+
"""
|
|
130
|
+
if name in KNOWN_BASE_BACKENDS:
|
|
131
|
+
return True
|
|
132
|
+
variant = variant_of(name)
|
|
133
|
+
return variant is not None and _VARIANT_RE.match(variant) is not None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def resolve_option_profiles[P](
|
|
137
|
+
profiles_by_backend: Mapping[str, Sequence[P]], backend: str
|
|
138
|
+
) -> list[P]:
|
|
139
|
+
"""バックエンド(バリアント可)に適用する option_profiles を解決する。
|
|
140
|
+
|
|
141
|
+
基底名のリスト(継承)を先に、バリアント固有のリストを後に連結する。
|
|
142
|
+
``name`` が衝突したらバリアント固有が**同じ位置で置き換える** —— 並び順を
|
|
143
|
+
安定させ、末尾に重複を作らないため。``backend`` が基底名そのものなら
|
|
144
|
+
``profiles_by_backend.get(backend, [])`` と完全に同一の結果になる
|
|
145
|
+
(後方互換)。
|
|
146
|
+
|
|
147
|
+
要素は ``.name`` 属性を持つ任意の型でよい。pydantic の
|
|
148
|
+
``LauncherOptionProfile`` と GUI の dataclass ``OptionProfile`` の双方から
|
|
149
|
+
使うため、この層は pydantic に依存しない (モジュール冒頭の方針どおり)。
|
|
150
|
+
"""
|
|
151
|
+
base = base_backend(backend)
|
|
152
|
+
inherited = list(profiles_by_backend.get(base, []))
|
|
153
|
+
if backend == base:
|
|
154
|
+
return inherited
|
|
155
|
+
own = list(profiles_by_backend.get(backend, []))
|
|
156
|
+
if not own:
|
|
157
|
+
return inherited
|
|
158
|
+
|
|
159
|
+
by_name = {p.name: p for p in own} # type: ignore[attr-defined]
|
|
160
|
+
merged: list[P] = []
|
|
161
|
+
replaced: set[str] = set()
|
|
162
|
+
for p in inherited:
|
|
163
|
+
name = p.name # type: ignore[attr-defined]
|
|
164
|
+
if name in by_name:
|
|
165
|
+
merged.append(by_name[name]) # 継承分と同じ位置で差し替え
|
|
166
|
+
replaced.add(name)
|
|
167
|
+
else:
|
|
168
|
+
merged.append(p)
|
|
169
|
+
merged.extend(
|
|
170
|
+
p for p in own if p.name not in replaced # type: ignore[attr-defined]
|
|
171
|
+
)
|
|
172
|
+
return merged
|
|
173
|
+
|
|
174
|
+
|
|
55
175
|
# ---------------------------------------------------------------------------
|
|
56
176
|
# 2.1 デバイス検出
|
|
57
177
|
# ---------------------------------------------------------------------------
|
|
@@ -249,6 +369,36 @@ class DeviceSelection:
|
|
|
249
369
|
return args
|
|
250
370
|
|
|
251
371
|
|
|
372
|
+
def foreign_device_ids(device_ids: Sequence[str], probe: DeviceProbe) -> list[str]:
|
|
373
|
+
"""``device_ids`` のうち **別ビルドの名前空間**に属するものを返す。
|
|
374
|
+
|
|
375
|
+
バックエンドバリアントを切り替えるとデバイス ID の名前空間も変わる
|
|
376
|
+
(``CUDA0`` と ``Vulkan0`` は同じ GPU を指さない)。CUDA ビルドで
|
|
377
|
+
``CUDA0`` を選んだまま Vulkan ビルドで起動すると ``--device CUDA0`` が
|
|
378
|
+
Vulkan ビルドに渡って起動失敗するので、spawn 前に弾くための判定。
|
|
379
|
+
|
|
380
|
+
判定は **id の完全一致ではなくバックエンド接頭辞** (:func:`backend_of` —
|
|
381
|
+
``"CUDA0"`` → ``"CUDA"``) で行う。理由は 2 つある:
|
|
382
|
+
|
|
383
|
+
1. 検出したいのは「ビルド違い」= 名前空間そのものの不一致であって、
|
|
384
|
+
同一ビルド内の番号ズレではない。UI はチェックボックスから id を渡す
|
|
385
|
+
ので番号ズレは実際には起きない。
|
|
386
|
+
2. 完全一致にすると ``--list-devices`` の出力形式が将来変わって
|
|
387
|
+
:func:`parse_list_devices` が一部の行だけ取りこぼした場合に、**正しい
|
|
388
|
+
id を誤って拒否してしまう**。接頭辞単位なら 1 行でも同じ接頭辞が
|
|
389
|
+
拾えていれば通る。
|
|
390
|
+
|
|
391
|
+
``probe.ok`` が False (``--list-devices`` 自体が失敗) のときと、デバイス
|
|
392
|
+
を 1 つも拾えなかったときは **常に空リスト** を返す。列挙できない環境で
|
|
393
|
+
機能を殺さないための best-effort 原則 (``hardware.py`` の 5-deps 不変則と
|
|
394
|
+
同じ姿勢)。
|
|
395
|
+
"""
|
|
396
|
+
if not probe.ok or not probe.devices:
|
|
397
|
+
return []
|
|
398
|
+
known_prefixes = {backend_of(d.id) for d in probe.devices}
|
|
399
|
+
return [d for d in device_ids if backend_of(d) not in known_prefixes]
|
|
400
|
+
|
|
401
|
+
|
|
252
402
|
# ---------------------------------------------------------------------------
|
|
253
403
|
# 2.3 tensor-split 自動提案
|
|
254
404
|
# ---------------------------------------------------------------------------
|
|
@@ -357,6 +507,36 @@ def build_auto_sweep_configs(
|
|
|
357
507
|
return configs
|
|
358
508
|
|
|
359
509
|
|
|
510
|
+
def build_cross_variant_sweep_configs(
|
|
511
|
+
probes: Sequence[tuple[str, Sequence[LlamaDevice]]],
|
|
512
|
+
*,
|
|
513
|
+
by: str = "total",
|
|
514
|
+
) -> list[tuple[str, str, DeviceSelection]]:
|
|
515
|
+
"""複数ビルド(バリアント)を横断するスイープ構成を生成する。
|
|
516
|
+
|
|
517
|
+
``probes`` は ``[(backend_name, devices), ...]`` —— 各バリアントを
|
|
518
|
+
``--list-devices`` でプローブした結果。返すのは
|
|
519
|
+
``[(label, backend_name, DeviceSelection), ...]`` で、ラベルには
|
|
520
|
+
バリアント名を前置する (``"cuda / CUDA0 単体"``)。``{config}`` プレース
|
|
521
|
+
ホルダ経由でベンチ結果 JSON もビルド別に見分けられる。
|
|
522
|
+
|
|
523
|
+
``build_auto_sweep_configs`` を各ビルドに対して呼ぶだけなので、
|
|
524
|
+
「バックエンド跨ぎの混成構成は作らない」という既存規則はビルド内でも
|
|
525
|
+
そのまま維持される。ビルド間の混成 (CUDA ビルドの CUDA0 と Vulkan
|
|
526
|
+
ビルドの Vulkan2 を同時に、など) は **原理的に不可能** ——1 プロセスは
|
|
527
|
+
1 つの実行ファイルでしか動かないので、ここでも作らない。
|
|
528
|
+
|
|
529
|
+
これで「同一モデルを CUDA ビルドと Vulkan ビルドで順に起動してベンチし、
|
|
530
|
+
どちらが速いか比較する」が 1 回のスイープで回る。
|
|
531
|
+
"""
|
|
532
|
+
out: list[tuple[str, str, DeviceSelection]] = []
|
|
533
|
+
for backend, devices in probes:
|
|
534
|
+
prefix = variant_of(backend) or backend
|
|
535
|
+
for label, selection in build_auto_sweep_configs(devices, by=by):
|
|
536
|
+
out.append((f"{prefix} / {label}", backend, selection))
|
|
537
|
+
return out
|
|
538
|
+
|
|
539
|
+
|
|
360
540
|
# ---------------------------------------------------------------------------
|
|
361
541
|
# 2.4 スイープ計画・状態
|
|
362
542
|
# ---------------------------------------------------------------------------
|
|
@@ -382,6 +562,10 @@ class SweepStep:
|
|
|
382
562
|
error: str | None = None
|
|
383
563
|
started_at: float = 0.0
|
|
384
564
|
ended_at: float = 0.0
|
|
565
|
+
# ステップ個別のバックエンド(バリアント横断スイープ用)。None なら
|
|
566
|
+
# ``SweepPlan.backend`` を使う=従来と完全に同じ argv。末尾の任意
|
|
567
|
+
# フィールドなので既存の位置指定コンストラクタ呼び出しは無影響。
|
|
568
|
+
backend: str | None = None
|
|
385
569
|
|
|
386
570
|
def as_dict(self) -> dict[str, Any]:
|
|
387
571
|
return {
|
|
@@ -393,6 +577,7 @@ class SweepStep:
|
|
|
393
577
|
"results_path": self.results_path,
|
|
394
578
|
"summary": self.summary,
|
|
395
579
|
"error": self.error,
|
|
580
|
+
"backend": self.backend,
|
|
396
581
|
}
|
|
397
582
|
|
|
398
583
|
|
|
@@ -400,7 +585,7 @@ class SweepStep:
|
|
|
400
585
|
class SweepPlan:
|
|
401
586
|
steps: list[SweepStep]
|
|
402
587
|
model_path: str
|
|
403
|
-
backend: str #
|
|
588
|
+
backend: str # プラン既定。step.backend が None のとき使われる
|
|
404
589
|
port: int
|
|
405
590
|
bench_cmd_template: str
|
|
406
591
|
results_dir: str | None = None
|
|
@@ -409,9 +594,20 @@ class SweepPlan:
|
|
|
409
594
|
|
|
410
595
|
|
|
411
596
|
def build_sweep_steps(
|
|
412
|
-
labeled: Sequence[tuple[str, DeviceSelection]]
|
|
597
|
+
labeled: Sequence[tuple[str, DeviceSelection]]
|
|
598
|
+
| Sequence[tuple[str, DeviceSelection, str | None]],
|
|
413
599
|
) -> list[SweepStep]:
|
|
414
|
-
|
|
600
|
+
"""``(label, selection)`` または ``(label, selection, backend)`` から生成。
|
|
601
|
+
|
|
602
|
+
3 要素形はバリアント横断スイープ用 (ステップごとに実行ファイルが違う)。
|
|
603
|
+
2 要素形は従来どおり ``backend=None`` になり、プラン既定で動く。
|
|
604
|
+
"""
|
|
605
|
+
steps: list[SweepStep] = []
|
|
606
|
+
for item in labeled:
|
|
607
|
+
lbl, sel = item[0], item[1]
|
|
608
|
+
backend = item[2] if len(item) == 3 else None
|
|
609
|
+
steps.append(SweepStep(label=lbl, selection=sel, backend=backend))
|
|
610
|
+
return steps
|
|
415
611
|
|
|
416
612
|
|
|
417
613
|
# ---------------------------------------------------------------------------
|
|
@@ -45,6 +45,7 @@ import re
|
|
|
45
45
|
from pathlib import Path
|
|
46
46
|
|
|
47
47
|
from coderouter.gguf_introspect import try_read_gguf_metadata
|
|
48
|
+
from coderouter.launcher_devices import base_backend
|
|
48
49
|
|
|
49
50
|
__all__ = ["find_draft_companion", "resolve_speculative"]
|
|
50
51
|
|
|
@@ -248,7 +249,10 @@ def resolve_speculative(
|
|
|
248
249
|
|
|
249
250
|
# 1. Non-llama.cpp backends do not support speculation. Reject explicit
|
|
250
251
|
# use; otherwise (auto default, no draft) stay a silent no-op.
|
|
251
|
-
|
|
252
|
+
# ``backend`` may be a variant (``llama.cpp-cuda``) — every llama.cpp
|
|
253
|
+
# build supports speculation, so compare the base name. A plain ``!=``
|
|
254
|
+
# here would silently drop the spec flags for variants.
|
|
255
|
+
if base_backend(backend) != _LLAMA_CPP:
|
|
252
256
|
if draft_model_path or mtp_mode == "off":
|
|
253
257
|
raise ValueError(
|
|
254
258
|
"draft_model_path / mtp_mode are only supported for llama.cpp"
|
coderouter/launcher_swap.py
CHANGED
|
@@ -50,6 +50,7 @@ from coderouter.ingress.launcher_routes import (
|
|
|
50
50
|
spawn_process,
|
|
51
51
|
stop_process,
|
|
52
52
|
)
|
|
53
|
+
from coderouter.launcher_devices import resolve_option_profiles
|
|
53
54
|
from coderouter.logging import get_logger
|
|
54
55
|
|
|
55
56
|
logger = get_logger(__name__)
|
|
@@ -353,10 +354,18 @@ class SwapManager:
|
|
|
353
354
|
reg.remove(proc_id)
|
|
354
355
|
|
|
355
356
|
def _resolve_options(self, spec: SwapModelSpec) -> dict[str, Any]:
|
|
356
|
-
"""Resolve ``spec.option_profile`` into the launcher options dict.
|
|
357
|
+
"""Resolve ``spec.option_profile`` into the launcher options dict.
|
|
358
|
+
|
|
359
|
+
``spec.backend`` may be a variant (``llama.cpp-cuda``), in which case
|
|
360
|
+
the base backend's presets are inherited — the same merge the manual
|
|
361
|
+
UI applies, so a swap entry can reference a shared preset without
|
|
362
|
+
duplicating it under every variant key.
|
|
363
|
+
"""
|
|
357
364
|
if not spec.option_profile or self._launcher_cfg is None:
|
|
358
365
|
return {}
|
|
359
|
-
profiles =
|
|
366
|
+
profiles = resolve_option_profiles(
|
|
367
|
+
self._launcher_cfg.option_profiles, spec.backend
|
|
368
|
+
)
|
|
360
369
|
for p in profiles:
|
|
361
370
|
if p.name == spec.option_profile:
|
|
362
371
|
return dict(p.args)
|
coderouter/routing/fallback.py
CHANGED
|
@@ -2911,7 +2911,15 @@ class FallbackEngine:
|
|
|
2911
2911
|
chat_req = to_chat_request(effective_request)
|
|
2912
2912
|
chat_req.stream = False
|
|
2913
2913
|
chat_resp = await adapter.generate(chat_req, overrides=overrides)
|
|
2914
|
-
|
|
2914
|
+
# H-4: the translation hop runs the tool-call repair
|
|
2915
|
+
# scanners over the whole assistant message. That is CPU
|
|
2916
|
+
# work, not I/O, so it is pushed onto a worker thread —
|
|
2917
|
+
# a pathological message must not stall the event loop
|
|
2918
|
+
# for every other in-flight request. `to_anthropic_response`
|
|
2919
|
+
# itself stays synchronous (many callers depend on it).
|
|
2920
|
+
resp = await asyncio.to_thread(
|
|
2921
|
+
to_anthropic_response, chat_resp, allowed_tool_names=tool_names
|
|
2922
|
+
)
|
|
2915
2923
|
except AdapterError as exc:
|
|
2916
2924
|
# v1.9-C: record the failure with its observed latency.
|
|
2917
2925
|
# Auth-flavored failures (401 / 403) carry no useful
|
|
@@ -3223,7 +3231,11 @@ class FallbackEngine:
|
|
|
3223
3231
|
chat_req = to_chat_request(effective_request)
|
|
3224
3232
|
chat_req.stream = False
|
|
3225
3233
|
chat_resp = await adapter.generate(chat_req, overrides=overrides)
|
|
3226
|
-
|
|
3234
|
+
# H-4: same as the non-streaming branch — keep the
|
|
3235
|
+
# CPU-bound repair scan off the event loop.
|
|
3236
|
+
anth_resp = await asyncio.to_thread(
|
|
3237
|
+
to_anthropic_response, chat_resp, allowed_tool_names=tool_names
|
|
3238
|
+
)
|
|
3227
3239
|
event_iter = synthesize_anthropic_stream_from_response(anth_resp)
|
|
3228
3240
|
first = await anext(event_iter)
|
|
3229
3241
|
else:
|
|
@@ -321,7 +321,20 @@ class AnthropicResponse(BaseModel):
|
|
|
321
321
|
role: Literal["assistant"] = "assistant"
|
|
322
322
|
model: str
|
|
323
323
|
content: list[dict[str, Any]]
|
|
324
|
-
|
|
324
|
+
# H-6: intentionally `str | None`, NOT a closed `Literal[...]`. Anthropic
|
|
325
|
+
# adds new stop_reason values over time (observed additions: pause_turn
|
|
326
|
+
# for long-running server-side-tool turns, refusal for safety-classifier
|
|
327
|
+
# stops, model_context_window_exceeded); a closed Literal rejects any
|
|
328
|
+
# response using a value minted after this code was written, which
|
|
329
|
+
# pydantic surfaces as a ValidationError that the adapter turns into a
|
|
330
|
+
# retryable AdapterError — silently discarding an already-billed,
|
|
331
|
+
# perfectly valid response and falling back to another provider. Known
|
|
332
|
+
# values as of writing: end_turn, max_tokens, stop_sequence, tool_use,
|
|
333
|
+
# pause_turn, refusal, model_context_window_exceeded. Other models in
|
|
334
|
+
# this module already use `extra="allow"` for the same forward-
|
|
335
|
+
# compatibility reason; this keeps that policy consistent for a
|
|
336
|
+
# declared field, which `extra="allow"` alone does not cover.
|
|
337
|
+
stop_reason: str | None = None
|
|
325
338
|
stop_sequence: str | None = None
|
|
326
339
|
usage: AnthropicUsage = Field(default_factory=AnthropicUsage)
|
|
327
340
|
|
|
@@ -846,6 +846,13 @@ _REVERSE_FINISH_REASON_MAP = {
|
|
|
846
846
|
"max_tokens": "length",
|
|
847
847
|
"tool_use": "tool_calls",
|
|
848
848
|
"stop_sequence": "stop",
|
|
849
|
+
# H-6: forward-compat stop_reason values (see translation/anthropic.py).
|
|
850
|
+
# The `.get(..., "stop")` fallback at each call site already keeps these
|
|
851
|
+
# from raising; mapping them explicitly just makes the OpenAI-shaped
|
|
852
|
+
# finish_reason semantically accurate instead of a generic "stop".
|
|
853
|
+
"pause_turn": "stop",
|
|
854
|
+
"refusal": "content_filter",
|
|
855
|
+
"model_context_window_exceeded": "length",
|
|
849
856
|
}
|
|
850
857
|
|
|
851
858
|
|