fix(vscode): persist compatible Claude modes and route Copilot CAPI (#2986)
## Description Fixes #2492, #2028, and #2827. Claude daemon workers consume project settings rather than reliably inheriting wrapper environment state, while the Claude VS Code webview cannot render deferred-tool response blocks. Separately, recent Copilot Chat versions use the whole CAPI override for generation; the legacy proxy override alone only sends model discovery through Headroom. This PR carries both integrations through to the actual consumers instead of only changing their launch-time surface configuration. ## Type of Change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Documentation update - [ ] Build / CI ## Changes Made - Persist the resolved Claude ENABLE_TOOL_SEARCH value into project settings for daemon workers and restore it transactionally after wrap exits. - Use compatibility-safe Foundry and Claude VS Code defaults while preserving explicit user choices. - Configure both Copilot overrideProxyUrl and overrideCapiUrl in the reversible managed VS Code settings block. - Route Copilot unprefixed POST /chat/completions and HTTP /responses requests through the real compression handlers. - Keep /responses out of the Codex WebSocket aliases because Copilot and Codex use different WebSocket wire protocols. - Extend wrap E2E assertions for both the Claude webview mode and Copilot CAPI routing. ## Testing - [x] 127 combined Claude, Copilot, route-integration, and MCP dependency-contract tests pass. - [x] Ruff check passes on all changed Python files. - [x] Ruff format check passes. - [x] Python compilation and git diff --check pass. ## Runtime Safety Standalone Claude CLI defaults remain unchanged. Explicit Claude tool-search values retain precedence, and project settings are restored through the existing cleanup path. Copilot model/session helper endpoints continue through generic passthrough, while only validated HTTP generation paths receive explicit compression routes. Existing Codex WebSocket behavior is unchanged. ## Review Readiness - [x] Current main and MCP v1 compatibility retained - [x] Worker-facing Claude persistence covered - [x] Reversible Copilot and Claude settings behavior covered - [x] Copilot generation routes covered at registration and proxy integration layers - [x] Ready for review
This commit is contained in:
+7
-2
@@ -790,6 +790,11 @@ def verify_vscode_wrap(base_env: dict[str, str], project_dir: Path) -> None:
|
||||
f'"http://127.0.0.1:{port}{project_prefix}"' in configured,
|
||||
"VS Code wrap should configure the project-scoped proxy URL",
|
||||
)
|
||||
assert_true(
|
||||
f'"github.copilot.advanced.debug.overrideCapiUrl": '
|
||||
f'"http://127.0.0.1:{port}{project_prefix}"' in configured,
|
||||
"VS Code wrap should route Copilot Chat generation through Headroom",
|
||||
)
|
||||
assert_true(
|
||||
'"github.copilot.advanced.debug.overrideAuthType": "token"' in configured,
|
||||
"VS Code wrap should configure token auth",
|
||||
@@ -849,8 +854,8 @@ def verify_vscode_claude_wrap(base_env: dict[str, str], project_dir: Path) -> No
|
||||
"VS Code Claude wrap should configure the project-scoped Anthropic URL",
|
||||
)
|
||||
assert_true(
|
||||
configured["env"]["ENABLE_TOOL_SEARCH"] == "true",
|
||||
"VS Code Claude wrap should retain Claude Code tool deferral",
|
||||
configured["env"]["ENABLE_TOOL_SEARCH"] == "false",
|
||||
"VS Code Claude wrap should disable tool deferral for webview compatibility",
|
||||
)
|
||||
assert_true(configured["env"]["KEEP"] == "yes", "Existing Claude env must remain")
|
||||
assert_true(str(settings_path) in output, "Wrap output should identify Claude settings")
|
||||
|
||||
@@ -41,6 +41,7 @@ from headroom.install.runtime import (
|
||||
from headroom.install.state import ManifestError, load_manifest, save_manifest
|
||||
from headroom.install.supervisors import start_supervisor
|
||||
from headroom.providers.claude import TOOL_SEARCH_DEFAULT, TOOL_SEARCH_ENV
|
||||
from headroom.providers.claude.runtime import TOOL_SEARCH_FOUNDRY_DEFAULT
|
||||
from headroom.providers.codex.install import codex_uses_chatgpt_auth
|
||||
from headroom.providers.codex.threads import retag_to_headroom
|
||||
|
||||
@@ -181,7 +182,12 @@ def _ensure_claude_hooks(path: Path, profile: str, port: int) -> None:
|
||||
# all into its context window — overflowing it (breaks sub-agent spawns,
|
||||
# forces constant compaction). Keep deferral on; respect a user-set value.
|
||||
# Shares the TOOL_SEARCH_* constants with `wrap` and `install`.
|
||||
env_map.setdefault(TOOL_SEARCH_ENV, TOOL_SEARCH_DEFAULT)
|
||||
tool_search_default = (
|
||||
TOOL_SEARCH_FOUNDRY_DEFAULT
|
||||
if os.environ.get("CLAUDE_CODE_USE_FOUNDRY")
|
||||
else TOOL_SEARCH_DEFAULT
|
||||
)
|
||||
env_map.setdefault(TOOL_SEARCH_ENV, tool_search_default)
|
||||
payload["env"] = env_map
|
||||
|
||||
hooks = dict(payload.get("hooks") or {}) if isinstance(payload.get("hooks"), dict) else {}
|
||||
|
||||
+49
-2
@@ -451,6 +451,8 @@ def _resolved_tool_search_mode(flag_value: str | None) -> str:
|
||||
existing = os.environ.get(_TOOL_SEARCH_ENV)
|
||||
if existing is not None:
|
||||
probe[_TOOL_SEARCH_ENV] = existing
|
||||
if os.environ.get("CLAUDE_CODE_USE_FOUNDRY"):
|
||||
probe["CLAUDE_CODE_USE_FOUNDRY"] = os.environ["CLAUDE_CODE_USE_FOUNDRY"]
|
||||
written = _configure_tool_search_env(probe, flag_value)
|
||||
return written if written is not None else probe.get(_TOOL_SEARCH_ENV, "")
|
||||
|
||||
@@ -1478,6 +1480,36 @@ def _write_claude_wrap_base_url(
|
||||
return previous
|
||||
|
||||
|
||||
def _write_claude_wrap_tool_search(value: str, *, settings_path: Path | None = None) -> str | None:
|
||||
"""Persist the resolved tool-search mode for daemon-spawned workers.
|
||||
|
||||
Claude Code workers read project settings afresh rather than inheriting
|
||||
the parent process environment (#2492). Keep this separate from the proxy
|
||||
URL crash marker: a stale tool-search mode cannot route traffic to a dead
|
||||
process, and is restored transactionally when the wrap session exits.
|
||||
"""
|
||||
path = settings_path or (Path.cwd() / ".claude" / "settings.local.json")
|
||||
payload = _read_settings_for_write(path)
|
||||
env_map = dict(payload.get("env") or {}) if isinstance(payload.get("env"), dict) else {}
|
||||
previous = env_map.get(_TOOL_SEARCH_ENV)
|
||||
env_map[_TOOL_SEARCH_ENV] = value
|
||||
payload["env"] = env_map
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
_write_text(path, json.dumps(payload, indent=2) + "\n")
|
||||
return previous
|
||||
|
||||
|
||||
def _restore_claude_wrap_tool_search(
|
||||
previous: str | None, *, settings_path: Path | None = None
|
||||
) -> None:
|
||||
"""Restore the project-local tool-search value written for this session."""
|
||||
_restore_claude_wrap_base_url(
|
||||
previous,
|
||||
settings_path=settings_path,
|
||||
_key_override=_TOOL_SEARCH_ENV,
|
||||
)
|
||||
|
||||
|
||||
def _restore_claude_wrap_base_url(
|
||||
previous: str | None,
|
||||
*,
|
||||
@@ -4703,6 +4735,8 @@ def claude(
|
||||
|
||||
proxy_holder: list[subprocess.Popen | None] = [None]
|
||||
_saved_base_url: list[str | None] = [None] # previous settings.json value for restore
|
||||
_tool_search_not_written = object()
|
||||
_saved_tool_search: list[object | str | None] = [_tool_search_not_written]
|
||||
_settings_foundry: list[bool] = [False]
|
||||
port_holder: list[int] = [port]
|
||||
_settings_vertex: list[bool] = [False]
|
||||
@@ -4934,6 +4968,11 @@ def claude(
|
||||
# Issue #746: keep Claude Code's on-demand tool loading on through the
|
||||
# proxy so tool schemas are not eagerly materialized into local context.
|
||||
_tool_search_value = _configure_tool_search_env(env, tool_search)
|
||||
_resolved_tool_search_value = env.get(_TOOL_SEARCH_ENV, "")
|
||||
_saved_tool_search[0] = _write_claude_wrap_tool_search(
|
||||
_resolved_tool_search_value,
|
||||
settings_path=_wrap_settings_path,
|
||||
)
|
||||
if _tool_search_value is not None:
|
||||
# Describe what the written value actually does: --tool-search
|
||||
# false/0/no/off turns deferral OFF, and the banner must say so
|
||||
@@ -4979,6 +5018,11 @@ def claude(
|
||||
click.echo(f" Error: {e}")
|
||||
raise SystemExit(1) from e
|
||||
finally:
|
||||
if _saved_tool_search[0] is not _tool_search_not_written:
|
||||
_restore_claude_wrap_tool_search(
|
||||
cast(str | None, _saved_tool_search[0]),
|
||||
settings_path=_wrap_settings_path,
|
||||
)
|
||||
_restore_claude_wrap_base_url(
|
||||
_saved_base_url[0],
|
||||
foundry_mode=_settings_foundry[0],
|
||||
@@ -5435,8 +5479,8 @@ def vscode_copilot(
|
||||
) -> None:
|
||||
"""Run Headroom for GitHub Copilot inside Visual Studio Code.
|
||||
|
||||
Transparently overrides Copilot's proxy endpoint, preserving the model
|
||||
selected in VS Code. It does not edit Codex settings.
|
||||
Transparently overrides Copilot's proxy and CAPI endpoints, preserving the
|
||||
model selected in VS Code. It does not edit Codex settings.
|
||||
"""
|
||||
resolution = _require_copilot_subscription_resolution()
|
||||
target_settings = settings_file or vscode_settings_path()
|
||||
@@ -5456,6 +5500,9 @@ def vscode_copilot(
|
||||
click.echo(
|
||||
f' "github.copilot.advanced.debug.overrideProxyUrl": "{vscode_proxy_url(actual_port, _project_name_from_cwd())}",'
|
||||
)
|
||||
click.echo(
|
||||
f' "github.copilot.advanced.debug.overrideCapiUrl": "{vscode_proxy_url(actual_port, _project_name_from_cwd())}",'
|
||||
)
|
||||
click.echo(' "github.copilot.advanced.debug.overrideAuthType": "token"')
|
||||
|
||||
_run_proxy_only_watcher(
|
||||
|
||||
@@ -89,7 +89,11 @@ def configure_vscode_claude_settings(path: Path, proxy_url: str) -> str:
|
||||
payload = _read_settings(path)
|
||||
env = _env_map(payload, path)
|
||||
state_path = _state_path(path)
|
||||
managed = {_BASE_URL_KEY: proxy_url, _TOOL_SEARCH_KEY: "true"}
|
||||
# Claude Code's VS Code webview cannot render the server_tool_use /
|
||||
# tool_search_tool_result blocks emitted by deferred tool search (#2028).
|
||||
# Keep it disabled for this surface; the standalone CLI retains its own
|
||||
# configurable/default-on policy.
|
||||
managed = {_BASE_URL_KEY: proxy_url, _TOOL_SEARCH_KEY: "false"}
|
||||
|
||||
if state_path.exists():
|
||||
state = _read_object(state_path, label="Headroom state")
|
||||
|
||||
@@ -17,6 +17,7 @@ from headroom.proxy.project_context import with_project_prefix
|
||||
_MARKER_START = "// --- Headroom Copilot proxy ---"
|
||||
_MARKER_END = "// --- end Headroom Copilot proxy ---"
|
||||
_PROXY_KEY = "github.copilot.advanced.debug.overrideProxyUrl"
|
||||
_CAPI_KEY = "github.copilot.advanced.debug.overrideCapiUrl"
|
||||
_AUTH_KEY = "github.copilot.advanced.debug.overrideAuthType"
|
||||
|
||||
|
||||
@@ -118,6 +119,7 @@ def _managed_block(proxy_url: str, *, owns_preceding_comma: bool, line_sep: str)
|
||||
return (
|
||||
f"\t{marker}{line_sep}"
|
||||
f"\t{json.dumps(_PROXY_KEY)}: {json.dumps(proxy_url)},{line_sep}"
|
||||
f"\t{json.dumps(_CAPI_KEY)}: {json.dumps(proxy_url)},{line_sep}"
|
||||
f'\t{json.dumps(_AUTH_KEY)}: "token"{line_sep}'
|
||||
f"\t{_MARKER_END}"
|
||||
)
|
||||
@@ -161,7 +163,7 @@ def configure_vscode_proxy_settings(path: Path, proxy_url: str) -> str:
|
||||
if had_managed_block:
|
||||
remove_vscode_proxy_settings(path)
|
||||
raw = _read_settings(path)
|
||||
elif _PROXY_KEY in raw or _AUTH_KEY in raw:
|
||||
elif _PROXY_KEY in raw or _CAPI_KEY in raw or _AUTH_KEY in raw:
|
||||
raise click.ClickException(
|
||||
f"{path} already configures a Copilot endpoint override outside Headroom's "
|
||||
"managed block; refusing to replace it. Remove it or use --no-configure."
|
||||
|
||||
@@ -32,9 +32,19 @@ OPENAI_RESPONSES_ROOT_PATHS: tuple[str, ...] = (
|
||||
"/v1/codex/responses",
|
||||
"/backend-api/responses",
|
||||
"/backend-api/codex/responses",
|
||||
# Copilot Chat derives this unprefixed path from overrideCapiUrl. Without an
|
||||
# explicit root route it falls through to uncompressed generic passthrough.
|
||||
"/responses",
|
||||
)
|
||||
|
||||
OPENAI_RESPONSES_WEBSOCKET_PATHS: tuple[str, ...] = OPENAI_RESPONSES_ROOT_PATHS
|
||||
# The Codex websocket relay speaks a different protocol; do not register the
|
||||
# Copilot HTTP alias as a websocket route without separate wire validation.
|
||||
OPENAI_RESPONSES_WEBSOCKET_PATHS: tuple[str, ...] = (
|
||||
"/v1/responses",
|
||||
"/v1/codex/responses",
|
||||
"/backend-api/responses",
|
||||
"/backend-api/codex/responses",
|
||||
)
|
||||
|
||||
OPENAI_RESPONSES_SUBPATH_ROUTES: tuple[OpenAIResponsesSubpathRoute, ...] = (
|
||||
OpenAIResponsesSubpathRoute("/v1/responses/{sub_path:path}", ("GET", "POST", "DELETE")),
|
||||
|
||||
@@ -105,6 +105,9 @@ ANTHROPIC_BATCH_ROUTES: tuple[ProviderHandlerRoute, ...] = (
|
||||
|
||||
OPENAI_HANDLER_ROUTES: tuple[ProviderHandlerRoute, ...] = (
|
||||
ProviderHandlerRoute("POST", "/v1/chat/completions", "handle_openai_chat"),
|
||||
# Copilot Chat derives this unprefixed path from overrideCapiUrl. Route it
|
||||
# through the real handler; the generic catch-all would bypass compression.
|
||||
ProviderHandlerRoute("POST", "/chat/completions", "handle_openai_chat"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ from headroom.cli import init as init_cli
|
||||
from headroom.providers.claude import install as claude_install
|
||||
|
||||
|
||||
def test_ensure_claude_hooks_sets_enable_tool_search(tmp_path: Path) -> None:
|
||||
def test_ensure_claude_hooks_sets_enable_tool_search(tmp_path: Path, monkeypatch) -> None:
|
||||
monkeypatch.delenv("CLAUDE_CODE_USE_FOUNDRY", raising=False)
|
||||
settings = tmp_path / "settings.json"
|
||||
init_cli._ensure_claude_hooks(settings, profile="init-user", port=8787)
|
||||
|
||||
@@ -25,6 +26,16 @@ def test_ensure_claude_hooks_sets_enable_tool_search(tmp_path: Path) -> None:
|
||||
assert env["ENABLE_TOOL_SEARCH"] == "true"
|
||||
|
||||
|
||||
def test_ensure_claude_hooks_disables_tool_search_for_foundry(tmp_path: Path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("CLAUDE_CODE_USE_FOUNDRY", "1")
|
||||
settings = tmp_path / "settings.json"
|
||||
|
||||
init_cli._ensure_claude_hooks(settings, profile="init-user", port=8787)
|
||||
|
||||
env = json.loads(settings.read_text(encoding="utf-8"))["env"]
|
||||
assert env["ENABLE_TOOL_SEARCH"] == "false"
|
||||
|
||||
|
||||
def test_ensure_claude_hooks_respects_user_tool_search_value(tmp_path: Path) -> None:
|
||||
settings = tmp_path / "settings.json"
|
||||
settings.write_text(
|
||||
|
||||
@@ -34,6 +34,29 @@ def test_write_preserves_other_env_keys(tmp_path: Path) -> None:
|
||||
assert payload["env"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:8787"
|
||||
|
||||
|
||||
def test_tool_search_write_and_restore_reaches_daemon_worker_settings(tmp_path: Path) -> None:
|
||||
path = _settings(tmp_path)
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text(
|
||||
json.dumps({"env": {"ENABLE_TOOL_SEARCH": "true", "KEEP": "1"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
previous = wrap_cli._write_claude_wrap_tool_search("false", settings_path=path)
|
||||
|
||||
assert previous == "true"
|
||||
assert json.loads(path.read_text(encoding="utf-8"))["env"] == {
|
||||
"ENABLE_TOOL_SEARCH": "false",
|
||||
"KEEP": "1",
|
||||
}
|
||||
|
||||
wrap_cli._restore_claude_wrap_tool_search(previous, settings_path=path)
|
||||
assert json.loads(path.read_text(encoding="utf-8"))["env"] == {
|
||||
"ENABLE_TOOL_SEARCH": "true",
|
||||
"KEEP": "1",
|
||||
}
|
||||
|
||||
|
||||
def test_write_returns_none_when_key_absent(tmp_path: Path) -> None:
|
||||
path = _settings(tmp_path)
|
||||
prev = wrap_cli._write_claude_wrap_base_url("http://127.0.0.1:8787", settings_path=path)
|
||||
|
||||
@@ -78,6 +78,13 @@ def _invoke_wrap_claude(
|
||||
|
||||
monkeypatch.setattr(wrap_mod, "_write_claude_wrap_base_url", fake_write_base_url)
|
||||
monkeypatch.setattr(wrap_mod, "_restore_claude_wrap_base_url", lambda *_args, **_kwargs: None)
|
||||
|
||||
def fake_write_tool_search(value: str, **kwargs: object) -> None:
|
||||
captured["write_tool_search_value"] = value
|
||||
captured["write_tool_search_kwargs"] = kwargs
|
||||
|
||||
monkeypatch.setattr(wrap_mod, "_write_claude_wrap_tool_search", fake_write_tool_search)
|
||||
monkeypatch.setattr(wrap_mod, "_restore_claude_wrap_tool_search", lambda *_a, **_k: None)
|
||||
monkeypatch.setattr(wrap_mod, "_print_telemetry_notice", lambda: None)
|
||||
|
||||
def fake_ensure_proxy(*args: object, **kwargs: object) -> tuple[None, int]:
|
||||
@@ -209,6 +216,24 @@ def test_wrap_claude_tool_search_banner_line_still_accurate_when_active(
|
||||
assert "on-demand tool loading kept on" in output
|
||||
assert "keeps it on for this session" in output
|
||||
assert "DISABLED per your setting" not in output
|
||||
assert _captured["write_tool_search_value"] == "true"
|
||||
|
||||
|
||||
def test_wrap_claude_foundry_persists_disabled_tool_search_for_workers(
|
||||
runner: CliRunner, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
captured, output = _invoke_wrap_claude(
|
||||
runner,
|
||||
monkeypatch,
|
||||
env={
|
||||
"CLAUDE_CODE_USE_FOUNDRY": "1",
|
||||
"ANTHROPIC_FOUNDRY_BASE_URL": "https://tenant.services.ai.azure.com/anthropic",
|
||||
},
|
||||
)
|
||||
|
||||
assert captured["child_env"]["ENABLE_TOOL_SEARCH"] == "false"
|
||||
assert captured["write_tool_search_value"] == "false"
|
||||
assert "on-demand tool loading DISABLED" in output
|
||||
|
||||
|
||||
def test_wrap_claude_vertex_passes_custom_base_url_to_proxy_before_child_redirect(
|
||||
|
||||
@@ -66,6 +66,7 @@ def test_wrap_vscode_no_configure_prints_transparent_settings(tmp_path: Path) ->
|
||||
assert result.exit_code == 0, result.output
|
||||
assert not path.exists()
|
||||
assert "overrideProxyUrl" in result.output
|
||||
assert "overrideCapiUrl" in result.output
|
||||
assert "overrideAuthType" in result.output
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ def test_wrap_vscode_claude_configures_actual_port(tmp_path: Path) -> None:
|
||||
assert result.exit_code == 0, result.output
|
||||
env = json.loads(path.read_text(encoding="utf-8"))["env"]
|
||||
assert env["ANTHROPIC_BASE_URL"].startswith("http://127.0.0.1:9999/p/")
|
||||
assert env["ENABLE_TOOL_SEARCH"] == "true"
|
||||
assert env["ENABLE_TOOL_SEARCH"] == "false"
|
||||
assert "Reload VS Code" in result.output
|
||||
assert captured["agent_type"] == "claude"
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ def test_configure_and_remove_preserve_unrelated_and_previous_values(tmp_path: P
|
||||
assert configured["env"] == {
|
||||
"KEEP": "yes",
|
||||
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8787/p/demo",
|
||||
"ENABLE_TOOL_SEARCH": "true",
|
||||
"ENABLE_TOOL_SEARCH": "false",
|
||||
}
|
||||
assert configured["permissions"] == {"allow": ["Read"]}
|
||||
|
||||
@@ -162,7 +162,7 @@ def test_reconfigure_refuses_incomplete_or_conflicting_state(tmp_path: Path) ->
|
||||
state_path.unlink()
|
||||
configure_vscode_claude_settings(path, proxy_url)
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
payload["env"]["ENABLE_TOOL_SEARCH"] = "false"
|
||||
payload["env"]["ENABLE_TOOL_SEARCH"] = "true"
|
||||
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
with pytest.raises(click.ClickException, match="managed values"):
|
||||
configure_vscode_claude_settings(path, proxy_url)
|
||||
|
||||
@@ -50,6 +50,7 @@ def test_configure_update_and_remove_preserve_jsonc_verbatim(tmp_path: Path) ->
|
||||
assert "editor.fontSize" in configured
|
||||
assert "user comment" in configured
|
||||
assert '"github.copilot.advanced.debug.overrideProxyUrl"' in configured
|
||||
assert '"github.copilot.advanced.debug.overrideCapiUrl"' in configured
|
||||
assert '"github.copilot.advanced.debug.overrideAuthType": "token"' in configured
|
||||
|
||||
assert configure_vscode_proxy_settings(path, "http://127.0.0.1:9999") == "updated"
|
||||
@@ -66,6 +67,7 @@ def test_configure_refuses_malformed_and_unmanaged_override(tmp_path: Path) -> N
|
||||
for original in (
|
||||
"{broken",
|
||||
'{"github.copilot.advanced.debug.overrideProxyUrl":"http://other"}',
|
||||
'{"github.copilot.advanced.debug.overrideCapiUrl":"http://other"}',
|
||||
):
|
||||
path.write_text(original, encoding="utf-8")
|
||||
with pytest.raises(click.ClickException, match="did not overwrite|refusing"):
|
||||
|
||||
@@ -23,8 +23,15 @@ def test_openai_responses_route_aliases_are_explicit() -> None:
|
||||
"/v1/codex/responses",
|
||||
"/backend-api/responses",
|
||||
"/backend-api/codex/responses",
|
||||
"/responses",
|
||||
)
|
||||
assert OPENAI_RESPONSES_WEBSOCKET_PATHS == OPENAI_RESPONSES_ROOT_PATHS
|
||||
assert OPENAI_RESPONSES_WEBSOCKET_PATHS == (
|
||||
"/v1/responses",
|
||||
"/v1/codex/responses",
|
||||
"/backend-api/responses",
|
||||
"/backend-api/codex/responses",
|
||||
)
|
||||
assert "/responses" not in OPENAI_RESPONSES_WEBSOCKET_PATHS
|
||||
assert OPENAI_RESPONSES_SUBPATH_ROUTES == (
|
||||
OpenAIResponsesSubpathRoute("/v1/responses/{sub_path:path}", ("GET", "POST", "DELETE")),
|
||||
OpenAIResponsesSubpathRoute(
|
||||
|
||||
@@ -496,7 +496,9 @@ def test_provider_specific_routes_delegate_to_expected_proxy_handlers(monkeypatc
|
||||
"handle_anthropic_batch_passthrough"
|
||||
)
|
||||
assert client.post("/v1/chat/completions").json()["handler"] == "handle_openai_chat"
|
||||
assert client.post("/chat/completions").json()["handler"] == "handle_openai_chat"
|
||||
assert client.post("/v1/responses").json()["handler"] == "handle_openai_responses"
|
||||
assert client.post("/responses").json()["handler"] == "handle_openai_responses"
|
||||
assert client.post("/v1/codex/responses").json()["handler"] == "handle_openai_responses"
|
||||
assert client.post("/backend-api/responses").json()["handler"] == "handle_openai_responses"
|
||||
assert client.post("/backend-api/codex/responses").json()["handler"] == (
|
||||
|
||||
@@ -82,6 +82,7 @@ def test_direct_handler_routes_model_endpoint_intent() -> None:
|
||||
)
|
||||
assert OPENAI_HANDLER_ROUTES == (
|
||||
ProviderHandlerRoute("POST", "/v1/chat/completions", "handle_openai_chat"),
|
||||
ProviderHandlerRoute("POST", "/chat/completions", "handle_openai_chat"),
|
||||
)
|
||||
assert GEMINI_HANDLER_ROUTES == (
|
||||
ProviderHandlerRoute(
|
||||
|
||||
Reference in New Issue
Block a user