@mmmbuto/anthmorph 0.1.3 → 0.1.4
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.
- package/CHANGELOG.md +34 -0
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +48 -123
- package/bin/anthmorph +0 -0
- package/docs/CLAUDE_CODE_SETUP.md +78 -0
- package/docs/PACKAGING.md +59 -0
- package/docs/RELEASE.md +82 -0
- package/package.json +16 -4
- package/scripts/anthmorphctl +150 -8
- package/scripts/docker_build_linux.sh +11 -0
- package/scripts/docker_npm_dry_run.sh +25 -0
- package/scripts/docker_release_checks.sh +18 -0
- package/scripts/docker_rust_test.sh +35 -0
- package/scripts/docker_secret_scan.sh +11 -0
- package/scripts/postinstall.js +10 -1
- package/scripts/test_claude_code_patterns_real.sh +150 -0
- package/src/config.rs +33 -0
- package/src/main.rs +24 -5
- package/src/models/anthropic.rs +40 -0
- package/src/proxy.rs +374 -49
- package/src/transform.rs +312 -21
- package/scripts/smoke_test.sh +0 -72
- package/tests/real_backends.rs +0 -213
package/scripts/anthmorphctl
CHANGED
|
@@ -27,18 +27,27 @@ usage:
|
|
|
27
27
|
anthmorphctl status
|
|
28
28
|
anthmorphctl logs
|
|
29
29
|
anthmorphctl print-config
|
|
30
|
+
anthmorphctl bootstrap claude-code [options]
|
|
30
31
|
anthmorphctl help
|
|
31
32
|
|
|
32
33
|
options for init:
|
|
33
34
|
--port PORT
|
|
34
35
|
--backend-url URL
|
|
35
36
|
--model MODEL
|
|
37
|
+
--compat-mode strict|compat
|
|
36
38
|
--reasoning-model MODEL
|
|
37
39
|
--ingress-api-key VALUE
|
|
38
40
|
--allow-origin ORIGIN
|
|
39
41
|
--key-env ENV_NAME
|
|
40
42
|
--api-key VALUE --save-key
|
|
41
43
|
|
|
44
|
+
options for bootstrap claude-code:
|
|
45
|
+
--write
|
|
46
|
+
--base-url URL
|
|
47
|
+
--token VALUE
|
|
48
|
+
--model MODEL
|
|
49
|
+
--timeout-ms MS
|
|
50
|
+
|
|
42
51
|
notes:
|
|
43
52
|
- by default the API key is not saved; use set key-env or --key-env
|
|
44
53
|
- set key VALUE --save stores the key in the local config file
|
|
@@ -63,9 +72,11 @@ save_config() {
|
|
|
63
72
|
tmp_file=$CONFIG_FILE.tmp
|
|
64
73
|
{
|
|
65
74
|
printf 'BACKEND_PROFILE=%s\n' "${BACKEND_PROFILE:-}"
|
|
75
|
+
printf 'COMPAT_MODE=%s\n' "${COMPAT_MODE:-strict}"
|
|
66
76
|
printf 'BACKEND_URL=%s\n' "${BACKEND_URL:-}"
|
|
67
|
-
printf '
|
|
77
|
+
printf 'PRIMARY_MODEL=%s\n' "${PRIMARY_MODEL:-}"
|
|
68
78
|
printf 'REASONING_MODEL=%s\n' "${REASONING_MODEL:-}"
|
|
79
|
+
printf 'FALLBACK_MODELS=%s\n' "${FALLBACK_MODELS:-}"
|
|
69
80
|
printf 'PORT=%s\n' "${PORT:-3000}"
|
|
70
81
|
printf 'INGRESS_API_KEY=%s\n' "${INGRESS_API_KEY:-}"
|
|
71
82
|
printf 'ALLOWED_ORIGINS=%s\n' "${ALLOWED_ORIGINS:-}"
|
|
@@ -122,6 +133,7 @@ ensure_bin_ready() {
|
|
|
122
133
|
|
|
123
134
|
if ! command -v cargo >/dev/null 2>&1; then
|
|
124
135
|
echo "cargo not found and anthmorph binary is missing or outdated" >&2
|
|
136
|
+
echo "build a Linux binary with scripts/docker_build_linux.sh or install Rust/Cargo locally" >&2
|
|
125
137
|
return 1
|
|
126
138
|
fi
|
|
127
139
|
|
|
@@ -153,7 +165,11 @@ parse_common_init_args() {
|
|
|
153
165
|
shift 2
|
|
154
166
|
;;
|
|
155
167
|
--model)
|
|
156
|
-
|
|
168
|
+
PRIMARY_MODEL=$2
|
|
169
|
+
shift 2
|
|
170
|
+
;;
|
|
171
|
+
--compat-mode)
|
|
172
|
+
COMPAT_MODE=$2
|
|
157
173
|
shift 2
|
|
158
174
|
;;
|
|
159
175
|
--reasoning-model)
|
|
@@ -202,9 +218,11 @@ init_cmd() {
|
|
|
202
218
|
shift
|
|
203
219
|
|
|
204
220
|
BACKEND_PROFILE=
|
|
221
|
+
COMPAT_MODE=compat
|
|
205
222
|
BACKEND_URL=
|
|
206
|
-
|
|
223
|
+
PRIMARY_MODEL=
|
|
207
224
|
REASONING_MODEL=
|
|
225
|
+
FALLBACK_MODELS=
|
|
208
226
|
PORT=3000
|
|
209
227
|
INGRESS_API_KEY=
|
|
210
228
|
ALLOWED_ORIGINS=
|
|
@@ -216,13 +234,14 @@ init_cmd() {
|
|
|
216
234
|
chutes)
|
|
217
235
|
BACKEND_PROFILE=chutes
|
|
218
236
|
BACKEND_URL=https://llm.chutes.ai/v1
|
|
219
|
-
|
|
237
|
+
PRIMARY_MODEL=Qwen/Qwen3.5-397B-A17B-TEE
|
|
238
|
+
FALLBACK_MODELS=zai-org/GLM-5-TEE,deepseek-ai/DeepSeek-V3.2-TEE
|
|
220
239
|
API_KEY_ENV=CHUTES_API_KEY
|
|
221
240
|
;;
|
|
222
241
|
minimax)
|
|
223
242
|
BACKEND_PROFILE=openai-generic
|
|
224
243
|
BACKEND_URL=${MINIMAX_BASE_URL:-https://api.minimax.io/v1}
|
|
225
|
-
|
|
244
|
+
PRIMARY_MODEL=${MINIMAX_MODEL:-MiniMax-M2.5}
|
|
226
245
|
API_KEY_ENV=MINIMAX_API_KEY
|
|
227
246
|
;;
|
|
228
247
|
openai)
|
|
@@ -236,7 +255,7 @@ init_cmd() {
|
|
|
236
255
|
|
|
237
256
|
parse_common_init_args "$@"
|
|
238
257
|
|
|
239
|
-
if [ -z "$BACKEND_URL" ] || [ -z "$
|
|
258
|
+
if [ -z "$BACKEND_URL" ] || [ -z "$PRIMARY_MODEL" ] || [ -z "$BACKEND_PROFILE" ]; then
|
|
240
259
|
echo "backend profile, backend url and model are required" >&2
|
|
241
260
|
exit 1
|
|
242
261
|
fi
|
|
@@ -249,6 +268,112 @@ init_cmd() {
|
|
|
249
268
|
echo "saved config to $CONFIG_FILE"
|
|
250
269
|
}
|
|
251
270
|
|
|
271
|
+
bootstrap_claude_code_cmd() {
|
|
272
|
+
ensure_config
|
|
273
|
+
load_config
|
|
274
|
+
|
|
275
|
+
write_mode=0
|
|
276
|
+
bootstrap_base_url=
|
|
277
|
+
bootstrap_token=
|
|
278
|
+
bootstrap_model=
|
|
279
|
+
bootstrap_timeout_ms=6000000
|
|
280
|
+
|
|
281
|
+
while [ "$#" -gt 0 ]; do
|
|
282
|
+
case "$1" in
|
|
283
|
+
--write)
|
|
284
|
+
write_mode=1
|
|
285
|
+
shift
|
|
286
|
+
;;
|
|
287
|
+
--base-url)
|
|
288
|
+
bootstrap_base_url=$2
|
|
289
|
+
shift 2
|
|
290
|
+
;;
|
|
291
|
+
--token)
|
|
292
|
+
bootstrap_token=$2
|
|
293
|
+
shift 2
|
|
294
|
+
;;
|
|
295
|
+
--model)
|
|
296
|
+
bootstrap_model=$2
|
|
297
|
+
shift 2
|
|
298
|
+
;;
|
|
299
|
+
--timeout-ms)
|
|
300
|
+
bootstrap_timeout_ms=$2
|
|
301
|
+
shift 2
|
|
302
|
+
;;
|
|
303
|
+
*)
|
|
304
|
+
echo "unknown bootstrap option: $1" >&2
|
|
305
|
+
exit 1
|
|
306
|
+
;;
|
|
307
|
+
esac
|
|
308
|
+
done
|
|
309
|
+
|
|
310
|
+
[ -n "$bootstrap_base_url" ] || bootstrap_base_url="http://127.0.0.1:${PORT:-3000}"
|
|
311
|
+
[ -n "$bootstrap_model" ] || bootstrap_model="${PRIMARY_MODEL:-}"
|
|
312
|
+
[ -n "$bootstrap_token" ] || bootstrap_token="${INGRESS_API_KEY:-anthmorph-local}"
|
|
313
|
+
|
|
314
|
+
settings_json=$(BOOTSTRAP_TOKEN="$bootstrap_token" \
|
|
315
|
+
BOOTSTRAP_BASE_URL="$bootstrap_base_url" \
|
|
316
|
+
BOOTSTRAP_TIMEOUT_MS="$bootstrap_timeout_ms" \
|
|
317
|
+
BOOTSTRAP_MODEL="$bootstrap_model" \
|
|
318
|
+
python3 - <<'PY'
|
|
319
|
+
import json
|
|
320
|
+
import os
|
|
321
|
+
|
|
322
|
+
model = os.environ["BOOTSTRAP_MODEL"]
|
|
323
|
+
payload = {
|
|
324
|
+
"env": {
|
|
325
|
+
"ANTHROPIC_AUTH_TOKEN": os.environ["BOOTSTRAP_TOKEN"],
|
|
326
|
+
"ANTHROPIC_BASE_URL": os.environ["BOOTSTRAP_BASE_URL"],
|
|
327
|
+
"API_TIMEOUT_MS": os.environ["BOOTSTRAP_TIMEOUT_MS"],
|
|
328
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL": model,
|
|
329
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL": model,
|
|
330
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL": model,
|
|
331
|
+
"CLAUDE_CODE_SUBAGENT_MODEL": model,
|
|
332
|
+
"ANTHROPIC_SMALL_FAST_MODEL": model,
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
print(json.dumps(payload, indent=2))
|
|
336
|
+
PY
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if [ "$write_mode" -eq 1 ]; then
|
|
340
|
+
mkdir -p "$HOME/.claude"
|
|
341
|
+
SETTINGS_FILE="$HOME/.claude/settings.json" SETTINGS_JSON="$settings_json" python3 - <<'PY'
|
|
342
|
+
import json
|
|
343
|
+
import os
|
|
344
|
+
from pathlib import Path
|
|
345
|
+
|
|
346
|
+
settings_path = Path(os.environ["SETTINGS_FILE"])
|
|
347
|
+
incoming = json.loads(os.environ["SETTINGS_JSON"])
|
|
348
|
+
existing = {}
|
|
349
|
+
if settings_path.exists():
|
|
350
|
+
try:
|
|
351
|
+
existing = json.loads(settings_path.read_text())
|
|
352
|
+
except Exception:
|
|
353
|
+
existing = {}
|
|
354
|
+
|
|
355
|
+
merged_env = existing.get("env", {}) if isinstance(existing.get("env"), dict) else {}
|
|
356
|
+
merged_env.update(incoming.get("env", {}))
|
|
357
|
+
existing["env"] = merged_env
|
|
358
|
+
settings_path.write_text(json.dumps(existing, indent=2) + "\n")
|
|
359
|
+
|
|
360
|
+
claude_json_path = Path.home() / ".claude.json"
|
|
361
|
+
claude_json = {}
|
|
362
|
+
if claude_json_path.exists():
|
|
363
|
+
try:
|
|
364
|
+
claude_json = json.loads(claude_json_path.read_text())
|
|
365
|
+
except Exception:
|
|
366
|
+
claude_json = {}
|
|
367
|
+
claude_json["hasCompletedOnboarding"] = True
|
|
368
|
+
claude_json_path.write_text(json.dumps(claude_json, indent=2) + "\n")
|
|
369
|
+
PY
|
|
370
|
+
echo "wrote $HOME/.claude/settings.json"
|
|
371
|
+
echo "set $HOME/.claude.json onboarding flag"
|
|
372
|
+
else
|
|
373
|
+
printf '%s\n' "$settings_json"
|
|
374
|
+
fi
|
|
375
|
+
}
|
|
376
|
+
|
|
252
377
|
set_cmd() {
|
|
253
378
|
ensure_config
|
|
254
379
|
load_config
|
|
@@ -337,8 +462,9 @@ start_cmd() {
|
|
|
337
462
|
set -- "$BIN_PATH" \
|
|
338
463
|
--port "$PORT" \
|
|
339
464
|
--backend-profile "$BACKEND_PROFILE" \
|
|
465
|
+
--compat-mode "${COMPAT_MODE:-strict}" \
|
|
340
466
|
--backend-url "$BACKEND_URL" \
|
|
341
|
-
--model "$
|
|
467
|
+
--model "$PRIMARY_MODEL" \
|
|
342
468
|
--api-key "$api_key"
|
|
343
469
|
|
|
344
470
|
if [ -n "${REASONING_MODEL:-}" ]; then
|
|
@@ -410,8 +536,10 @@ status_cmd() {
|
|
|
410
536
|
fi
|
|
411
537
|
fi
|
|
412
538
|
echo "profile: ${BACKEND_PROFILE:-}"
|
|
539
|
+
echo "compat_mode: ${COMPAT_MODE:-strict}"
|
|
413
540
|
echo "backend_url: ${BACKEND_URL:-}"
|
|
414
|
-
echo "
|
|
541
|
+
echo "primary_model: ${PRIMARY_MODEL:-}"
|
|
542
|
+
echo "fallback_models: ${FALLBACK_MODELS:-}"
|
|
415
543
|
echo "port: ${PORT:-}"
|
|
416
544
|
if [ -n "${API_KEY_ENV:-}" ]; then
|
|
417
545
|
key_source=env:$API_KEY_ENV
|
|
@@ -472,6 +600,20 @@ case "$cmd" in
|
|
|
472
600
|
print-config)
|
|
473
601
|
print_config_cmd
|
|
474
602
|
;;
|
|
603
|
+
bootstrap)
|
|
604
|
+
shift
|
|
605
|
+
subcmd=${1:-}
|
|
606
|
+
case "$subcmd" in
|
|
607
|
+
claude-code)
|
|
608
|
+
shift
|
|
609
|
+
bootstrap_claude_code_cmd "$@"
|
|
610
|
+
;;
|
|
611
|
+
*)
|
|
612
|
+
echo "unknown bootstrap target: $subcmd" >&2
|
|
613
|
+
exit 1
|
|
614
|
+
;;
|
|
615
|
+
esac
|
|
616
|
+
;;
|
|
475
617
|
help|-h|--help)
|
|
476
618
|
usage
|
|
477
619
|
;;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
SCRIPT_PATH=$(readlink -f -- "$0" 2>/dev/null || printf "%s" "$0")
|
|
5
|
+
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")/.." && pwd)
|
|
6
|
+
|
|
7
|
+
docker run --rm \
|
|
8
|
+
-v "$ROOT_DIR:/work" \
|
|
9
|
+
-w /work \
|
|
10
|
+
rust:1.89-bookworm \
|
|
11
|
+
sh -lc 'export PATH=/usr/local/cargo/bin:$PATH; cargo build --release'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
SCRIPT_PATH=$(readlink -f -- "$0" 2>/dev/null || printf "%s" "$0")
|
|
5
|
+
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")/.." && pwd)
|
|
6
|
+
MODE=${1:-pack}
|
|
7
|
+
|
|
8
|
+
case "$MODE" in
|
|
9
|
+
pack)
|
|
10
|
+
CMD='npm pack --dry-run'
|
|
11
|
+
;;
|
|
12
|
+
publish)
|
|
13
|
+
CMD='npm publish --dry-run'
|
|
14
|
+
;;
|
|
15
|
+
*)
|
|
16
|
+
echo "usage: $0 [pack|publish]" >&2
|
|
17
|
+
exit 1
|
|
18
|
+
;;
|
|
19
|
+
esac
|
|
20
|
+
|
|
21
|
+
docker run --rm \
|
|
22
|
+
-v "$ROOT_DIR:/work" \
|
|
23
|
+
-w /work \
|
|
24
|
+
node:22-bookworm \
|
|
25
|
+
sh -lc "$CMD"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
SCRIPT_PATH=$(readlink -f -- "$0" 2>/dev/null || printf "%s" "$0")
|
|
5
|
+
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")/.." && pwd)
|
|
6
|
+
|
|
7
|
+
echo "[1/4] Secret scan"
|
|
8
|
+
sh "$ROOT_DIR/scripts/docker_secret_scan.sh"
|
|
9
|
+
|
|
10
|
+
echo "[2/4] Rust tests"
|
|
11
|
+
sh "$ROOT_DIR/scripts/docker_rust_test.sh"
|
|
12
|
+
|
|
13
|
+
echo "[3/4] Linux release build"
|
|
14
|
+
sh "$ROOT_DIR/scripts/docker_build_linux.sh"
|
|
15
|
+
|
|
16
|
+
echo "[4/4] npm dry-runs"
|
|
17
|
+
sh "$ROOT_DIR/scripts/docker_npm_dry_run.sh"
|
|
18
|
+
sh "$ROOT_DIR/scripts/docker_npm_dry_run.sh" publish
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
SCRIPT_PATH=$(readlink -f -- "$0" 2>/dev/null || printf "%s" "$0")
|
|
5
|
+
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")/.." && pwd)
|
|
6
|
+
PAYLOAD_DIR_DEFAULT=/opt/claude-proxy/tests/payloads
|
|
7
|
+
|
|
8
|
+
set -- docker run --rm -v "$ROOT_DIR:/work" -w /work
|
|
9
|
+
|
|
10
|
+
if [ -d "${ANTHMORPH_CLAUDE_PAYLOAD_DIR:-$PAYLOAD_DIR_DEFAULT}" ]; then
|
|
11
|
+
PAYLOAD_DIR_HOST=${ANTHMORPH_CLAUDE_PAYLOAD_DIR:-$PAYLOAD_DIR_DEFAULT}
|
|
12
|
+
set -- "$@" -v "$PAYLOAD_DIR_HOST:$PAYLOAD_DIR_HOST:ro"
|
|
13
|
+
ANTHMORPH_CLAUDE_PAYLOAD_DIR=$PAYLOAD_DIR_HOST
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
for env_name in \
|
|
17
|
+
CHUTES_API_KEY \
|
|
18
|
+
CHUTES_BASE_URL \
|
|
19
|
+
CHUTES_MODEL \
|
|
20
|
+
MINIMAX_API_KEY \
|
|
21
|
+
MINIMAX_BASE_URL \
|
|
22
|
+
MINIMAX_MODEL \
|
|
23
|
+
ALIBABA_CODE_API_KEY \
|
|
24
|
+
ALIBABA_BASE_URL \
|
|
25
|
+
ALIBABA_MODEL \
|
|
26
|
+
ANTHMORPH_CLAUDE_PAYLOAD_DIR
|
|
27
|
+
do
|
|
28
|
+
eval "env_value=\${$env_name-}"
|
|
29
|
+
if [ -n "${env_value:-}" ]; then
|
|
30
|
+
set -- "$@" -e "$env_name=$env_value"
|
|
31
|
+
fi
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
set -- "$@" rust:1.89-bookworm sh -lc 'export PATH=/usr/local/cargo/bin:$PATH; cargo test -- --nocapture'
|
|
35
|
+
"$@"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
SCRIPT_PATH=$(readlink -f -- "$0" 2>/dev/null || printf "%s" "$0")
|
|
5
|
+
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")/.." && pwd)
|
|
6
|
+
|
|
7
|
+
docker run --rm \
|
|
8
|
+
-v "$ROOT_DIR:/repo" \
|
|
9
|
+
-w /repo \
|
|
10
|
+
zricethezav/gitleaks:latest \
|
|
11
|
+
detect --source . --no-git --redact
|
package/scripts/postinstall.js
CHANGED
|
@@ -46,7 +46,16 @@ function ensurePrebuiltPermissions() {
|
|
|
46
46
|
|
|
47
47
|
function buildRelease() {
|
|
48
48
|
if (!hasCargo()) {
|
|
49
|
-
console.error(
|
|
49
|
+
console.error(
|
|
50
|
+
[
|
|
51
|
+
"[anthmorph] cargo not found; cannot build a local Linux/macOS binary.",
|
|
52
|
+
"[anthmorph] Supported install paths:",
|
|
53
|
+
" 1. install Rust/Cargo and rerun the install",
|
|
54
|
+
" 2. run scripts/docker_build_linux.sh from the package checkout",
|
|
55
|
+
" 3. use Termux on Android/aarch64 to consume the bundled prebuilt",
|
|
56
|
+
"[anthmorph] See docs/PACKAGING.md for details.",
|
|
57
|
+
].join("\n"),
|
|
58
|
+
);
|
|
50
59
|
process.exit(1);
|
|
51
60
|
}
|
|
52
61
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
PROFILE=${1:-chutes}
|
|
5
|
+
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
6
|
+
PAYLOAD_DIR=${ANTHMORPH_CLAUDE_PAYLOAD_DIR:-/opt/claude-proxy/tests/payloads}
|
|
7
|
+
BIN_PATH=${ANTHMORPH_BIN:-$ROOT_DIR/target/release/anthmorph}
|
|
8
|
+
PORT=${ANTHMORPH_TEST_PORT:-3119}
|
|
9
|
+
LOG_FILE=${ANTHMORPH_TEST_LOG:-$ROOT_DIR/.anthmorph/claude-code-real.log}
|
|
10
|
+
PID_FILE=${ANTHMORPH_TEST_PID:-$ROOT_DIR/.anthmorph/claude-code-real.pid}
|
|
11
|
+
|
|
12
|
+
mkdir -p "$(dirname -- "$LOG_FILE")"
|
|
13
|
+
|
|
14
|
+
if [ ! -d "$PAYLOAD_DIR" ]; then
|
|
15
|
+
echo "payload dir not found: $PAYLOAD_DIR" >&2
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
if [ ! -x "$BIN_PATH" ]; then
|
|
20
|
+
cargo build --release --quiet
|
|
21
|
+
BIN_PATH=$ROOT_DIR/target/release/anthmorph
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
case "$PROFILE" in
|
|
25
|
+
chutes)
|
|
26
|
+
: "${CHUTES_API_KEY:?missing CHUTES_API_KEY}"
|
|
27
|
+
BACKEND_PROFILE=chutes
|
|
28
|
+
BACKEND_URL=${CHUTES_BASE_URL:-https://llm.chutes.ai/v1}
|
|
29
|
+
MODEL=${CHUTES_MODEL:-deepseek-ai/DeepSeek-V3.2-TEE}
|
|
30
|
+
API_KEY=$CHUTES_API_KEY
|
|
31
|
+
EXPECT_NO_THINK=0
|
|
32
|
+
;;
|
|
33
|
+
minimax)
|
|
34
|
+
: "${MINIMAX_API_KEY:?missing MINIMAX_API_KEY}"
|
|
35
|
+
BACKEND_PROFILE=openai-generic
|
|
36
|
+
BACKEND_URL=${MINIMAX_BASE_URL:-https://api.minimax.io/v1}
|
|
37
|
+
MODEL=${MINIMAX_MODEL:-MiniMax-M2.5}
|
|
38
|
+
API_KEY=$MINIMAX_API_KEY
|
|
39
|
+
EXPECT_NO_THINK=1
|
|
40
|
+
;;
|
|
41
|
+
*)
|
|
42
|
+
echo "unsupported profile: $PROFILE" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
;;
|
|
45
|
+
esac
|
|
46
|
+
|
|
47
|
+
cleanup() {
|
|
48
|
+
if [ -f "$PID_FILE" ]; then
|
|
49
|
+
pid=$(cat "$PID_FILE" 2>/dev/null || true)
|
|
50
|
+
if [ -n "${pid:-}" ]; then
|
|
51
|
+
kill "$pid" 2>/dev/null || true
|
|
52
|
+
wait "$pid" 2>/dev/null || true
|
|
53
|
+
fi
|
|
54
|
+
rm -f "$PID_FILE"
|
|
55
|
+
fi
|
|
56
|
+
}
|
|
57
|
+
trap cleanup EXIT INT TERM
|
|
58
|
+
|
|
59
|
+
"$BIN_PATH" \
|
|
60
|
+
--port "$PORT" \
|
|
61
|
+
--backend-profile "$BACKEND_PROFILE" \
|
|
62
|
+
--compat-mode compat \
|
|
63
|
+
--backend-url "$BACKEND_URL" \
|
|
64
|
+
--model "$MODEL" \
|
|
65
|
+
--api-key "$API_KEY" \
|
|
66
|
+
>"$LOG_FILE" 2>&1 &
|
|
67
|
+
echo $! > "$PID_FILE"
|
|
68
|
+
|
|
69
|
+
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
|
|
70
|
+
if curl -fsS "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
|
71
|
+
break
|
|
72
|
+
fi
|
|
73
|
+
sleep 1
|
|
74
|
+
done
|
|
75
|
+
|
|
76
|
+
PAYLOADS="
|
|
77
|
+
basic_request.json
|
|
78
|
+
content_blocks_text.json
|
|
79
|
+
content_blocks_mixed.json
|
|
80
|
+
conversation_3_system.json
|
|
81
|
+
conversation_2_followup.json
|
|
82
|
+
conversation_4_tools.json
|
|
83
|
+
tool_result.json
|
|
84
|
+
claude_code_adaptive_thinking.json
|
|
85
|
+
cache_control_request.json
|
|
86
|
+
documents_request.json
|
|
87
|
+
unknown_content_blocks.json
|
|
88
|
+
multi_tool_request.json
|
|
89
|
+
"
|
|
90
|
+
|
|
91
|
+
passed=0
|
|
92
|
+
quarantined=0
|
|
93
|
+
|
|
94
|
+
is_retryable() {
|
|
95
|
+
case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in
|
|
96
|
+
*"maximum capacity"*|*"try again later"*|*"rate limit"*|*"temporarily unavailable"*|*"overloaded"*|*"timeout"*)
|
|
97
|
+
return 0
|
|
98
|
+
;;
|
|
99
|
+
esac
|
|
100
|
+
return 1
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for payload_name in $PAYLOADS; do
|
|
104
|
+
payload_file=$PAYLOAD_DIR/$payload_name
|
|
105
|
+
payload=$(sed "s|{{MODEL}}|$MODEL|g" "$payload_file")
|
|
106
|
+
response_file=$(mktemp)
|
|
107
|
+
status=$(curl -sS -N -o "$response_file" -w "%{http_code}" \
|
|
108
|
+
"http://127.0.0.1:$PORT/v1/messages" \
|
|
109
|
+
-H 'content-type: application/json' \
|
|
110
|
+
-d "$payload")
|
|
111
|
+
body=$(cat "$response_file")
|
|
112
|
+
rm -f "$response_file"
|
|
113
|
+
|
|
114
|
+
if [ "$status" != "200" ]; then
|
|
115
|
+
if is_retryable "$body" || [ "$status" = "429" ] || [ "$status" -ge 500 ] 2>/dev/null; then
|
|
116
|
+
echo "QUARANTINE $payload_name status=$status"
|
|
117
|
+
quarantined=$((quarantined + 1))
|
|
118
|
+
continue
|
|
119
|
+
fi
|
|
120
|
+
echo "FAIL $payload_name status=$status"
|
|
121
|
+
printf '%s\n' "$body"
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
printf '%s' "$body" | grep -q 'event: message_start' || {
|
|
126
|
+
echo "FAIL $payload_name missing message_start"
|
|
127
|
+
printf '%s\n' "$body"
|
|
128
|
+
exit 1
|
|
129
|
+
}
|
|
130
|
+
printf '%s' "$body" | grep -q 'event: message_stop' || {
|
|
131
|
+
echo "FAIL $payload_name missing message_stop"
|
|
132
|
+
printf '%s\n' "$body"
|
|
133
|
+
exit 1
|
|
134
|
+
}
|
|
135
|
+
if printf '%s' "$body" | grep -q '"choices"'; then
|
|
136
|
+
echo "FAIL $payload_name leaked OpenAI wire format"
|
|
137
|
+
printf '%s\n' "$body"
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
if [ "$EXPECT_NO_THINK" = "1" ] && printf '%s' "$body" | grep -q '<think>'; then
|
|
141
|
+
echo "FAIL $payload_name leaked <think> tags"
|
|
142
|
+
printf '%s\n' "$body"
|
|
143
|
+
exit 1
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
echo "PASS $payload_name"
|
|
147
|
+
passed=$((passed + 1))
|
|
148
|
+
done
|
|
149
|
+
|
|
150
|
+
echo "passed=$passed quarantined=$quarantined profile=$PROFILE"
|
package/src/config.rs
CHANGED
|
@@ -37,3 +37,36 @@ impl FromStr for BackendProfile {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
|
42
|
+
pub enum CompatMode {
|
|
43
|
+
Strict,
|
|
44
|
+
Compat,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
impl CompatMode {
|
|
48
|
+
pub fn as_str(self) -> &'static str {
|
|
49
|
+
match self {
|
|
50
|
+
CompatMode::Strict => "strict",
|
|
51
|
+
CompatMode::Compat => "compat",
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pub fn is_strict(self) -> bool {
|
|
56
|
+
matches!(self, CompatMode::Strict)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
impl FromStr for CompatMode {
|
|
61
|
+
type Err = String;
|
|
62
|
+
|
|
63
|
+
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
|
64
|
+
match value.trim().to_ascii_lowercase().as_str() {
|
|
65
|
+
"strict" => Ok(Self::Strict),
|
|
66
|
+
"compat" | "compatible" => Ok(Self::Compat),
|
|
67
|
+
other => Err(format!(
|
|
68
|
+
"unsupported compat mode '{other}', expected 'strict' or 'compat'"
|
|
69
|
+
)),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/main.rs
CHANGED
|
@@ -6,7 +6,7 @@ mod transform;
|
|
|
6
6
|
|
|
7
7
|
use axum::{routing::post, Extension, Router};
|
|
8
8
|
use clap::Parser;
|
|
9
|
-
use config::BackendProfile;
|
|
9
|
+
use config::{BackendProfile, CompatMode};
|
|
10
10
|
use proxy::{build_cors_layer, Config};
|
|
11
11
|
use reqwest::Client;
|
|
12
12
|
use std::sync::Arc;
|
|
@@ -29,6 +29,8 @@ struct Cli {
|
|
|
29
29
|
api_key: Option<String>,
|
|
30
30
|
#[arg(long, value_enum)]
|
|
31
31
|
backend_profile: Option<BackendProfile>,
|
|
32
|
+
#[arg(long, value_enum)]
|
|
33
|
+
compat_mode: Option<CompatMode>,
|
|
32
34
|
#[arg(long)]
|
|
33
35
|
ingress_api_key: Option<String>,
|
|
34
36
|
#[arg(long)]
|
|
@@ -48,7 +50,7 @@ async fn main() -> anyhow::Result<()> {
|
|
|
48
50
|
config.backend_url = url;
|
|
49
51
|
}
|
|
50
52
|
if let Some(m) = cli.model {
|
|
51
|
-
config.
|
|
53
|
+
config.primary_model = m;
|
|
52
54
|
}
|
|
53
55
|
if let Some(m) = cli.reasoning_model {
|
|
54
56
|
config.reasoning_model = Some(m);
|
|
@@ -59,6 +61,9 @@ async fn main() -> anyhow::Result<()> {
|
|
|
59
61
|
if let Some(profile) = cli.backend_profile {
|
|
60
62
|
config.backend_profile = profile;
|
|
61
63
|
}
|
|
64
|
+
if let Some(mode) = cli.compat_mode {
|
|
65
|
+
config.compat_mode = mode;
|
|
66
|
+
}
|
|
62
67
|
if let Some(k) = cli.ingress_api_key {
|
|
63
68
|
config.ingress_api_key = Some(k);
|
|
64
69
|
}
|
|
@@ -77,7 +82,8 @@ async fn main() -> anyhow::Result<()> {
|
|
|
77
82
|
tracing::info!("AnthMorph v{}", env!("CARGO_PKG_VERSION"));
|
|
78
83
|
tracing::info!("Backend URL: {}", config.backend_url);
|
|
79
84
|
tracing::info!("Backend Profile: {}", config.backend_profile.as_str());
|
|
80
|
-
tracing::info!("
|
|
85
|
+
tracing::info!("Compat Mode: {}", config.compat_mode.as_str());
|
|
86
|
+
tracing::info!("Primary Model: {}", config.primary_model);
|
|
81
87
|
if let Some(ref m) = config.reasoning_model {
|
|
82
88
|
tracing::info!("Reasoning Model: {}", m);
|
|
83
89
|
}
|
|
@@ -93,6 +99,11 @@ async fn main() -> anyhow::Result<()> {
|
|
|
93
99
|
|
|
94
100
|
let app = Router::new()
|
|
95
101
|
.route("/v1/messages", post(proxy::proxy_handler))
|
|
102
|
+
.route(
|
|
103
|
+
"/v1/messages/count_tokens",
|
|
104
|
+
post(proxy::count_tokens_handler),
|
|
105
|
+
)
|
|
106
|
+
.route("/v1/models", axum::routing::get(proxy::models_handler))
|
|
96
107
|
.route("/health", axum::routing::get(health_handler))
|
|
97
108
|
.layer(Extension(config.clone()))
|
|
98
109
|
.layer(Extension(client))
|
|
@@ -115,6 +126,14 @@ async fn main() -> anyhow::Result<()> {
|
|
|
115
126
|
Ok(())
|
|
116
127
|
}
|
|
117
128
|
|
|
118
|
-
async fn health_handler(
|
|
119
|
-
|
|
129
|
+
async fn health_handler(
|
|
130
|
+
Extension(config): Extension<Arc<Config>>,
|
|
131
|
+
) -> axum::Json<serde_json::Value> {
|
|
132
|
+
axum::Json(serde_json::json!({
|
|
133
|
+
"status": "ok",
|
|
134
|
+
"backend_profile": config.backend_profile.as_str(),
|
|
135
|
+
"compat_mode": config.compat_mode.as_str(),
|
|
136
|
+
"resolved_model": config.primary_model,
|
|
137
|
+
"port": config.port,
|
|
138
|
+
}))
|
|
120
139
|
}
|