@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.
@@ -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 'MODEL=%s\n' "${MODEL:-}"
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
- MODEL=$2
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
- MODEL=
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
- MODEL=Qwen/Qwen3.5-397B-A17B-TEE,zai-org/GLM-5-TEE,deepseek-ai/DeepSeek-V3.2-TEE
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
- MODEL=${MINIMAX_MODEL:-MiniMax-M2.5}
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 "$MODEL" ] || [ -z "$BACKEND_PROFILE" ]; then
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 "$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 "model: ${MODEL:-}"
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
@@ -46,7 +46,16 @@ function ensurePrebuiltPermissions() {
46
46
 
47
47
  function buildRelease() {
48
48
  if (!hasCargo()) {
49
- console.error("[anthmorph] cargo not found; cannot build local binary");
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.model = m;
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!("Model: {}", config.model);
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() -> &'static str {
119
- "OK"
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
  }