feat: add DeepSeek Harness client support (#430)
* feat: add DeepSeek Harness client * fix: harden dsh forward proxy capture --------- Co-authored-by: WEIFENG2333 <fengeto@gmail.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
@@ -64,7 +64,7 @@ It works with [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Co
|
||||
- 🔎 **Debug behavior with evidence**: compare adjacent requests and pinpoint which prompt, message, tool, or parameter changed.
|
||||
- 📦 **Share one portable artifact**: each run writes a local trace session that can be exported to a self-contained HTML viewer for review or archiving.
|
||||
- 🔒 **Keep traces on your machine**: no hosted dashboard is required, and common auth headers are redacted before recording.
|
||||
- 🧩 **Use one workflow across clients**: trace Claude Code, Codex CLI, Codex App, Gemini CLI, Grok Build CLI, Kimi CLI, MiMo Code, OpenCode, OpenClaw, Pi, Hermes Agent, Cursor CLI, Qoder CLI, and CodeBuddy.
|
||||
- 🧩 **Use one workflow across clients**: trace Claude Code, Codex CLI, Codex App, Gemini CLI, Grok Build CLI, DeepSeek Harness, Kimi CLI, MiMo Code, OpenCode, OpenClaw, Pi, Hermes Agent, Cursor CLI, Qoder CLI, and CodeBuddy.
|
||||
|
||||
## Supported Clients
|
||||
|
||||
@@ -75,6 +75,7 @@ It works with [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Co
|
||||
| [Codex App](https://openai.com/codex/) | Desktop app launched through forward proxy mode so backend HTTP/WebSocket request bodies are captured |
|
||||
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | Google OAuth / Code Assist traffic |
|
||||
| [Grok Build CLI](https://docs.x.ai/build/overview) | Grok subscription OAuth sessions through the official CLI chat proxy |
|
||||
| [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) | `dsh` headless tasks and custom profiles using DeepSeek or compatible gateways |
|
||||
| [Kimi CLI](https://github.com/MoonshotAI/kimi-cli) | Legacy kimi-cli and the newer Kimi Code CLI |
|
||||
| [MiMo Code](https://mimo.xiaomi.com/en/mimocode) | MiMo Code sessions (OpenCode fork with multi-provider support) |
|
||||
| [OpenCode](https://opencode.ai) | Multi-provider OpenCode sessions |
|
||||
@@ -123,6 +124,9 @@ claude-tap --tap-client gemini -- -p "hello"
|
||||
# Grok Build CLI
|
||||
claude-tap --tap-client grok -- -p "hello"
|
||||
|
||||
# DeepSeek Harness headless task
|
||||
claude-tap --tap-client dsh -- --profile headless "Reply OK"
|
||||
|
||||
# Kimi CLI
|
||||
claude-tap --tap-client kimi
|
||||
|
||||
@@ -525,6 +529,25 @@ GROK_CLI_CHAT_PROXY_BASE_URL=https://grok-gateway.example.com/v1 \
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>DeepSeek Harness examples</summary>
|
||||
|
||||
DeepSeek Harness (`dsh`) uses forward proxy mode by default. This captures model traffic whether the endpoint comes from `DEEPSEEK_BASE_URL` or a stored dsh model setting, including loopback gateways normally covered by `NO_PROXY`. The launcher verifies that Node supports `--use-env-proxy`, records only Chat Completions traffic, and passes all arguments after `--` to dsh unchanged. If that Node capability is unavailable, upgrade Node or use reverse mode with an environment-configured endpoint.
|
||||
|
||||
```bash
|
||||
# One-shot headless task
|
||||
claude-tap --tap-client dsh -- --profile headless "Summarize this repository"
|
||||
|
||||
# Custom dsh profile
|
||||
claude-tap --tap-client dsh -- --profile my-profile
|
||||
|
||||
# Reverse mode for deployments configured only through DEEPSEEK_BASE_URL
|
||||
claude-tap --tap-client dsh --tap-proxy-mode reverse \
|
||||
-- --profile headless "Reply OK"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>CodeBuddy CLI examples</summary>
|
||||
|
||||
@@ -604,7 +627,7 @@ By default the launcher points at the current checkout; pass `--installed` if `c
|
||||
All flags are forwarded to the selected client, except these `--tap-*` ones:
|
||||
|
||||
```
|
||||
--tap-client CLIENT Client to launch/listen to: claude (default), agy, codex, codexapp, gemini, grok, kimi, kimi-code, mimo, opencode, openclaw, pi, hermes, cursor, qoder, or codebuddy
|
||||
--tap-client CLIENT Client to launch/listen to: claude (default), agy, codex, codexapp, dsh, gemini, grok, kimi, kimi-code, mimo, opencode, openclaw, pi, hermes, cursor, qoder, or codebuddy
|
||||
--tap-target URL Upstream API URL (default: auto per client)
|
||||
--tap-live Start real-time viewer while the client runs (default: on)
|
||||
--tap-no-live Disable the real-time viewer server (pre-v0.1.75 behavior)
|
||||
@@ -616,7 +639,7 @@ All flags are forwarded to the selected client, except these `--tap-*` ones:
|
||||
--tap-no-launch Only start the proxy, don't launch client
|
||||
--tap-max-traces N Max trace sessions to keep (default: 50, 0 = unlimited)
|
||||
--tap-store-stream-events Persist raw SSE/WebSocket event arrays during capture so viewer/export output can show them (default: off)
|
||||
--tap-proxy-mode MODE Proxy mode: reverse or forward (default: reverse for claude/codex/grok/kimi/kimi-code/openclaw/codebuddy, forward for agy/codexapp/gemini/mimo/opencode/pi/hermes/qoder; cursor is transcript-only and ignores proxy MITM)
|
||||
--tap-proxy-mode MODE Proxy mode: reverse or forward (default: reverse for claude/codex/grok/kimi/kimi-code/openclaw/codebuddy, forward for agy/codexapp/dsh/gemini/mimo/opencode/pi/hermes/qoder; cursor is transcript-only and ignores proxy MITM)
|
||||
--tap-trust-ca On macOS, explicitly trust the local CA in the user login keychain before launch (agy does this automatically)
|
||||
```
|
||||
|
||||
|
||||
+26
-3
@@ -64,7 +64,7 @@
|
||||
- 🔎 **用证据定位问题**:对比相邻请求,明确是哪段 prompt、消息、工具或参数发生了变化。
|
||||
- 📦 **留下可分享证据**:每次运行都会写入 JSONL trace,并生成自包含 HTML 查看器,方便 review 或归档。
|
||||
- 🔒 **数据留在本机**:不依赖云端 dashboard;常见认证 header 会在记录前自动脱敏。
|
||||
- 🧩 **覆盖主流编码客户端**:同一套流程可用于 Claude Code、Codex CLI、Codex App、Gemini CLI、Grok Build CLI、Kimi CLI、MiMo Code、OpenCode、OpenClaw、Pi、Hermes Agent、Cursor CLI、Qoder CLI、Antigravity CLI 和 CodeBuddy CLI。
|
||||
- 🧩 **覆盖主流编码客户端**:同一套流程可用于 Claude Code、Codex CLI、Codex App、Gemini CLI、Grok Build CLI、DeepSeek Harness、Kimi CLI、MiMo Code、OpenCode、OpenClaw、Pi、Hermes Agent、Cursor CLI、Qoder CLI、Antigravity CLI 和 CodeBuddy CLI。
|
||||
|
||||
## 支持的客户端
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
| [Codex App](https://openai.com/codex/) | 通过 forward proxy 启动桌面 App,捕获后端 HTTP/WebSocket 请求体 |
|
||||
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | Google OAuth / Code Assist 的多 Google 端点流量 |
|
||||
| [Grok Build CLI](https://docs.x.ai/build/overview) | 通过官方 CLI chat proxy 捕获 Grok 订阅 OAuth 会话 |
|
||||
| [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) | 使用 DeepSeek 或兼容网关的 `dsh` headless 任务和自定义 profile |
|
||||
| [Kimi CLI](https://github.com/MoonshotAI/kimi-cli) | 旧版 kimi-cli 和新版 Kimi Code CLI |
|
||||
| [MiMo Code](https://mimo.xiaomi.com/en/mimocode) | MiMo Code 会话(基于 OpenCode 的多提供方 fork) |
|
||||
| [OpenCode](https://opencode.ai) | 多提供方 OpenCode 会话 |
|
||||
@@ -123,6 +124,9 @@ claude-tap --tap-client gemini -- -p "hello"
|
||||
# Grok Build CLI
|
||||
claude-tap --tap-client grok -- -p "hello"
|
||||
|
||||
# DeepSeek Harness headless 任务
|
||||
claude-tap --tap-client dsh -- --profile headless "Reply OK"
|
||||
|
||||
# Kimi CLI
|
||||
claude-tap --tap-client kimi
|
||||
|
||||
@@ -517,6 +521,25 @@ GROK_CLI_CHAT_PROXY_BASE_URL=https://grok-gateway.example.com/v1 \
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>DeepSeek Harness 示例</summary>
|
||||
|
||||
DeepSeek Harness(`dsh`)默认使用 forward proxy。无论模型端点来自 `DEEPSEEK_BASE_URL`,还是保存在 dsh 模型设置中,都可以捕获,包括通常会被 `NO_PROXY` 绕过的本地网关。启动器会确认 Node 支持 `--use-env-proxy`,仅记录 Chat Completions 流量,并将 `--` 后的参数原样传给 dsh。如果当前 Node 不支持该能力,请升级 Node,或对环境变量配置的端点使用 reverse 模式。
|
||||
|
||||
```bash
|
||||
# 一次性 headless 任务
|
||||
claude-tap --tap-client dsh -- --profile headless "Summarize this repository"
|
||||
|
||||
# 自定义 dsh profile
|
||||
claude-tap --tap-client dsh -- --profile my-profile
|
||||
|
||||
# 仅通过 DEEPSEEK_BASE_URL 配置的部署也可显式使用 reverse 模式
|
||||
claude-tap --tap-client dsh --tap-proxy-mode reverse \
|
||||
-- --profile headless "Reply OK"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>CodeBuddy CLI 示例</summary>
|
||||
|
||||
@@ -595,7 +618,7 @@ macOS 上,`claude-tap build-macos-app` 会生成本地 `Claude Tap.app`。该
|
||||
除以下 `--tap-*` 参数外,所有参数均透传给所选客户端:
|
||||
|
||||
```
|
||||
--tap-client CLIENT 启动或监听的客户端: claude(默认)/ agy / codex / codexapp / gemini / grok / kimi / kimi-code / mimo / opencode / openclaw / pi / hermes / cursor / qoder / codebuddy
|
||||
--tap-client CLIENT 启动或监听的客户端: claude(默认)/ agy / codex / codexapp / dsh / gemini / grok / kimi / kimi-code / mimo / opencode / openclaw / pi / hermes / cursor / qoder / codebuddy
|
||||
--tap-target URL 上游 API 地址(默认: 根据客户端自动选择)
|
||||
--tap-live 客户端运行时启动实时查看器(默认开启)
|
||||
--tap-no-live 关闭实时查看器(恢复 v0.1.75 之前的行为)
|
||||
@@ -607,7 +630,7 @@ macOS 上,`claude-tap build-macos-app` 会生成本地 `Claude Tap.app`。该
|
||||
--tap-no-launch 仅启动代理,不启动客户端
|
||||
--tap-max-traces N 最大保留 trace 数量(默认: 50,0 = 不限)
|
||||
--tap-store-stream-events 捕获时把原始 SSE/WebSocket event 数组写入 trace 存储,以便查看器/导出结果展示(默认关闭)
|
||||
--tap-proxy-mode MODE 代理模式: reverse 或 forward(默认:claude/codex/grok/kimi/kimi-code/openclaw/codebuddy 用 reverse,agy/codexapp/gemini/mimo/opencode/pi/hermes/qoder 用 forward;cursor 为 transcript-only,不走 MITM 代理)
|
||||
--tap-proxy-mode MODE 代理模式: reverse 或 forward(默认:claude/codex/grok/kimi/kimi-code/openclaw/codebuddy 用 reverse,agy/codexapp/dsh/gemini/mimo/opencode/pi/hermes/qoder 用 forward;cursor 为 transcript-only,不走 MITM 代理)
|
||||
--tap-trust-ca macOS 上显式把本地 CA 信任到当前用户 login keychain(agy 会自动执行)
|
||||
```
|
||||
|
||||
|
||||
+6
-1
@@ -481,6 +481,7 @@ async def async_main(args: argparse.Namespace):
|
||||
].forward_base_url_allowed_path_prefixes,
|
||||
trace_methods=CLIENT_CONFIGS[args.client].forward_trace_methods,
|
||||
trace_path_prefixes=CLIENT_CONFIGS[args.client].forward_trace_path_prefixes,
|
||||
trace_path_suffixes=CLIENT_CONFIGS[args.client].forward_trace_path_suffixes,
|
||||
store_stream_events=args.store_stream_events,
|
||||
capture_only=capture_only,
|
||||
)
|
||||
@@ -735,6 +736,10 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
' claude-tap --tap-client grok -- -p "hello"\n'
|
||||
" # Authenticate first with `grok login`\n"
|
||||
"\n"
|
||||
"deepseek harness (forward proxy mode):\n"
|
||||
' claude-tap --tap-client dsh -- --profile headless "Reply OK"\n'
|
||||
" # Forward mode captures both stored and environment-configured DeepSeek endpoints\n"
|
||||
"\n"
|
||||
"opencode (multi-provider; defaults to forward proxy mode):\n"
|
||||
" # Forward proxy captures every provider opencode talks to\n"
|
||||
" claude-tap --tap-client opencode\n"
|
||||
@@ -846,7 +851,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
help=(
|
||||
"'reverse' sets provider base URL, 'forward' sets HTTPS_PROXY with CONNECT/TLS termination. "
|
||||
"Default depends on the client: 'reverse' for claude/codex/grok/kimi/kimi-code/openclaw/codebuddy, "
|
||||
"'forward' for agy/codexapp/gemini/mimo/opencode/pi/hermes/qoder. "
|
||||
"'forward' for agy/codexapp/dsh/gemini/mimo/opencode/pi/hermes/qoder. "
|
||||
"Ignored for transcript-only clients such as cursor."
|
||||
),
|
||||
)
|
||||
|
||||
@@ -328,6 +328,7 @@ class ClientConfig:
|
||||
# product traffic can still relay everything while persisting only model API calls.
|
||||
forward_trace_methods: tuple[str, ...] = ()
|
||||
forward_trace_path_prefixes: tuple[str, ...] = ()
|
||||
forward_trace_path_suffixes: tuple[str, ...] = ()
|
||||
# Transcript-only clients are observed from local session logs instead of a
|
||||
# spawned process and do not need a reverse or forward proxy.
|
||||
transcript_only: bool = False
|
||||
@@ -429,6 +430,19 @@ CLIENT_CONFIGS: dict[str, ClientConfig] = {
|
||||
"/v1/traces",
|
||||
),
|
||||
),
|
||||
"dsh": ClientConfig(
|
||||
cmd="dsh",
|
||||
label="DeepSeek Harness",
|
||||
install_url="https://github.com/deepseek-ai/deepseek-harness",
|
||||
base_url_env="DEEPSEEK_BASE_URL",
|
||||
base_url_suffix="",
|
||||
default_target="https://api.deepseek.com",
|
||||
# A stored dsh model baseURL outranks DEEPSEEK_BASE_URL. Forward mode
|
||||
# captures both stored and environment-configured endpoints reliably.
|
||||
default_proxy_mode="forward",
|
||||
forward_trace_methods=("POST",),
|
||||
forward_trace_path_suffixes=("/chat/completions",),
|
||||
),
|
||||
"codexapp": ClientConfig(
|
||||
# Prefer the current ChatGPT.app host binary when present; resolution
|
||||
# still falls back through _codex_app_executable_candidates().
|
||||
@@ -594,6 +608,27 @@ def _prefer_windows_command_shim(resolved_cmd: str) -> str:
|
||||
return resolved_cmd
|
||||
|
||||
|
||||
def _node_supports_env_proxy(env: dict[str, str]) -> bool:
|
||||
"""Return whether the Node runtime on PATH supports ``--use-env-proxy``."""
|
||||
node_cmd = shutil.which("node", path=env.get("PATH"))
|
||||
if node_cmd is None:
|
||||
return False
|
||||
probe_env = env.copy()
|
||||
probe_env.pop("NODE_OPTIONS", None)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[node_cmd, "--use-env-proxy", "--version"],
|
||||
env=probe_env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=5,
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
async def run_client(
|
||||
port: int,
|
||||
extra_args: list[str],
|
||||
@@ -646,6 +681,14 @@ async def run_client(
|
||||
inject_proxy = not cfg.transcript_only
|
||||
|
||||
if inject_proxy and proxy_mode == "forward":
|
||||
if client == "dsh" and not _node_supports_env_proxy(env):
|
||||
print(
|
||||
"\nError: DeepSeek Harness forward capture requires a Node runtime "
|
||||
"with --use-env-proxy support.\n"
|
||||
"Upgrade Node until `node --use-env-proxy --version` succeeds, or use "
|
||||
"--tap-proxy-mode reverse when dsh is configured through DEEPSEEK_BASE_URL.\n"
|
||||
)
|
||||
return 1
|
||||
proxy_url = f"http://127.0.0.1:{port}"
|
||||
if client == "codexapp":
|
||||
cmd_args.insert(0, f"--proxy-server={proxy_url}")
|
||||
@@ -659,11 +702,18 @@ async def run_client(
|
||||
env["http_proxy"] = proxy_url
|
||||
env["https_proxy"] = proxy_url
|
||||
env["all_proxy"] = proxy_url
|
||||
if client == "pi":
|
||||
# Pi's SSE transport uses Node fetch, which only reads proxy env vars
|
||||
# when built-in environment proxy support is enabled.
|
||||
if client in {"dsh", "pi"}:
|
||||
# These clients use Node fetch, which only reads proxy env vars when
|
||||
# built-in environment proxy support is enabled.
|
||||
env["NODE_USE_ENV_PROXY"] = "1"
|
||||
_extend_no_proxy(env, ("localhost", "127.0.0.1", "::1"))
|
||||
if client == "dsh":
|
||||
# A dsh model can store any baseURL, including a loopback gateway.
|
||||
# Route every child request through tap; the proxy's own upstream
|
||||
# session still honors the user's original NO_PROXY settings.
|
||||
env["NO_PROXY"] = ""
|
||||
env["no_proxy"] = ""
|
||||
else:
|
||||
_extend_no_proxy(env, ("localhost", "127.0.0.1", "::1"))
|
||||
if client == "mimo":
|
||||
# MiMo defaults to mimo-only mode and ignores provider env vars unless disabled.
|
||||
env["MIMOCODE_MIMO_ONLY"] = "false"
|
||||
@@ -1307,6 +1357,14 @@ def _detect_grok_target() -> str:
|
||||
return CLIENT_CONFIGS["grok"].default_target
|
||||
|
||||
|
||||
def _detect_dsh_target() -> str:
|
||||
"""Auto-detect the DeepSeek endpoint used by dsh reverse mode."""
|
||||
env_target = os.environ.get(CLIENT_CONFIGS["dsh"].base_url_env, "").strip()
|
||||
if env_target:
|
||||
return env_target
|
||||
return CLIENT_CONFIGS["dsh"].default_target
|
||||
|
||||
|
||||
def _read_codebuddy_endpoint_cache() -> str | None:
|
||||
"""Return the host URL from CodeBuddy's login-time endpoint cache, or None."""
|
||||
config_dir = os.environ.get("CODEBUDDY_CONFIG_DIR", "").strip()
|
||||
@@ -2232,6 +2290,7 @@ TARGET_DETECTORS = {
|
||||
"claude": _detect_claude_target,
|
||||
"codex": _detect_codex_target,
|
||||
"codebuddy": _detect_codebuddy_target,
|
||||
"dsh": _detect_dsh_target,
|
||||
"grok": _detect_grok_target,
|
||||
"kimi-code": _detect_kimi_code_target,
|
||||
"openclaw": _detect_openclaw_target,
|
||||
|
||||
@@ -122,6 +122,11 @@ def _matches_path_prefix(path: str, prefixes: tuple[str, ...]) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _matches_path_suffix(path: str, suffixes: tuple[str, ...]) -> bool:
|
||||
clean = path.split("?", 1)[0].rstrip("/").lower()
|
||||
return any(clean.endswith(suffix.lower()) for suffix in suffixes)
|
||||
|
||||
|
||||
def _header_value(headers: Mapping[str, str], name: str) -> str:
|
||||
if value := headers.get(name):
|
||||
return value
|
||||
@@ -273,6 +278,7 @@ class ForwardProxyServer:
|
||||
local_reverse_allowed_path_prefixes: tuple[str, ...] = (),
|
||||
trace_methods: tuple[str, ...] = (),
|
||||
trace_path_prefixes: tuple[str, ...] = (),
|
||||
trace_path_suffixes: tuple[str, ...] = (),
|
||||
store_stream_events: bool = False,
|
||||
capture_only: bool = False,
|
||||
) -> None:
|
||||
@@ -285,6 +291,7 @@ class ForwardProxyServer:
|
||||
self._local_reverse_allowed_path_prefixes = local_reverse_allowed_path_prefixes
|
||||
self._trace_methods = frozenset(method.upper() for method in trace_methods)
|
||||
self._trace_path_prefixes = trace_path_prefixes
|
||||
self._trace_path_suffixes = trace_path_suffixes
|
||||
self._store_stream_events = store_stream_events
|
||||
self._capture_only = capture_only
|
||||
self._server: asyncio.Server | None = None
|
||||
@@ -296,7 +303,11 @@ class ForwardProxyServer:
|
||||
def _should_trace_request(self, method: str, path: str) -> bool:
|
||||
if self._trace_methods and method.upper() not in self._trace_methods:
|
||||
return False
|
||||
if self._trace_path_prefixes and not _matches_path_prefix(path, self._trace_path_prefixes):
|
||||
has_path_filter = bool(self._trace_path_prefixes or self._trace_path_suffixes)
|
||||
matches_path = _matches_path_prefix(path, self._trace_path_prefixes) or _matches_path_suffix(
|
||||
path, self._trace_path_suffixes
|
||||
)
|
||||
if has_path_filter and not matches_path:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
owner: claude-tap-maintainers
|
||||
last_reviewed: 2026-07-18
|
||||
last_reviewed: 2026-08-13
|
||||
source_of_truth: AGENTS.md
|
||||
---
|
||||
|
||||
@@ -26,6 +26,8 @@ Simplified Chinese version: [支持矩阵](support-matrix.zh.md).
|
||||
| Gemini CLI | Google OAuth / Code Assist | Forward proxy (Google endpoints) | n/a | HTTP/SSE | Real E2E verified |
|
||||
| Gemini CLI | API key / Vertex-compatible config (`--tap-proxy-mode reverse`) | `https://generativelanguage.googleapis.com` | none | HTTP/SSE | Unit-tested |
|
||||
| Grok Build CLI | Grok subscription OAuth (`grok login`) | `https://cli-chat-proxy.grok.com/v1` | `/v1` | HTTP/SSE Responses plus storage/trace audit records | Real E2E verified with Grok 0.2.101 |
|
||||
| DeepSeek Harness (`dsh`) | `DEEPSEEK_API_KEY` or dsh credential store | Configured DeepSeek endpoint, including loopback gateways; default `https://api.deepseek.com` | n/a | Forward proxy HTTP/SSE Chat Completions; requires Node `--use-env-proxy` support | Real E2E verified with dsh 0.0.1-rc.2; local-gateway E2E covered |
|
||||
| DeepSeek Harness (`dsh`) | Environment-configured endpoint (`--tap-proxy-mode reverse`) | `DEEPSEEK_BASE_URL` or `https://api.deepseek.com` | none | HTTP/SSE Chat Completions | Unit-tested (`DEEPSEEK_BASE_URL`) |
|
||||
| Kimi CLI (legacy kimi-cli) | Kimi CLI auth/config | `https://api.kimi.com/coding/v1` | none | HTTP/SSE Chat Completions | Unit-tested (`KIMI_BASE_URL`) |
|
||||
| Kimi CLI (legacy kimi-cli) | Kimi CLI auth/config | `https://api.moonshot.ai/v1` | none | HTTP/SSE Chat Completions | Supported by config |
|
||||
| Kimi Code CLI | `~/.kimi-code/config.toml` + OAuth (`managed:kimi-code`) | `https://api.kimi.com/coding/v1` | none | HTTP/SSE Chat Completions | Unit-tested (`KIMI_CODE_HOME` sandbox) |
|
||||
@@ -57,6 +59,7 @@ Each client in `CLIENT_CONFIGS` declares a `default_proxy_mode` used when
|
||||
| `codexapp` | `forward` | Codex desktop runtime is a macOS `.app` bundle (`ChatGPT.app` today, legacy `Codex.app`; both use `com.openai.codex`) with no `OPENAI_BASE_URL`-style override; forward proxy captures its real upstream HTTP/WebSocket traffic, filtered to `/backend-api/codex/responses` |
|
||||
| `gemini` | `forward` | Google OAuth / Code Assist uses several Google endpoints; forward proxy captures the flow without assuming a single base URL |
|
||||
| `grok` | `reverse` | The official CLI honors `GROK_CLI_CHAT_PROXY_BASE_URL`; reverse mode captures model traffic plus storage/trace audit records without installing a local CA |
|
||||
| `dsh` | `forward` | A stored dsh model `baseURL` outranks `DEEPSEEK_BASE_URL`; verified Node environment-proxy support captures stored, environment-configured, and loopback endpoints while persisting only Chat Completions traffic |
|
||||
| `kimi` | `reverse` | Legacy kimi-cli; native `KIMI_BASE_URL` env var |
|
||||
| `kimi-code` | `reverse` | Patches `~/.kimi-code/config.toml` via temporary `KIMI_CODE_HOME` sandbox |
|
||||
| `mimo` | `forward` | OpenCode fork; multi-provider — forward proxy captures every upstream regardless of which env var the client honors |
|
||||
@@ -124,6 +127,7 @@ strip = CLIENT_CONFIGS[client].reverse_strip_path_prefix(target)
|
||||
- `test_forward_proxy_client_filter_*` (`tests/test_e2e.py`) — verifies forward proxy `forward_trace_methods`/`forward_trace_path_prefixes` filtering relays Codex App product traffic while only tracing `/backend-api/codex/responses` HTTP and WebSocket calls
|
||||
- `test_gemini_registered_in_client_configs` — verifies Gemini CLI registration and default forward mode
|
||||
- `test_grok_*` — verifies Grok Build registration, reverse-mode URL injection, target detection, `/v1` routing, and fake-upstream Responses/storage/trace capture
|
||||
- `test_dsh_*` — verifies dsh registration, Node proxy capability checks, loopback/`NO_PROXY` forwarding, Chat Completions-only trace filtering, reverse-mode `DEEPSEEK_BASE_URL` injection, target detection, argument passthrough, and fake-upstream capture
|
||||
- `test_run_client_gemini_forward_sets_proxy_ca_and_skips_base_url_envs` — verifies Gemini forward proxy launch env
|
||||
- `test_run_client_gemini_reverse_sets_both_base_url_envs` — verifies Gemini reverse proxy base URL env injection
|
||||
- `test_viewer_renders_gemini_semantic_sections` — verifies Gemini systemInstruction, contents, functionDeclarations, functionCall, functionResponse, SSE output, and token usage render as semantic viewer sections
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
owner: claude-tap-maintainers
|
||||
last_reviewed: 2026-07-18
|
||||
last_reviewed: 2026-08-13
|
||||
source_of_truth: AGENTS.md
|
||||
---
|
||||
|
||||
@@ -26,6 +26,8 @@ English version: [Support Matrix](support-matrix.md).
|
||||
| Gemini CLI | Google OAuth / Code Assist | Forward proxy(Google 端点) | n/a | HTTP/SSE | 真实 E2E 已验证 |
|
||||
| Gemini CLI | API key / Vertex 兼容配置(`--tap-proxy-mode reverse`) | `https://generativelanguage.googleapis.com` | 无 | HTTP/SSE | 单测覆盖 |
|
||||
| Grok Build CLI | Grok 订阅 OAuth(`grok login`) | `https://cli-chat-proxy.grok.com/v1` | `/v1` | HTTP/SSE Responses,以及 storage/trace 审计记录 | 已使用 Grok 0.2.101 完成真实 E2E 验证 |
|
||||
| DeepSeek Harness(`dsh`) | `DEEPSEEK_API_KEY` 或 dsh 凭据存储 | 已配置的 DeepSeek 端点,包括本地回环网关;默认 `https://api.deepseek.com` | n/a | Forward proxy HTTP/SSE Chat Completions;要求 Node 支持 `--use-env-proxy` | 已使用 dsh 0.0.1-rc.2 完成真实 E2E 验证;本地网关 E2E 已覆盖 |
|
||||
| DeepSeek Harness(`dsh`) | 环境变量配置的端点(`--tap-proxy-mode reverse`) | `DEEPSEEK_BASE_URL` 或 `https://api.deepseek.com` | 无 | HTTP/SSE Chat Completions | 单测覆盖(`DEEPSEEK_BASE_URL`) |
|
||||
| Kimi CLI(旧版 kimi-cli) | Kimi CLI 认证/配置 | `https://api.kimi.com/coding/v1` | 无 | HTTP/SSE Chat Completions | 单测覆盖(`KIMI_BASE_URL`) |
|
||||
| Kimi CLI(旧版 kimi-cli) | Kimi CLI 认证/配置 | `https://api.moonshot.ai/v1` | 无 | HTTP/SSE Chat Completions | 配置支持 |
|
||||
| Kimi Code CLI | `~/.kimi-code/config.toml` + OAuth(`managed:kimi-code`) | `https://api.kimi.com/coding/v1` | 无 | HTTP/SSE Chat Completions | 单测覆盖(`KIMI_CODE_HOME` sandbox) |
|
||||
@@ -56,6 +58,7 @@ English version: [Support Matrix](support-matrix.md).
|
||||
| `codexapp` | `forward` | Codex 桌面端是 macOS `.app` bundle(当前多为 `ChatGPT.app`,旧版为 `Codex.app`,bundle id 均为 `com.openai.codex`),没有 `OPENAI_BASE_URL` 式覆盖;forward proxy 捕获真实上游 HTTP/WebSocket 流量,并过滤为仅 `/backend-api/codex/responses` |
|
||||
| `gemini` | `forward` | Google OAuth / Code Assist 会访问多个 Google 端点;forward proxy 不依赖单一 base URL,更适合作为默认 |
|
||||
| `grok` | `reverse` | 官方 CLI 原生支持 `GROK_CLI_CHAT_PROXY_BASE_URL`;reverse 模式无需安装本地 CA 即可捕获模型流量和 storage/trace 审计记录 |
|
||||
| `dsh` | `forward` | dsh 模型设置中保存的 `baseURL` 优先于 `DEEPSEEK_BASE_URL`;经能力确认的 Node 环境代理可以捕获设置、环境变量及本地回环端点,并且仅持久化 Chat Completions 流量 |
|
||||
| `kimi` | `reverse` | 旧版 kimi-cli;原生 `KIMI_BASE_URL` 环境变量 |
|
||||
| `kimi-code` | `reverse` | 通过临时 `KIMI_CODE_HOME` sandbox 补丁 `~/.kimi-code/config.toml` |
|
||||
| `mimo` | `forward` | OpenCode fork;多 provider — forward proxy 可以捕获所有上游,而不依赖客户端支持哪个环境变量 |
|
||||
@@ -118,6 +121,7 @@ strip = CLIENT_CONFIGS[client].reverse_strip_path_prefix(target)
|
||||
- `test_forward_proxy_client_filter_*`(`tests/test_e2e.py`):验证 forward proxy 的 `forward_trace_methods`/`forward_trace_path_prefixes` 过滤会照常转发 Codex App 产品流量,但只记录 `/backend-api/codex/responses` 的 HTTP 和 WebSocket 请求
|
||||
- `test_gemini_registered_in_client_configs`:验证 Gemini CLI 注册和默认 forward 模式
|
||||
- `test_grok_*`:验证 Grok Build 注册、reverse 模式 URL 注入、目标探测、`/v1` 路由和 fake upstream Responses/storage/trace 捕获
|
||||
- `test_dsh_*`:验证 dsh 注册、Node 代理能力检查、本地回环/`NO_PROXY` 转发、仅记录 Chat Completions、reverse 模式 `DEEPSEEK_BASE_URL` 注入、目标探测、参数透传和 fake upstream 捕获
|
||||
- `test_run_client_gemini_forward_sets_proxy_ca_and_skips_base_url_envs`:验证 Gemini forward proxy 启动环境变量
|
||||
- `test_run_client_gemini_reverse_sets_both_base_url_envs`:验证 Gemini reverse proxy base URL 环境变量注入
|
||||
- `test_viewer_renders_gemini_semantic_sections`:验证 Gemini systemInstruction、contents、functionDeclarations、functionCall、functionResponse、SSE output 和 token usage 会渲染为语义化 viewer 区块
|
||||
|
||||
@@ -14,6 +14,7 @@ SUPPORTED_CLIENTS = {
|
||||
"claude",
|
||||
"codex",
|
||||
"codexapp",
|
||||
"dsh",
|
||||
"gemini",
|
||||
"grok",
|
||||
"kimi",
|
||||
@@ -35,6 +36,7 @@ SUPPORTED_DEFAULT_PROXY_MODES = {
|
||||
"claude": "reverse",
|
||||
"codex": "reverse",
|
||||
"codexapp": "forward",
|
||||
"dsh": "forward",
|
||||
"gemini": "forward",
|
||||
"grok": "reverse",
|
||||
"kimi": "reverse",
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from claude_tap import parse_args
|
||||
from claude_tap.cli import CLIENT_CONFIGS, _reverse_proxy_trace_options, run_client
|
||||
from claude_tap.cli_clients import _detect_dsh_target, _node_supports_env_proxy
|
||||
|
||||
|
||||
class _DummyProc:
|
||||
def __init__(self) -> None:
|
||||
self.pid = 12345
|
||||
self.returncode: int | None = None
|
||||
|
||||
async def wait(self) -> int:
|
||||
self.returncode = 0
|
||||
return 0
|
||||
|
||||
def terminate(self) -> None:
|
||||
self.returncode = 0
|
||||
|
||||
def kill(self) -> None:
|
||||
self.returncode = -9
|
||||
|
||||
|
||||
def test_dsh_registered_in_client_configs() -> None:
|
||||
cfg = CLIENT_CONFIGS["dsh"]
|
||||
|
||||
assert cfg.cmd == "dsh"
|
||||
assert cfg.label == "DeepSeek Harness"
|
||||
assert cfg.default_target == "https://api.deepseek.com"
|
||||
assert cfg.base_url_env == "DEEPSEEK_BASE_URL"
|
||||
assert cfg.base_url_suffix == ""
|
||||
assert cfg.default_proxy_mode == "forward"
|
||||
assert cfg.forward_trace_methods == ("POST",)
|
||||
assert cfg.forward_trace_path_suffixes == ("/chat/completions",)
|
||||
|
||||
|
||||
def test_parse_args_dsh_defaults_to_forward_mode() -> None:
|
||||
args = parse_args(["--tap-client", "dsh"])
|
||||
|
||||
assert args.client == "dsh"
|
||||
assert args.target == "https://api.deepseek.com"
|
||||
assert args.proxy_mode == "forward"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_client_dsh_forward_enables_node_proxy_and_preserves_args(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_create_subprocess_exec(*cmd, **kwargs):
|
||||
captured["cmd"] = cmd
|
||||
captured["env"] = kwargs["env"]
|
||||
return _DummyProc()
|
||||
|
||||
monkeypatch.delenv("DEEPSEEK_BASE_URL", raising=False)
|
||||
monkeypatch.setenv("NO_PROXY", "localhost,corp.example")
|
||||
monkeypatch.setenv("no_proxy", "127.0.0.1")
|
||||
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/dsh")
|
||||
monkeypatch.setattr("claude_tap.cli_clients._node_supports_env_proxy", lambda _: True)
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
|
||||
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
|
||||
|
||||
code = await run_client(
|
||||
43123,
|
||||
["--profile", "headless", "Reply OK"],
|
||||
client="dsh",
|
||||
proxy_mode="forward",
|
||||
)
|
||||
|
||||
assert code == 0
|
||||
assert captured["cmd"] == ("/tmp/dsh", "--profile", "headless", "Reply OK")
|
||||
env = captured["env"]
|
||||
assert env["HTTPS_PROXY"] == "http://127.0.0.1:43123"
|
||||
assert env["NODE_USE_ENV_PROXY"] == "1"
|
||||
assert env["NO_PROXY"] == ""
|
||||
assert env["no_proxy"] == ""
|
||||
assert "DEEPSEEK_BASE_URL" not in env
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_client_dsh_forward_rejects_node_without_env_proxy_support(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/dsh")
|
||||
monkeypatch.setattr("claude_tap.cli_clients._node_supports_env_proxy", lambda _: False)
|
||||
|
||||
code = await run_client(43123, [], client="dsh", proxy_mode="forward")
|
||||
|
||||
assert code == 1
|
||||
output = capsys.readouterr().out
|
||||
assert "requires a Node runtime with --use-env-proxy support" in output
|
||||
assert "--tap-proxy-mode reverse" in output
|
||||
|
||||
|
||||
def test_node_supports_env_proxy_probes_node_on_path(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
captured["cmd"] = cmd
|
||||
captured["env"] = kwargs["env"]
|
||||
return type("Result", (), {"returncode": 0})()
|
||||
|
||||
monkeypatch.setattr("claude_tap.cli_clients.shutil.which", lambda cmd, path=None: "/opt/node")
|
||||
monkeypatch.setattr("claude_tap.cli_clients.subprocess.run", fake_run)
|
||||
|
||||
assert _node_supports_env_proxy({"PATH": "/opt/bin", "NODE_OPTIONS": "--inspect"})
|
||||
assert captured["cmd"] == ["/opt/node", "--use-env-proxy", "--version"]
|
||||
assert "NODE_OPTIONS" not in captured["env"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_client_dsh_reverse_sets_base_url_and_preserves_args(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_create_subprocess_exec(*cmd, **kwargs):
|
||||
captured["cmd"] = cmd
|
||||
captured["env"] = kwargs["env"]
|
||||
return _DummyProc()
|
||||
|
||||
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/dsh")
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
|
||||
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
|
||||
|
||||
code = await run_client(
|
||||
43123,
|
||||
["--profile", "headless", "Reply OK"],
|
||||
client="dsh",
|
||||
proxy_mode="reverse",
|
||||
)
|
||||
|
||||
assert code == 0
|
||||
assert captured["cmd"] == ("/tmp/dsh", "--profile", "headless", "Reply OK")
|
||||
assert captured["env"]["DEEPSEEK_BASE_URL"] == "http://127.0.0.1:43123"
|
||||
|
||||
|
||||
def test_detect_dsh_target_reads_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("DEEPSEEK_BASE_URL", "https://gateway.example.test")
|
||||
|
||||
assert _detect_dsh_target() == "https://gateway.example.test"
|
||||
|
||||
|
||||
def test_detect_dsh_target_falls_back_to_public_api(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("DEEPSEEK_BASE_URL", raising=False)
|
||||
|
||||
assert _detect_dsh_target() == "https://api.deepseek.com"
|
||||
|
||||
|
||||
def test_dsh_reverse_trace_options_do_not_strip_path_prefix() -> None:
|
||||
options = _reverse_proxy_trace_options("dsh", "https://api.deepseek.com")
|
||||
|
||||
assert options == {"strip_path_prefix": "", "force_http": False}
|
||||
@@ -441,12 +441,17 @@ def _run_claude_tap(
|
||||
tap_client="claude",
|
||||
target_suffix="",
|
||||
no_live=False,
|
||||
proxy_mode=None,
|
||||
client_args=None,
|
||||
client_env=None,
|
||||
):
|
||||
"""Run claude_tap as a subprocess pointing at `upstream_port`.
|
||||
Returns the CompletedProcess."""
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = fake_bin_dir + ":" + env.get("PATH", "")
|
||||
env["PYTHONPATH"] = str(PROJECT_ROOT) + os.pathsep + env.get("PYTHONPATH", "")
|
||||
if client_env:
|
||||
env.update(client_env)
|
||||
|
||||
env = e2e_env(env, trace_dir)
|
||||
cmd = [
|
||||
@@ -462,6 +467,11 @@ def _run_claude_tap(
|
||||
cmd.extend(["--tap-client", tap_client])
|
||||
if no_live:
|
||||
cmd.append("--tap-no-live")
|
||||
if proxy_mode:
|
||||
cmd.extend(["--tap-proxy-mode", proxy_mode])
|
||||
if client_args:
|
||||
cmd.append("--")
|
||||
cmd.extend(client_args)
|
||||
|
||||
return subprocess.run(
|
||||
cmd,
|
||||
@@ -2078,6 +2088,137 @@ def test_grok_client_reverse_proxy():
|
||||
_cleanup(trace_dir, fake_bin_dir, "grok")
|
||||
|
||||
|
||||
FAKE_DSH_SCRIPT = r"""#!/usr/bin/env python3
|
||||
# Fake dsh CLI that mirrors a headless DeepSeek Chat Completions request.
|
||||
import json, os, sys, urllib.request
|
||||
|
||||
if sys.argv[1:] != ["--profile", "headless", "Reply with exactly: HELLO_DSH"]:
|
||||
print(f"[fake-dsh] Unexpected argv: {sys.argv[1:]}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
base = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
|
||||
telemetry_req = urllib.request.Request(
|
||||
f"{base}/v1/logs",
|
||||
data=b"{}",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(telemetry_req) as telemetry_resp:
|
||||
assert telemetry_resp.status == 204
|
||||
url = f"{base}/chat/completions"
|
||||
req_body = json.dumps({
|
||||
"model": "deepseek-v4-flash",
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are a helpful software engineer assistant."},
|
||||
{"role": "user", "content": "Reply with exactly: HELLO_DSH"},
|
||||
],
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "bash",
|
||||
"description": "Run a shell command.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"command": {"type": "string"}},
|
||||
"required": ["command"],
|
||||
},
|
||||
},
|
||||
}],
|
||||
"stream": True,
|
||||
"stream_options": {"include_usage": True},
|
||||
}).encode()
|
||||
req = urllib.request.Request(url, data=req_body, headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer dsh-test-key-12345678",
|
||||
})
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
chunks = resp.read().decode()
|
||||
print(f"[fake-dsh] status={resp.status} stream-bytes={len(chunks)}")
|
||||
except Exception as error:
|
||||
print(f"[fake-dsh] Error: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("HELLO_DSH")
|
||||
"""
|
||||
|
||||
|
||||
def test_dsh_client_forward_proxy_captures_local_gateway():
|
||||
"""Test dsh forward mode against a loopback Chat Completions gateway."""
|
||||
|
||||
async def handler(request):
|
||||
from aiohttp import web
|
||||
|
||||
if request.path == "/gateway/v1/v1/logs":
|
||||
return web.Response(status=204)
|
||||
|
||||
body = await request.json()
|
||||
assert request.path == "/gateway/v1/chat/completions"
|
||||
assert body["model"] == "deepseek-v4-flash"
|
||||
assert body["tools"][0]["function"]["name"] == "bash"
|
||||
assert body["stream_options"] == {"include_usage": True}
|
||||
|
||||
resp = web.StreamResponse(status=200, headers={"Content-Type": "text/event-stream"})
|
||||
await resp.prepare(request)
|
||||
chunks = [
|
||||
{
|
||||
"id": "dsh_chat_1",
|
||||
"model": body["model"],
|
||||
"choices": [{"delta": {"role": "assistant", "reasoning_content": "Need exact text."}}],
|
||||
},
|
||||
{
|
||||
"id": "dsh_chat_1",
|
||||
"model": body["model"],
|
||||
"choices": [{"delta": {"content": "HELLO_DSH"}}],
|
||||
},
|
||||
{
|
||||
"id": "dsh_chat_1",
|
||||
"model": body["model"],
|
||||
"choices": [{"delta": {}, "finish_reason": "stop"}],
|
||||
"usage": {"prompt_tokens": 21, "completion_tokens": 3, "total_tokens": 24},
|
||||
},
|
||||
]
|
||||
for chunk in chunks:
|
||||
await resp.write(f"data: {json.dumps(chunk)}\n\n".encode())
|
||||
await resp.write(b"data: [DONE]\n\n")
|
||||
await resp.write_eof()
|
||||
return resp
|
||||
|
||||
trace_dir = tempfile.mkdtemp(prefix="claude_tap_test_dsh_")
|
||||
fake_bin_dir = tempfile.mkdtemp(prefix="fake_bin_dsh_")
|
||||
fake_dsh = Path(fake_bin_dir) / "dsh"
|
||||
fake_dsh.write_text(FAKE_DSH_SCRIPT)
|
||||
fake_dsh.chmod(fake_dsh.stat().st_mode | stat.S_IEXEC)
|
||||
stop = _start_fake_upstream(19248, handler)
|
||||
|
||||
try:
|
||||
proc = _run_claude_tap(
|
||||
Path(__file__).parent,
|
||||
trace_dir,
|
||||
fake_bin_dir,
|
||||
19248,
|
||||
tap_client="dsh",
|
||||
proxy_mode="forward",
|
||||
client_args=["--profile", "headless", "Reply with exactly: HELLO_DSH"],
|
||||
client_env={"DEEPSEEK_BASE_URL": "http://127.0.0.1:19248/gateway/v1"},
|
||||
)
|
||||
|
||||
assert proc.returncode == 0, f"dsh mode failed: stdout={proc.stdout} stderr={proc.stderr}"
|
||||
records = read_trace_records(trace_dir)
|
||||
assert len(records) == 1
|
||||
record = records[0]
|
||||
assert record["request"]["path"] == "/gateway/v1/chat/completions"
|
||||
assert record["upstream_base_url"] == "http://127.0.0.1:19248"
|
||||
assert record["request"]["body"]["model"] == "deepseek-v4-flash"
|
||||
assert record["request"]["body"]["tools"][0]["function"]["name"] == "bash"
|
||||
assert record["response"]["body"]["content"][0]["type"] == "thinking"
|
||||
assert record["response"]["body"]["content"][1]["text"] == "HELLO_DSH"
|
||||
assert record["response"]["body"]["usage"]["input_tokens"] == 21
|
||||
assert "forward proxy" in proc.stdout
|
||||
finally:
|
||||
stop()
|
||||
_cleanup(trace_dir, fake_bin_dir, "dsh")
|
||||
|
||||
|
||||
FAKE_KIMI_SCRIPT = r"""#!/usr/bin/env python3
|
||||
# Fake Kimi CLI that sends one streaming Chat Completions request via KIMI_BASE_URL
|
||||
import json, os, sys, urllib.request
|
||||
|
||||
Reference in New Issue
Block a user